Last active
June 9, 2022 23:34
-
-
Save alexcthomas/6df11f8a7b10a40a1dbc6adf7440995f to your computer and use it in GitHub Desktop.
Copy data from a Time Machine volume mounted on a Linux box.
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
""" | |
A script to rebuild an Apple Time Machine (TM) drive somewhere sensible | |
The original and best is from here: | |
https://gist.github.com/vjt/5183305 | |
""" | |
import os | |
import sys | |
import pdb | |
import shutil | |
import argparse | |
import subprocess | |
import os.path as osp | |
HFS_PREFIX = '.HFS+ Private Directory Data' | |
BACKUP_LIST_DIRECTORY = 'Backups.backupdb' | |
def find_hfs_directory(dirpath): | |
root_dirs = os.listdir(dirpath) | |
if BACKUP_LIST_DIRECTORY not in root_dirs: | |
return | |
for d in root_dirs: | |
if d.startswith(HFS_PREFIX): | |
return osp.join(dirpath, d) | |
def main(src, target, root_dir=None): | |
if not osp.exists(src): | |
raise ValueError('Source directory does not exist') | |
if root_dir is None: | |
root_dir = src | |
hfs_dir = None | |
while True: | |
hfs_dir = find_hfs_directory(root_dir) | |
if hfs_dir is not None: | |
print('Root directory found at ' + root_dir) | |
break | |
root_dir = osp.dirname(root_dir) | |
if root_dir == '/': | |
break | |
if hfs_dir is None: | |
print('Could not find Time Machine (TM) root directory. Please specify it manually.') | |
else: | |
if BACKUP_LIST_DIRECTORY not in root_dir: | |
raise RuntimeError(BACKUP_LIST_DIRECTORY+' not found in the root directory.') | |
if HFS_PREFIX not in root_dir: | |
raise RuntimeError(HFS_PREFIX+' not found in the root directory.') | |
if not osp.exists(target): | |
os.makedirs(target) | |
inner(src, target, hfs_dir) | |
def inner(src, target, hfsd): | |
cmd = ['find', src, '-mindepth', '1', '-maxdepth', '1', '-and', '-not', '-name', '.', '-and', '-not', '-name', '..'] | |
try: | |
result = subprocess.check_output(cmd).decode() | |
except Exception as e: | |
print('failed to run find for', src) | |
print(e) | |
return | |
for entry in result.split('\n')[:-1]: | |
if '.DS_Store' in entry: | |
continue | |
dest = osp.join(target, osp.basename(entry)) | |
cmd = ['stat', '-c', "'%h %F'", entry] | |
try: | |
stat_result = subprocess.check_output(cmd).decode().strip("\n'") | |
except Exception as e: | |
print('failed to stat', entry) | |
print(e) | |
continue | |
hlnum, _, typ = stat_result.partition(' ') | |
# if typ in ('regular file', 'symbolic link'): | |
if typ in ('regular file',): | |
if not osp.exists(dest): | |
try: | |
shutil.copy2(entry, dest) | |
except Exception as e: | |
print('failed to copy', entry) | |
print(e) | |
continue | |
elif typ == 'directory': | |
print(dest) | |
if not osp.exists(dest): | |
os.makedirs(dest) | |
inner(entry, dest, hfsd) | |
elif typ == 'regular empty file': | |
hldir = osp.join(hfsd, 'dir_'+hlnum) | |
if osp.exists(hldir): | |
if not osp.exists(dest): | |
os.makedirs(dest) | |
inner(hldir, dest, hfsd) | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser( | |
description='Rebuild an Apple Time Machine (TM) drive somewhere sensible', | |
formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |
parser.add_argument('source', type=str, | |
help='Location of the Time Machine (TM) backup you wish to restore. Should end with something like "Backups.backupdb/<machine name>/<backup version>/Macintosh HD"') | |
parser.add_argument('target', type=str, | |
help='Where you want the data to end up') | |
parser.add_argument('-r', '--root_dir', type=str, default=None, | |
help='Location of Time Machine (TM) root directory. Only needed if this script fails to infer it from the source argument.') | |
params = parser.parse_args() | |
main(params.source, params.target, params.root_dir) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment