Skip to content

Instantly share code, notes, and snippets.

@alexcthomas
Last active June 9, 2022 23:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexcthomas/6df11f8a7b10a40a1dbc6adf7440995f to your computer and use it in GitHub Desktop.
Save alexcthomas/6df11f8a7b10a40a1dbc6adf7440995f to your computer and use it in GitHub Desktop.
Copy data from a Time Machine volume mounted on a Linux box.
"""
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