Skip to content

Instantly share code, notes, and snippets.

@eahrold
Forked from pudquick/mount_shares.py
Last active November 21, 2020 16:11
Show Gist options
  • Save eahrold/14e8e14567a38c05f7ca to your computer and use it in GitHub Desktop.
Save eahrold/14e8e14567a38c05f7ca to your computer and use it in GitHub Desktop.
Programmatically mount shares in OS X via python without the need for AppleScript
import os
import objc, CoreFoundation
from ctypes import c_void_p, pointer, cast
# The only reason I'm doing this the XML way is because I don't have a better way (yet)
# for correcting a function signature -after- it's already been imported.
# The problem is the last argument is a pointer to a CFArrayRef, which works out to a
# pointer to a pointer to a CFArray. pyobjc doesn't handle that much abstraction, so I created
# a custom opaque type 'CFArrayRefRef' and manually handle the conversion to/from pointer.
NetFS_bridgesupport = \
"""<?xml version='1.0'?>
<!DOCTYPE signatures SYSTEM "file://localhost/System/Library/DTDs/BridgeSupport.dtd">
<signatures version='1.0'>
<depends_on path='/System/Library/Frameworks/SystemConfiguration.framework'/>
<depends_on path='/System/Library/Frameworks/CoreFoundation.framework'/>
<depends_on path='/System/Library/Frameworks/DiskArbitration.framework'/>
<string_constant name='kNAUIOptionKey' nsstring='true' value='UIOption'/>
<string_constant name='kNAUIOptionNoUI' nsstring='true' value='NoUI'/>
<string_constant name='kNetFSAllowSubMountsKey' nsstring='true' value='AllowSubMounts'/>
<string_constant name='kNetFSMountAtMountDirKey' nsstring='true' value='MountAtMountDir'/>
<opaque name='CFArrayRefRef' type='^{CFArrayRefRef=}' />
<function name='NetFSMountURLSync'>
<arg type='^{__CFURL=}'/>
<arg type='^{__CFURL=}'/>
<arg type='^{__CFString=}'/>
<arg type='^{__CFString=}'/>
<arg type='^{__CFDictionary=}'/>
<arg type='^{__CFDictionary=}'/>
<arg type='^{CFArrayRefRef=}'/>
<retval type='i'/>
</function>
</signatures>"""
# This is fun - lets you refer dict keys like dict.keyname
class attrdict(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
# Create 'NetFS' framework object from XML above
NetFS = attrdict()
objc.parseBridgeSupport(NetFS_bridgesupport,
NetFS,
objc.pathForFramework('NetFS.framework'))
class ArrayPair(object):
def __init__(self):
# pylint: disable=bad-super-call
super(type(self), self).__init__()
# Build a pointer to a null array (which OS X will replace anyways)
self.cArray = pointer(c_void_p(None))
# Now we cast it to our custom opaque type
self.oArray = NetFS.CFArrayRefRef(c_void_p=cast(self.cArray, c_void_p))
def contents(self):
# Cast the pointer cArray now points to into an
# objc object (CFArray/NSArray here)
# pylint: disable=no-member
return [str(x) for x in objc.objc_object(c_void_p=self.cArray.contents)]
class OSXMounterError(Exception):
'''Exception to throw if git fails
TODO: it'd be nice to stub out some of the common error codes here
and provide some usefull feedback about the cause.
Error Codes: -128 = User cancelled
-17 = Already mounted
-97 = Authentication issue
'''
pass
class OSXMounter(object):
'''Mount a share point'''
def __init__(self, base_url, username=None, password=None,
allow_interaction=False, allow_submounts=True):
'''
base_url: The FQDN or IP address of the server
username: Authentication username
password: Authentication password
allow_interaction: Should user interaction be allowed, such as
authentication or selectable share point window.
Defaults to False.
allow_submounts: Should submounts be allowed. Defaults to True
'''
# pylint: disable=bad-super-call
super(type(self), self).__init__()
self._base_url = base_url
self._current_mounts = {}
self.username = username
self.password = password
self.allow_interaction = allow_interaction
self.allow_submounts = allow_submounts
self.mount_options = {}
self.open_options = {}
self.mountpaths = ArrayPair()
def connect_to_server(self):
'''Emulate Finder's "Connect To Server..."'''
# Hold onto the current allow_interaction state, and
# reset it to that value after the connect_to_server method
allow_interactions_store = self.allow_interaction
self.allow_interaction = True
results = self.mount_share("")
self.allow_interaction = allow_interactions_store
return results
def mount_share(self, share_path='', mount_path=None):
'''Mounts a share at /Volumes, returns
the mount point or raises an error'''
# pylint: disable=no-member
sh_url = CoreFoundation.CFURLCreateWithString(None,
os.path.join(self._base_url,
self._encode(share_path)),
None)
# pylint: disable=bad-super-call
# Set UI to reduced interaction
if not self.allow_interaction:
self.open_options[NetFS.kNAUIOptionKey] = NetFS.kNAUIOptionNoUI
# Allow mounting sub-directories of root shares
# Also specify the share should be mounted directly
# at (not under) mount_path
if self.allow_submounts:
self.mount_options[NetFS.kNetFSAllowSubMountsKey] = True
mo_url = None
if mount_path:
# pylint: disable=no-member
mo_url = CoreFoundation.CFURLCreateWithString(None,
mount_path,
None)
self.mount_options[NetFS.kNetFSAllowSubMountsKey] = True
self.mount_options[NetFS.kNetFSMountAtMountDirKey] = True
# Attempt to mount!
output = NetFS.NetFSMountURLSync(sh_url,
mo_url,
self.username,
self.password,
self.open_options,
self.mount_options,
self.mountpaths.oArray)
# Check if it worked
if output != 0:
if output == -128:
# User cancelled don't raise
return
raise OSXMounterError('Error mounting url "%s": %s' %
(share_path, output))
# Oh cool, it worked - return the resulting mount point path
mount = self.mountpaths.contents()[0]
# if no path was specified, grab the last path component from the mount
if share_path == '':
share_path = os.path.basename(mount)
self._current_mounts[share_path] = mount
return mount
def umount(self, share_path):
'''Unmount a volume by name'''
if share_path in self._current_mounts[share_path]:
mount_path = self._current_mounts[share_path]
print("Unmounting %s" % mount_path)
# TODO: Unmount
def _encode(self, string):
''' Handle percent encoding for a string if required.'''
# pylint: disable=no-member
if string and not "%" in string:
return CoreFoundation.CFURLCreateStringByAddingPercentEscapes(None,
string,
None,
"!*'();:@&=+$,/?%#[]",
CoreFoundation.kCFStringEncodingUTF8)
return string
# Example usage:
mounter = OSXMounter('afp://pretendco.com')
# NetFSMountURLSync handles username and password encoding.
mounter.username = 'y&urN@me'
mounter.password = 'superSecretP@$$word'
# Mount a share
mounter.mount_share('ShareName')
# Mount a second share
mounter.mount_share('Some Other Share')
# Or just emulate Finder
mounter.connect_to_server()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment