View Full Version : [all variants] script to deal with pseudo-hardlinked directories in Time Machine backups

June 16th, 2010, 06:42 AM
Hi folks. Just wanted to share a script that I wrote to migrate data off of a Time Machine backup. As anyone who's tried this knows, it's not straightforward because most directories are pseudo-hardlinked according to some hare-brained scheme Apple came up with.

(Hard-linked directories actually show up as zero-length regular files with bizarre link counts greater than 128. The link count corresponds to a directory called "dir_$N" (where $N is the link count) inside of a special directory at the root of the Time Machine volume called ".HFS+ Private Directory Data^M" (where the ^M is a literal carriage-return character).)

Here's the script (it's also attached, for convenience). It works pretty much the same as "rsync -av <SRC_DIR> <DST_DIR>" would, if the pseudo-hardlinks weren't in the way. In other words, it just recursively copies the first directory to the second.

So the first argument (the source directory) is typically something like


if you want the entire latest backup. It can also be a relative path, if you've already changed to a directory inside the backup.


# This is a script for a Linux machine to copy the contents of a Time Machine
# backup.

tm-usage() {
echo "Usage: $(basename "$0") <source_directory> <destination_directory>"
echo " where <source_directory> is inside a Time Machine volume"
exit 1

[ $# -eq 2 ] || tm-usage

SRC=$(readlink -f "$1")
DST=$(readlink -f "$2")

[ -d "$SRC" ] || {
echo "$SRC is not a directory"

[ -d "$DST" ] || {
echo "$DST is not a directory"

# Awful hack for finding the special directory which contains the pseudo-
# hardlinked directories.
HLDIR=$(echo "$(df "$SRC" | tail -1 | awk '{ print $6 }')"/.HFS+\ Private*)
[ -d "$HLDIR" ] || {
echo "I couldn't find the special directory with pseudo-hardlinks."
echo "Are you sure that"
echo " $SRC"
echo "is inside a Time Machine volume?"
echo "If so, don't give up; you probably just need to make some trivial"
echo "modification to this script to make it work."
exit 2

tm-copy() {

local SRC="$1"
local DST="$2"

# Copy the regular stuff
rsync -av "$SRC" "$DST"

cd "$SRC"
DST="$DST/$(basename "$SRC")"

# Find all the pseudo-hardlinked directories and invoke ourself recursively to
# copy their contents
find . -links +127 -printf "%P\n" | {
while read DIR; do

local DIRNUM=$(stat -c %h "$SRC/$DIR")
local DSTDIR="$DST/$(dirname "$DIR")"
local NAME="$DSTDIR/$(basename "$DIR")"

[ $(stat -c %s "$NAME") -eq 0 ] || {
echo "Sanity check failed. Bailing out."
exit 3

rm "$NAME"
tm-copy "$HLDIR/dir_$DIRNUM" "$DSTDIR"
mv "$DSTDIR/dir_$DIRNUM" "$NAME"



tm-copy "$SRC" "$DST"

July 7th, 2011, 01:10 PM
Am total n00b at this and need some help please. I havent got a clue how execute this script, and when I try I am getting this error:

time-machine-copy: 6: Syntax error: Bad function name

Any suggestions appreciated. Thanks

July 8th, 2011, 02:06 PM
There is also a fuse module which supports reading time machine backups and presenting them in a much more logical manner. Was recently covered on phoronix http://www.phoronix.com/scan.php?page=news_item&px=OTY0NQ or you can try grab it from github and test it yourself https://github.com/babali/tmfs