Skip to content

Instantly share code, notes, and snippets.

@Nathaniel-Wu
Created December 25, 2022 20:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nathaniel-Wu/21cd075f0534cc5c4bb24314a9c9b11e to your computer and use it in GitHub Desktop.
Save Nathaniel-Wu/21cd075f0534cc5c4bb24314a9c9b11e to your computer and use it in GitHub Desktop.
Current macOS Time Machine implementation on APFS volumes makes it very complicated to restore files from command line, this script shows how to do so.
#!/usr/bin/env bash
# MIT License
#
# Copyright (c) 2022 Nathaniel-Wu
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
backup_id="$1"
! grep -Fq "${backup_id}" <<< "$(tmutil listbackups)" && exit 1
src="$(realpath "$2")"
dst="$(realpath "$3")"
# for some reason mount_apfs sometimes will fail when you try to mount a snapshot older than the one previously mounted
# the workaround is to unmount and re-mount the volume
backup_volume="$(tmutil destinationinfo | grep -E '^\s*Mount Point' | gsed -E -e 's/^\s*Mount Point\s*:\s*//g' -e 's/\s*$//g')"
snapshot_mount_point="$(mktemp -d)"
if ! mount_apfs_error_message="$(mount_apfs -o 'nobrowse,rdonly' -s "com.apple.TimeMachine.${backup_id}" "${backup_volume}" "${snapshot_mount_point}" 2>&1)"; then
if [ "${mount_apfs_error_message}" == 'mount_apfs: volume could not be mounted: Resource busy' ]; then
backup_volume_info="$(diskutil info "${backup_volume}")"
backup_physical_store="$(grep -E '^\s*APFS Physical Store' <<< "${backup_volume_info}" | grep -Eo 'disk\d+')"
! diskutil unmountDisk "${backup_physical_store}" && exit $?
backup_volume_id="$(grep -E '^\s*Device Identifier' <<< "${backup_volume_info}" | grep -Eo 'disk\d+[^[:space:]]*')"
! diskutil mount "${backup_volume_id}" && exit $?
! mount_apfs -o 'nobrowse,rdonly' -s "com.apple.TimeMachine.${backup_id}" "${backup_volume}" "${snapshot_mount_point}" && exit $?
else
mount_apfs_error_code=$?
echo "${mount_apfs_error_message}" >&2
exit "${mount_apfs_error_code}"
fi
fi
src_backup="${snapshot_mount_point}/${backup_id}/Macintosh HD - Data${src}"
if [ -e "${src_backup}" ]; then
if [ -d "$src_backup" ]; then
rsync -auvpH "${src_backup}/" "${dst}"
elif [ -f "$src_backup" ]; then
rsync -auvp "${src_backup}" "${dst}"
fi
else
echo "'${src_backup}' does not exist." >&2
exit 2
fi
diskutil unmount "${snapshot_mount_point}" && rm -rf -- "${snapshot_mount_point}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment