#!/usr/bin/env bash # BITSC LICENSE NOTICE (MODIFIED ISC LICENSE) # # TIME MACHINE BLESS # # Copyright (c) 2022 Lee Ockert # https://github.com/torstenvl # # THIS WORK IS PROVIDED "AS IS" WITH NO WARRANTY OF ANY KIND. THE IMPLIED # WARRANTIES OF MERCHANTABILITY, FITNESS, NON-INFRINGEMENT, AND TITLE ARE # EXPRESSLY DISCLAIMED. NO AUTHOR SHALL BE LIABLE UNDER ANY THEORY OF LAW # FOR ANY DAMAGES OF ANY KIND RESULTING FROM THE USE OF THIS WORK. # # Permission to use, copy, modify, and/or distribute this work for any # purpose is hereby granted, provided this notice appears in all copies. function dispusage() { if [ ${#1} -gt 0 ]; then printf "%s\n\n" "${1}" fi echo "\ TIME MACHINE BLESS USAGE ${0} DESCRIPTION Time Machine Blessing modifies the metadata of a snapshot directory (i.e., a datestamped directory inside a Backups.backupdb/machinename/ directory) so that the metadata reflects a backup completed on that date and the metadata of the top-level drive matches that of the current drive. These modifications should allow restoration of files within the Time Machine restore UI. " } function canonicalname() { echo $(stat -f %R ${1}) } ############################################################################ ## PARSE & MAKE SENSE OF COMMAND LINE ## ############################################################################ if [ ! $# -eq 1 ]; then dispusage "Invalid number of arguments" && exit fi # Try to get canonical name DATEDIRNAME=$(stat -f %R ${1}) if [[ "${DATEDIRNAME}" == "" ]]; then dispusage "Could not get canonical name of directory ${1}" && exit fi # Ensure it's a directory if [ ! -d "${DATEDIRNAME}" ]; then dispusage "${DATEDIRNAME} is not a valid directory" && exit fi # Ensure it's a Backups.backupdb dir's subdir's subdir if [[ ! "${DATEDIRNAME}" =~ ^.*\/Backups\.backupdb\/.*\/.*$ ]]; then dispusage "${DATEDIRNAME} does not appear to be in a Backups.backupdb directory" && exit fi # Get the basename and ensure it's in a Time Machine timestamp format DATEDIRBASENAME=$(basename "${DATEDIRNAME}") if [[ ! "${DATEDIRBASENAME}" =~ ^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]\/?$ ]]; then dispusage "${DATEDIRBASENAME} does not appear to be a snapshot directory" && exit fi # Get the Unix timestamp from the directory's Time Machine timestamp format TIMESTAMP=$(date -j -f "%Y-%m-%d-%H%M%S" "${DATEDIRBASENAME}" +"%s") TIMESTAMP="${TIMESTAMP}100000" if [[ ! "${TIMESTAMP}" =~ ^[0-9]*$ ]]; then dispusage "Could not automatically set snapshot timestamp due to folder name format" && exit fi if [ -d "${DATEDIRNAME}/Macintosh HD - Data" ]; then DRVDIRNAME="${DATEDIRNAME}/Macintosh HD - Data" elif [ -d "${DATEDIRNAME}/Macintosh HD" ]; then DRVDIRNAME="${DATEDIRNAME}/Macintosh HD" else ## TODO: Take the output of `ls -d ${DATEDIRNAME}` and try to autodetect? dispusage "Could not find a volume name within ${DATEDIRNAME}" && exit fi ############################################################################ ## GET NECESSARY SYSTEM INFORMATION ## ############################################################################ VOLGRPUUID=`diskutil info / | awk -F' ' '/^ APFS Volume Group/{print $(NF)}'` VOLDSKUUID=`diskutil info / | awk -F' ' '/^ Volume UUID/{print $(NF)}'` if [ ! VOLGRPUUID == "" ]; then TGTUUID="${VOLGRPUUID}" elif [ ! VOLDSKUUID == "" ]; then TGTUUID="${VOLDSKUUID}" fi KERNELVER=`uname -a | sed 's/.*Version \([0-9][0-9]*\).*/\1/g'` if [ $KERNELVER -lt 20 ]; then SIMONSAYS="sudo /System/Library/Extensions/TMSafetyNet.kext/Contents/Helpers/bypass" else SIMONSAYS="sudo" fi ############################################################################ ## CONFIRM ACTION INFORMATION ## ############################################################################ printf "\n\ Snapshot Directory: %s\n\ Snapshot Volume: %s\n\ Computer Volume: %s\n\ Volume Group: %s\n\\n" "${DATEDIRNAME}" "${DRVDIRNAME}" "${VOLDSKUUID}" "${VOLGRPUUID}" printf "\ Preparing to run the following commands:\n\ %s xattr -c \"%s\"\n\ %s xattr -c \"%s\"\n\ %s xattr -w \"com.apple.backupd.SnapshotCompletionDate\" \"%s\" \"%s\"\n\ %s xattr -w \"com.apple.backupd.SnapshotState\" %s \"%s\"\n\ %s xattr -w \"com.apple.backupd.SnapshotVolumeUUID\" \"%s\" \"%s\"\n\ \n" "${SIMONSAYS}" "${DATEDIRNAME}" \ "${SIMONSAYS}" "${DRVDIRNAME}" \ "${SIMONSAYS}" "${TIMESTAMP}" "${DATEDIRNAME}" \ "${SIMONSAYS}" "4" "${DATEDIRNAME}" \ "${SIMONSAYS}" "${TGTUUID}" "${DRVDIRNAME}" printf "Does everything look right?\n" select response in "Bless Time Machine Snapshot" "ABORT ABORT ABORT!"; do if [ "${response}" == "Bless Time Machine Snapshot" ]; then "${SIMONSAYS}" xattr -c "${DATEDIRNAME}" && \ "${SIMONSAYS}" xattr -c "${DRVDIRNAME}" && \ "${SIMONSAYS}" xattr -w "com.apple.backupd.SnapshotCompletionDate" "${TIMESTAMP}" "${DATEDIRNAME}" && \ "${SIMONSAYS}" xattr -w "com.apple.backupd.SnapshotState" "4" "${DATEDIRNAME}" && \ "${SIMONSAYS}" xattr -w "com.apple.backupd.SnapshotVolumeUUID" "${TGTUUID}" "${DRVDIRNAME}" if [ ! $? -eq 0 ]; then printf "\nOperation failed.\n\n" else printf "\nOperation completed.\n\n" printf "The snapshot directory now has the following metadata:\n" printf "%s\n" "——————————————————————————————————————————————————————" xattr -lv "${DATEDIRNAME}" printf "\n\n" printf "The snapshot volume now has the following metadata:\n" printf "%s\n" "———————————————————————————————————————————————————" xattr -lv "${DRVDIRNAME}" printf "\n\n" fi break else printf "\nOperation aborted. No action has been taken.\n\n" break fi done