Created
January 6, 2020 04:15
-
-
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`.
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
#!/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