Skip to content

Instantly share code, notes, and snippets.

@pmeulen
Created September 22, 2017 17:58
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save pmeulen/2bb9099244f4fa9585f052c9df8a13cc to your computer and use it in GitHub Desktop.
Save pmeulen/2bb9099244f4fa9585f052c9df8a13cc to your computer and use it in GitHub Desktop.
Script to repair a Time Machine network volume
#!/bin/bash
set -e
###############################################################################
# This script tries to repair a Time Machine *network* backup (i.e. an APF
# share containing a sparsebundle) that is shared over a network using e.g. an
# Apple TimeCapsule, a NAS, Raspberry PI, ...
# The script must be run on the computer that created the backup
#
# This procedure is based on various pieces of wisdom floating around the
# internet. A thank you to everybody who shared!
#
# !! Use at your own risk.
# !! No guarantee whatsoever that this does what you want it to do
#
# !! Whenever possible use a wired network connection.
# !! This will much improve the speed and reliability of the repair process
#
# !! You need to set the configuration below before running this script
###############################################################################
###############################################################################
# When you get "Time Machine completed a verification of your backups. To
# improve reliability, Time Machine must create a new backup for you" that is
# of course what you could do. However, if you let Time Machine create a new
# backup it will delete *all* your existing backups and start a new backup. That
# takes a long time, and you will lose all your previous backups.
#
# Alternatively you can use this script to (try to) repair your existing backup.
# This script:
# - clears the "immutable" flag that Time Machine set when it declared the volume
# unfit
# - mounts the TimeMachine backup image (i.e. the .sparebundle) and
# uses fsck_hfs the verify and repair its filesystem
# - Upadates com.apple.TimeMachine.MachineID.plist to mark the backup as "healthy"
###############################################################################
###############################################################################
# Start configuration
# Name of the computer that created the backup.
# This is the "Computer Name" in the "Sharing" System Preferences pane.
# TODO: Set computer name
COMPUTER_NAME=""
# Set AFP_HOST, AFP_USER and AFP_VOLUME below to specify the location of the disk containing
# the sparsebundle. This is the location that contains the the disk that contains the
# $COMPUTER_NAME.sparsebundle file
# Hostname (DNS name, IP address or Bonjour) of the host to connect to
# TODO: Set host name
AFP_HOST=""
# Username for authenticating to AFP
# TODO: Set user name
AFP_USER="TimeMachine"
# Name of the AFP share to mount
# TODO: Set AFP share name
AFP_VOLUME="TMBackup"
# End configuration
###############################################################################
if [[ $(whoami) != 'root' ]]; then
echo "Script must be run as root"
exit 1
fi
# Prevent a timemache backup from interfering
echo "Disabling Time Machine"
/usr/bin/tmutil disable
# Create directory for mountpoint
MOUNTDIR=`mktemp -q -d /Volumes/Timemachine.repair.XXXX`
if [[ $? != 0 ]]; then
echo "Could not create directory for mountpoint"
exit 1
fi
echo "Created temporary mountpoint \"$MOUNTDIR\""
# Mount share. -i asks for password when needed
echo "Mounting "afp://$AFP_USER@$AFP_HOST/$AFP_VOLUME"..."
mount_afp -i afp://$AFP_USER@$AFP_HOST/$AFP_VOLUME $MOUNTDIR
if [[ $? != 0 ]]; then
echo "Mount failed"
echo "Removing temporary mountpoint"
rmdir $MOUNTDIR
exit 1
fi
echo "Mounted AFP volume at \"$MOUNTDIR\""
SPARSEBUNDLE=$MOUNTDIR/$COMPUTER_NAME.sparsebundle
if [ ! -d $SPARSEBUNDLE ]; then
echo "Could not find sparsebundle at \"$SPARSEBUNDLE\""
/sbin/umount $MOUNTDIR
/bin/rmdir $MOUNTDIR
exit 1
fi
echo "Found sparsebundle at $SPARSEBUNDLE"
PLIST=${SPARSEBUNDLE}/com.apple.TimeMachine.MachineID.plist
echo "Reading com.apple.TimeMachine.MachineID.plist from the sparsebundle"
if [ ! -e ${SPARSEBUNDLE} ]; then
echo "Could not find com.apple.TimeMachine.MachineID.plist in the sparsebudle"
/sbin/umount $MOUNTDIR
/bin/rmdir $MOUNTDIR
exit 1
fi
echo "Reading the verification state of the volume..."
VERIFICATION_STATE=`/usr/bin/defaults read $PLIST VerificationState`
echo VerificationState=$VERIFICATION_STATE
echo "0=OK; 2=failed"
# The -v -v option will list any changes made
echo "Clearing user immutable flag in sparsebundle"
/usr/bin/chflags -R -v -v nouchg "$SPARSEBUNDLE"
if [[ $? != 0 ]]; then
echo "chflags failed"
exit 1
fi
# Attach the sparsebundle, but disable any checks
echo "Attaching sparsebundle ..."
DEV_SPARSEBUNDLE=`/usr/bin/hdiutil attach -nomount -readwrite -noverify -noautofsck "$SPARSEBUNDLE" | /usr/bin/grep Apple_HFS | /usr/bin/cut -f 1 -d " "`
if [[ `echo $DEV_SPARSEBUNDLE | cut -c 1-5` != '/dev/' ]]; then
echo "Attaching sparsebundle failed"
exit 1
fi
echo "Sparsebundle was attached at \"$DEV_SPARSEBUNDLE\""
# Do a fsck of the sparsebundle
# When things are really broken, add a "-r" to the fsck to rebuild the B-tree
echo "fsck'ing sparsebundle. This will take some time..."
# -d for debug; -c to specify max memory used by the fsck
/sbin/fsck_hfs -f -y -d -c 8192m $DEV_SPARSEBUNDLE
# This is the magic sauce the will let TimeMachine accept the disk again
# The "com.apple.TimeMachine.MachineID.plist" file contains a VerificationState property
# that needs to be 0 for TM to accept the disk.
# If nonzero it will contain a RecoveryBackupDeclinedDate as well that needs to be removed
echo "Creating .backup and then updating \"$PLIST\""
/bin/cp $PLIST $PLIST.backup
# PM: defaults also converts plist from xml to binary format. Should work, but not what I want.
# So use ugly sed construction below instead (thanks, google)
#/usr/bin/defaults write $PLIST VerificationState 0
#/usr/bin/defaults delete $PLIST RecoveryBackupDeclinedDate
/usr/bin/sed -e '/RecoveryBackupDeclinedDate/{N;d;}' \
-e '/VerificationState/{n;s/2/0/;}' \
"$PLIST.backup" \
> "$PLIST"
echo "Detaching sparsebundle"
/usr/bin/hdiutil detach $DEV_SPARSEBUNDLE
echo "Unmounting AFP volume"
/sbin/umount $MOUNTDIR
if [ -d $SPARSEBUNDLE ]; then
echo "Removing temporary mountpoint"
/bin/rmdir $MOUNTDIR
fi
echo "Enabling timemachine"
/usr/bin/tmutil enable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment