-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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