Skip to content

Instantly share code, notes, and snippets.

@Tantalus13A98B5F
Created January 6, 2020 04:15
Show Gist options
  • Save Tantalus13A98B5F/2de7cd26a6d2045afaa5d222fb62c541 to your computer and use it in GitHub Desktop.
Save Tantalus13A98B5F/2de7cd26a6d2045afaa5d222fb62c541 to your computer and use it in GitHub Desktop.
An NTFS mounter for macOS, switching disks between the internal `mount_ntfs` and the additional `ntfs-3g`.
#!/usr/bin/env python3
import subprocess
import plistlib
import os
def execute_and_grab_output(args):
proc = subprocess.Popen(args, stdout=subprocess.PIPE)
return proc.stdout.read()
class UserCancel(Exception):
pass
def yes_or_no(prompt, default=''):
label = 'y/n'
if default:
default = default[0].lower()
if default == 'y':
label = 'Y/n'
elif default == 'n':
label = 'y/N'
else:
raise ValueError('Invalid default', default)
while True:
inp = input('{} [{}] '.format(prompt, label)).strip()
if inp:
inp = inp[0].lower()
else:
inp = default
if inp == 'y':
return True
elif inp == 'n':
return False
def select(optionlist, prompt='', default=0):
# for users, starts from 1
# for programmers, starts from 0
if not optionlist:
raise ValueError('optionlist should not be empty', optionlist)
prompt2 = (
'Type the number to select: '
'[q=quit, default={}] ').format(default + 1)
while True:
if prompt:
print(prompt)
for num, text in enumerate(optionlist, 1):
print(' {}) {}'.format(num, text))
inp = input(prompt2).strip()
if inp == 'q':
raise UserCancel
if inp == '':
return default
try:
inp = int(inp) - 1
item = optionlist[inp]
return inp
except:
print('Invalid selection!')
def get_disk_list():
plist = execute_and_grab_output(['diskutil', 'list', '-plist'])
return plistlib.loads(plist)
def get_device_detail(label):
plist = execute_and_grab_output(['diskutil', 'info', '-plist', label])
return plistlib.loads(plist)
def get_ntfs_volumes():
disks = get_disk_list()['AllDisks']
ret = []
for label in disks:
detail = get_device_detail(label)
if detail.get('FilesystemType') == 'ntfs':
ret.append(detail)
return ret
def repr_ntfs_volume(data):
ret = '{} => {} ({}GB, {})'.format(
data['DeviceNode'],
data['MountPoint'],
round(data['VolumeSize'] / 1000**3, 2),
'Writable' if data['WritableVolume'] else 'ReadOnly'
)
return ret
def remount(mounter, devicenode, mountpoint):
ret = subprocess.call(['sudo', 'diskutil', 'umount', devicenode])
if ret:
raise OSError('umount failed', devicenode, ret)
if not os.path.exists(mountpoint):
ret = subprocess.call(['sudo', 'mkdir', mountpoint])
if ret:
raise OSError('mkdir failed', mountpoint, ret)
ret = subprocess.call(['sudo', mounter, devicenode, mountpoint])
if ret:
raise OSError('mount failed', mounter, devicenode, mountpoint, ret)
def main():
try:
# select volume
volumes = get_ntfs_volumes()
if not volumes:
print('No NTFS volumes available!')
return
num = select(
[repr_ntfs_volume(i) for i in volumes],
'Select a volume to change:'
)
vol = volumes[num]
devicenode = vol['DeviceNode']
mountpoint = vol['MountPoint']
# select mounter
mounter_options = ['mount_ntfs', 'ntfs-3g']
num = select(
mounter_options,
'Select a mounter to use:',
0 if vol['WritableVolume'] else 1
)
mounter = mounter_options[num]
# remount
print('=>', mounter, devicenode, mountpoint)
ret = yes_or_no('Confirm to go?', 'y')
if not ret:
raise UserCancel
remount(mounter, devicenode, mountpoint)
new_detail = get_device_detail(vol['DeviceIdentifier'])
print('Succeed!', repr_ntfs_volume(new_detail))
ret = yes_or_no('Open Finder for you?', 'y')
if ret:
subprocess.call(['open', mountpoint])
except UserCancel:
print('Canceled.')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment