├── LICENSE ├── README.md ├── dev ├── dirtimefix__DO_NOT_RUN__DEVELOPMENT_ONLY.sh └── tmdiskenum__DO_NOT_RUN__DEVELOPMENT_ONLY.sh ├── dirdedupe.sh ├── tmbless.sh └── tmimport.sh /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Joshua Lee Ockert 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 12 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Time Machine Utilities** 2 | ========================== 3 | 4 | Time Machine Utilities consists of three shell scripts to assist in fixing some issues that may arise when trying to import and use older Time Machine backups, particularly those created on a previous Mac. 5 | 6 | - `tmimport.sh` imports a Time Machine backup collection, including making the metadata of the backup collection match the current computer. 7 | - `tmbless.sh` blesses an individual Time Machine backup, including making the metadata of the drive in the backup match the main drive of the current computer. 8 | - `dirdedupe.sh` is an advanced utility to manually deduplicate files using hard links, similar to how Time Machine works internally. This is sometimes helpful when a previous backup was not yet imported when creating a new Time Machine backup. 9 | 10 | **Installation** 11 | ---------------- 12 | Simply save the utilities in your path and mark them as executable. 13 | 14 | If you don't know your system path, you can find it with this Terminal command: 15 | 16 | echo "${PATH}" 17 | 18 | To mark a utility as executable, use the `chmod` command: 19 | 20 | chmod +x tmimport.sh 21 | chmod +x tmbless.sh 22 | chmod +x dirdedupe.sh 23 | 24 | If you're still lost, do this: 25 | 1. In your Home folder, create a new folder called `.bin` 26 | 2. Download and save the utilities to your new `.bin` folder 27 | 3. Use a text editor to open `.zshrc` (or `.bash_profile` on older Macs) 28 | from your Home folder and add the following line at the end: 29 | 30 | `PATH=${PATH}:${HOME}/.bin` 31 | 32 | 4. In Terminal, run the following commands: 33 | 34 | `chmod +x ~/.bin/*.sh` 35 | 36 | 5. Quit (or restart) the Terminal. 37 | 38 | ______________________________________________________________________________ 39 | 40 | 41 | **tmimport** 42 | ------------ 43 | TIME MACHINE IMPORT 44 | 45 | USAGE 46 | 47 | tmimport.sh 48 | 49 | DESCRIPTION 50 | 51 | Time Machine Importer attempts to inherit the backup history of a backup 52 | drive. If the backup drive is not paired to the current computer, Time 53 | Machine Importer will attempt to pair the drive with the current computer 54 | by modifying the model, MAC address, and UUID/UDID reflected in the 55 | metadata of the backup drive. 56 | 57 | The backup drive should be specified by disk or volume name (or path) or 58 | the mount point of the backup drive. For example: 59 | - ./tmimport.sh /dev/disk1s1 60 | - ./tmimport.sh /Volumes/TMBACKUP 61 | 62 | While some additional heuristics may assist in identifying a backup drive 63 | that is specified in other ways, they are untested and not guaranteed 64 | to work correctly. 65 | ______________________________________________________________________________ 66 | 67 | 68 | **tmbless** 69 | ------------ 70 | TIME MACHINE BLESS 71 | 72 | USAGE 73 | 74 | tmbless.sh 75 | 76 | DESCRIPTION 77 | 78 | Time Machine Bless marks a snapshot directory as valid and recognizable 79 | by Time Machine. 80 | 81 | It does this by modifying the metadata of the snapshot directory so that 82 | the it accurately reflects the date the snapshot was created and the 83 | metadata of the 'drive' subdirectory within that snapshot directory 84 | matches the current drive. 85 | 86 | These modifications should allow restoration of files within the Time 87 | Machine restore UI. 88 | ______________________________________________________________________________ 89 | 90 | 91 | **dirdedupe** 92 | ------------- 93 | DIRECTORY DE-DUPLICATOR 94 | 95 | USAGE 96 | 97 | dirdedupe.sh [--execute] masterdir subjectdir 98 | 99 | DESCRIPTION 100 | 101 | For each file in subjectdir, replace it with a hard link to the matching 102 | file (if any) in masterdir. A file will be considered a match if, and 103 | only if, it shares the same file name, relative path, and contents. 104 | 105 | OPTIONS 106 | 107 | --execute Actually remove and link duplicate files. By default, this 108 | program runs in test mode. 109 | 110 | In the example below, you can see that running `dirdedupe.sh` results in the files being linked, while preserving the date and time stamps of the enclosing directories. 111 | 112 | dirdedupe example 113 | -------------------------------------------------------------------------------- /dev/dirtimefix__DO_NOT_RUN__DEVELOPMENT_ONLY.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # DIRECTORY TIME FIX 4 | # 5 | # Copyright (c) 2023 Joshua Lee Ockert 6 | # 7 | # THIS WORK IS PROVIDED "AS IS" WITH NO WARRANTY OF ANY KIND. THE IMPLIED 8 | # WARRANTIES OF MERCHANTABILITY, FITNESS, NON-INFRINGEMENT, AND TITLE ARE 9 | # EXPRESSLY DISCLAIMED. NO AUTHOR SHALL BE LIABLE UNDER ANY THEORY OF LAW 10 | # FOR ANY DAMAGES OF ANY KIND RESULTING FROM THE USE OF THIS WORK. 11 | # 12 | # Permission to use, copy, modify, and/or distribute this work for any 13 | # purpose is hereby granted, provided this notice appears in all copies. 14 | 15 | 16 | ############################################################################ 17 | ## FUNCTION TO PRINT USAGE INSTRUCTIONS ## 18 | ############################################################################ 19 | printusage() { 20 | echo " 21 | DIRECTORY TIME FIX 22 | 23 | USAGE 24 | 25 | dirtimefix.sh [--execute] 26 | 27 | DESCRIPTION 28 | 29 | For each directory in with a modification time within the past 30 | minutes, reset its modification time (via touch -r) to match 31 | the newest file it contains. 32 | 33 | OPTIONS 34 | 35 | --execute Actually reset modification times. By default, this program 36 | runs in test mode. 37 | " 38 | } 39 | 40 | 41 | ############################################################################ 42 | ## PARSE & MAKE SENSE OF COMMAND LINE ## 43 | ############################################################################ 44 | 45 | # CHECK FOR THE EXECUTE FLAG (DEFAULT IS TESTING-ONLY MODE) 46 | REALLYRUN=0 47 | if [ "${1}" == "--execute" ]; then 48 | REALLYRUN=1 49 | shift 50 | fi 51 | 52 | EMTPYDIRS="" 53 | 54 | # MAKE SURE WE HAVE THE RIGHT NUMBER OF ARGUMENTS AND THEY'RE VALID 55 | if [ ! $# -eq 2 ]; then 56 | echo && echo "Wrong number of arguments!" && printusage && exit 57 | else 58 | topdir=$1 59 | minutes=$2 60 | if [ ! -d "${topdir}" ]; then 61 | echo && echo "${topdir} is not a directory!" && printusage && exit 62 | elif [[ ! "${minutes}" =~ ^[0-9]+$ ]]; then 63 | echo && echo "${minutes} is not a whole number of minutes!" && printusage && exit 64 | fi 65 | fi 66 | 67 | 68 | ############################################################################ 69 | ## ADJUST MODIFICATION TIMES (OR NOT) ## 70 | ############################################################################ 71 | echo find "${topdir}" -type d -mtime -"${minutes}"m -d 72 | find "${topdir}" -type d -mtime -"${minutes}"m -d -print0 | while read -d $'\0' moddir 73 | do 74 | srchdir="${moddir}" 75 | newest=$(find "${srchdir}" \( -type d -or -type f \) -mtime +"${minutes}"m -mindepth 1 -maxdepth 1 -exec ls -td {} + | head -1) 76 | if [ "${newest}" == "" ]; then 77 | newest=$(find "${srchdir}/.." \( -type d -or -type f \) -mtime +"${minutes}"m -mindepth 1 -maxdepth 1 -exec ls -td {} + | head -1) 78 | if [ "${newest}" == "" ]; then 79 | newest=$(find "${srchdir}/../.." \( -type d -or -type f \) -mtime +"${minutes}"m -mindepth 1 -maxdepth 1 -exec ls -td {} + | head -1) 80 | if [ "${newest}" == "" ]; then 81 | newest=$(find "${srchdir}/../../.." \( -type d -or -type f \) -mtime +"${minutes}"m -mindepth 1 -maxdepth 1 -exec ls -td {} + | head -1) 82 | if [ "${newest}" == "" ]; then 83 | newest=$(find "${srchdir}/../../../.." \( -type d -or -type f \) -mtime +"${minutes}"m -mindepth 1 -maxdepth 1 -exec ls -td {} + | head -1) 84 | if [ "${newest}" == "" ]; then 85 | echo "EMPTY DIR AT ${moddir}! Could not find a file to borrow mtime from!" 86 | fi 87 | fi 88 | fi 89 | fi 90 | fi 91 | if [ ! "${newest}" == "" ]; then 92 | if [ $REALLYRUN -gt 0 ]; then 93 | echo "Setting mtime of \"${moddir}\" to mtime of \"${newest}\"" 94 | touch -r "${newest}" "${moddir}" 95 | else 96 | echo "NOT setting mtime of \"${moddir}\" to mtime of \"${newest}\"" 97 | fi 98 | fi 99 | done 100 | 101 | exit 102 | 103 | -------------------------------------------------------------------------------- /dev/tmdiskenum__DO_NOT_RUN__DEVELOPMENT_ONLY.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | function storebackupdrivelist() { 5 | 6 | # Search for external drives 7 | local ext_drv_srch=`diskutil list | grep external` 8 | local ext_drv_list=`diskutil list | grep external | sed 's/.*\(disk[0-9][0-9]*\).*/\1/g'` 9 | 10 | # Then, for each one... 11 | for drivename in $ext_drv_list; do 12 | # Get the HFS partitions 13 | local ext_hfs_ptns=`diskutil list ${drivename} | grep "Apple_HFS" | grep "${drivename}" | cut -w -f7` 14 | # And get their volume names, mount points, and mount status 15 | for partitionname in $ext_hfs_ptns; do 16 | local volumename=`diskutil info /dev/${partitionname} | grep "Volume Name:" | cut -w -f4` 17 | local mountpoint=`diskutil info /dev/${partitionname} | grep "Mount Point:" | cut -w -f4` 18 | if [ mountpoint == "" ]; then 19 | mountpoint="(none)" 20 | local mountstatus="NO" 21 | else 22 | local mountstatus="YES" 23 | fi 24 | if [ ${#drives[@]} -eq 0 ]; then 25 | drives=("${partitionname}") 26 | else 27 | drives=("${drives[@]}","${partitionname}") 28 | fi 29 | drives=("${drives[@]}","${volumename}") 30 | drives=("${drives[@]}","${mountstatus}") 31 | drives=("${drives[@]}","${mountpoint}") 32 | drives=("${drives[@]}","EOL") 33 | done 34 | done 35 | } 36 | 37 | declare -a drives 38 | storebackupdrivelist drives 39 | 40 | echo "drives = ${drives}" 41 | -------------------------------------------------------------------------------- /dirdedupe.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2023 Joshua Lee Ockert 4 | # 5 | # THIS WORK IS PROVIDED "AS IS" WITH NO WARRANTY OF ANY KIND. THE IMPLIED 6 | # WARRANTIES OF MERCHANTABILITY, FITNESS, NON-INFRINGEMENT, AND TITLE ARE 7 | # EXPRESSLY DISCLAIMED. NO AUTHOR SHALL BE LIABLE UNDER ANY THEORY OF LAW 8 | # FOR ANY DAMAGES OF ANY KIND RESULTING FROM THE USE OF THIS WORK. 9 | # 10 | # Permission to use, copy, modify, and/or distribute this work for any 11 | # purpose is hereby granted, provided this notice appears in all copies. 12 | # 13 | # SPDX-License-Identifier: ISC 14 | 15 | 16 | ############################################################################ 17 | ## FUNCTION TO PRINT USAGE INSTRUCTIONS ## 18 | ############################################################################ 19 | printusage() { 20 | echo " 21 | DIRECTORY DE-DUPLICATOR 22 | 23 | USAGE 24 | 25 | dirdedupe.sh [--execute] masterdir subjectdir 26 | 27 | DESCRIPTION 28 | 29 | For each file in subjectdir, replace it with a hard link to the matching 30 | file (if any) in masterdir. A file will be considered a match if, and 31 | only if, it shares the same file name, relative path, and contents. 32 | 33 | OPTIONS 34 | 35 | --execute Actually remove and link duplicate files. By default, this 36 | program runs in test mode. 37 | 38 | " 39 | } 40 | 41 | 42 | ############################################################################ 43 | ## PARSE & MAKE SENSE OF COMMAND LINE ## 44 | ############################################################################ 45 | 46 | # CHECK FOR THE EXECUTE FLAG (DEFAULT IS TESTING-ONLY MODE) 47 | REALLYRUN=0 48 | if [ "${1}" == "--execute" ]; then 49 | REALLYRUN=1 50 | shift 51 | fi 52 | 53 | # MAKE SURE WE HAVE THE RIGHT NUMBER OF ARGUMENTS AND THEY'RE VALID 54 | if [ ! $# -eq 2 ]; then 55 | echo && echo "Wrong number of arguments!" && printusage && exit 56 | else 57 | masterdir=$1 58 | subjectdir=$2 59 | if [ ! -d "${masterdir}" ]; then 60 | echo && echo "${masterdir} is not a directory!" && printusage && exit 61 | elif [ ! -d "${subjectdir}" ]; then 62 | echo && echo "${subjectdir} is not a directory!" && printusage && exit 63 | fi 64 | fi 65 | 66 | ############################################################################ 67 | ## MAKE A TEMPORARY FILE FOR PRESERVING TIMESTAMPS ## 68 | ############################################################################ 69 | TEMPFILE=$(mktemp) 70 | trap "rm -f ${TEMPFILE}" EXIT 71 | 72 | ############################################################################ 73 | ## HARDLINK THE DUPLICATES (OR NOT) ## 74 | ############################################################################ 75 | find "${subjectdir}" -print0 | while read -d $'\0' subjectfile 76 | do 77 | if [ -f "${subjectfile}" ]; then 78 | masterfile="${subjectfile/#${subjectdir}/${masterdir}}" 79 | if [ -f "${masterfile}" ]; then 80 | if [ ! "${subjectfile}" -ef "${masterfile}" ]; then 81 | cmp -s "${masterfile}" "${subjectfile}" 82 | if [ $? -eq 0 ]; then 83 | if [ $REALLYRUN -gt 0 ]; then 84 | echo "LINK \"${masterfile}\" <-- \"${subjectfile}\"" 85 | # Store the mtime/atime of subject file's directory 86 | TEMPSUBJDIR=`dirname "${subjectfile}"` 87 | touch -r "${TEMPSUBJDIR}" "${TEMPFILE}" 88 | # Link the subject file to the corresponding file in 89 | # the master directory 90 | ln -Pf "${masterfile}" "${subjectfile}" 91 | # Restore the mtime/atime of subject file's directory 92 | touch -r "${TEMPFILE}" "${TEMPSUBJDIR}" 93 | else 94 | echo "HYPO \"${masterfile}\" <~~ \"${subjectfile}\"" 95 | fi 96 | #else 97 | #echo "MOD \"${masterfile}\" \"${subjectfile}\"" 98 | fi #END check for files being the same 99 | #else 100 | #echo "ID \"${masterfile}\" <-> \"${subjectfile}\"" 101 | fi # END check for inode equality 102 | #else 103 | #echo "NEW \"${subjectfile}\" " 104 | fi # END check if master file exists 105 | fi 106 | done 107 | 108 | exit 109 | 110 | -------------------------------------------------------------------------------- /tmbless.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2023 Joshua Lee Ockert 4 | # 5 | # THIS WORK IS PROVIDED "AS IS" WITH NO WARRANTY OF ANY KIND. THE IMPLIED 6 | # WARRANTIES OF MERCHANTABILITY, FITNESS, NON-INFRINGEMENT, AND TITLE ARE 7 | # EXPRESSLY DISCLAIMED. NO AUTHOR SHALL BE LIABLE UNDER ANY THEORY OF LAW 8 | # FOR ANY DAMAGES OF ANY KIND RESULTING FROM THE USE OF THIS WORK. 9 | # 10 | # Permission to use, copy, modify, and/or distribute this work for any 11 | # purpose is hereby granted, provided this notice appears in all copies. 12 | # 13 | # SPDX-License-Identifier: ISC 14 | 15 | 16 | function dispusage() { 17 | if [ ${#1} -gt 0 ]; then 18 | printf "%s\n\n" "${1}" 19 | fi 20 | echo "\ 21 | TIME MACHINE BLESS 22 | 23 | USAGE 24 | 25 | ${0} 26 | 27 | DESCRIPTION 28 | 29 | Time Machine Bless marks a snapshot directory as valid and recognizable 30 | by Time Machine. 31 | 32 | It does this by modifying the metadata of the snapshot directory so that 33 | the it accurately reflects the date the snapshot was created and the 34 | metadata of the 'drive' subdirectory within that snapshot directory 35 | matches the current drive. 36 | 37 | These modifications should allow restoration of files within the Time 38 | Machine restore UI. 39 | " 40 | } 41 | 42 | function canonicalname() { 43 | echo $(stat -f %R ${1}) 44 | } 45 | 46 | ############################################################################ 47 | ## PARSE & MAKE SENSE OF COMMAND LINE ## 48 | ############################################################################ 49 | if [ ! $# -eq 1 ]; then 50 | dispusage "Invalid number of arguments" && exit 51 | fi 52 | 53 | # Try to get canonical name 54 | DATEDIRNAME=$(stat -f %R ${1}) 55 | if [[ "${DATEDIRNAME}" == "" ]]; then 56 | dispusage "Could not get canonical name of directory ${1}" && exit 57 | fi 58 | 59 | # Ensure it's a directory 60 | if [ ! -d "${DATEDIRNAME}" ]; then 61 | dispusage "${DATEDIRNAME} is not a valid directory" && exit 62 | fi 63 | 64 | # Ensure it's a Backups.backupdb dir's subdir's subdir 65 | if [[ ! "${DATEDIRNAME}" =~ ^.*\/Backups\.backupdb\/.*\/.*$ ]]; then 66 | dispusage "${DATEDIRNAME} does not appear to be in a Backups.backupdb directory" && exit 67 | fi 68 | 69 | # Get the basename and ensure it's in a Time Machine timestamp format 70 | DATEDIRBASENAME=$(basename "${DATEDIRNAME}") 71 | 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 72 | dispusage "${DATEDIRBASENAME} does not appear to be a snapshot directory" && exit 73 | fi 74 | 75 | # Get the Unix timestamp from the directory's Time Machine timestamp format 76 | TIMESTAMP=$(date -j -f "%Y-%m-%d-%H%M%S" "${DATEDIRBASENAME}" +"%s") 77 | TIMESTAMP="${TIMESTAMP}100000" 78 | if [[ ! "${TIMESTAMP}" =~ ^[0-9]*$ ]]; then 79 | dispusage "Could not automatically set snapshot timestamp due to folder name format" && exit 80 | fi 81 | 82 | if [ -d "${DATEDIRNAME}/Macintosh HD - Data" ]; then 83 | DRVDIRNAME="${DATEDIRNAME}/Macintosh HD - Data" 84 | elif [ -d "${DATEDIRNAME}/Macintosh HD" ]; then 85 | DRVDIRNAME="${DATEDIRNAME}/Macintosh HD" 86 | else 87 | ## TODO: Take the output of `ls -d ${DATEDIRNAME}` and try to autodetect? 88 | dispusage "Could not find a volume name within ${DATEDIRNAME}" && exit 89 | fi 90 | 91 | ############################################################################ 92 | ## GET NECESSARY SYSTEM INFORMATION ## 93 | ############################################################################ 94 | VOLGRPUUID=`diskutil info / | awk -F' ' '/^ APFS Volume Group/{print $(NF)}'` 95 | VOLDSKUUID=`diskutil info / | awk -F' ' '/^ Volume UUID/{print $(NF)}'` 96 | 97 | if [ ! VOLGRPUUID == "" ]; then 98 | TGTUUID="${VOLGRPUUID}" 99 | elif [ ! VOLDSKUUID == "" ]; then 100 | TGTUUID="${VOLDSKUUID}" 101 | fi 102 | 103 | 104 | KERNELVER=`uname -a | sed 's/.*Version \([0-9][0-9]*\).*/\1/g'` 105 | if [ $KERNELVER -lt 20 ]; then 106 | SIMONSAYS="sudo /System/Library/Extensions/TMSafetyNet.kext/Contents/Helpers/bypass" 107 | else 108 | SIMONSAYS="sudo" 109 | fi 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | ############################################################################ 119 | ## CONFIRM ACTION INFORMATION ## 120 | ############################################################################ 121 | printf "\n\ 122 | Snapshot Directory: %s\n\ 123 | Snapshot Volume: %s\n\ 124 | Computer Volume: %s\n\ 125 | Volume Group: %s\n\\n" "${DATEDIRNAME}" "${DRVDIRNAME}" "${VOLDSKUUID}" "${VOLGRPUUID}" 126 | 127 | printf "\ 128 | Preparing to run the following commands:\n\ 129 | %s xattr -c \"%s\"\n\ 130 | %s xattr -c \"%s\"\n\ 131 | %s xattr -w \"com.apple.backupd.SnapshotCompletionDate\" \"%s\" \"%s\"\n\ 132 | %s xattr -w \"com.apple.backupd.SnapshotState\" %s \"%s\"\n\ 133 | %s xattr -w \"com.apple.backupd.SnapshotVolumeUUID\" \"%s\" \"%s\"\n\ 134 | \n" "${SIMONSAYS}" "${DATEDIRNAME}" \ 135 | "${SIMONSAYS}" "${DRVDIRNAME}" \ 136 | "${SIMONSAYS}" "${TIMESTAMP}" "${DATEDIRNAME}" \ 137 | "${SIMONSAYS}" "4" "${DATEDIRNAME}" \ 138 | "${SIMONSAYS}" "${TGTUUID}" "${DRVDIRNAME}" 139 | 140 | printf "Does everything look right?\n" 141 | 142 | select response in "Bless Time Machine Snapshot" "ABORT ABORT ABORT!"; do 143 | if [ "${response}" == "Bless Time Machine Snapshot" ]; then 144 | "${SIMONSAYS}" xattr -c "${DATEDIRNAME}" && \ 145 | "${SIMONSAYS}" xattr -c "${DRVDIRNAME}" && \ 146 | "${SIMONSAYS}" xattr -w "com.apple.backupd.SnapshotCompletionDate" "${TIMESTAMP}" "${DATEDIRNAME}" && \ 147 | "${SIMONSAYS}" xattr -w "com.apple.backupd.SnapshotState" "4" "${DATEDIRNAME}" && \ 148 | "${SIMONSAYS}" xattr -w "com.apple.backupd.SnapshotVolumeUUID" "${TGTUUID}" "${DRVDIRNAME}" 149 | 150 | if [ ! $? -eq 0 ]; then 151 | printf "\nOperation failed.\n\n" 152 | else 153 | printf "\nOperation completed.\n\n" 154 | printf "The snapshot directory now has the following metadata:\n" 155 | printf "%s\n" "——————————————————————————————————————————————————————" 156 | xattr -lv "${DATEDIRNAME}" 157 | printf "\n\n" 158 | printf "The snapshot volume now has the following metadata:\n" 159 | printf "%s\n" "———————————————————————————————————————————————————" 160 | xattr -lv "${DRVDIRNAME}" 161 | printf "\n\n" 162 | fi 163 | break 164 | else 165 | printf "\nOperation aborted. No action has been taken.\n\n" 166 | break 167 | fi 168 | done 169 | -------------------------------------------------------------------------------- /tmimport.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2023 Joshua Lee Ockert 4 | # 5 | # THIS WORK IS PROVIDED "AS IS" WITH NO WARRANTY OF ANY KIND. THE IMPLIED 6 | # WARRANTIES OF MERCHANTABILITY, FITNESS, NON-INFRINGEMENT, AND TITLE ARE 7 | # EXPRESSLY DISCLAIMED. NO AUTHOR SHALL BE LIABLE UNDER ANY THEORY OF LAW 8 | # FOR ANY DAMAGES OF ANY KIND RESULTING FROM THE USE OF THIS WORK. 9 | # 10 | # Permission to use, copy, modify, and/or distribute this work for any 11 | # purpose is hereby granted, provided this notice appears in all copies. 12 | # 13 | # SPDX-License-Identifier: ISC 14 | 15 | 16 | function dispusage() { 17 | if [ ${#1} -gt 0 ]; then 18 | printf "%s\n\n" "${1}" 19 | fi 20 | echo "\ 21 | TIME MACHINE IMPORT 22 | 23 | USAGE 24 | 25 | ${0} 26 | 27 | DESCRIPTION 28 | 29 | Time Machine Importer attempts to inherit the backup history of a backup 30 | drive. If the backup drive is not paired to the current computer, Time 31 | Machine Importer will attempt to pair the drive with the current computer 32 | by modifying the model, MAC address, and UUID/UDID reflected in the 33 | metadata of the backup drive. 34 | 35 | The backup drive should be specified by disk or volume name (or path) or 36 | the mount point of the backup drive. For example: 37 | - ${0} /dev/disk1s1 38 | - ${0} /Volumes/TMBACKUP 39 | 40 | While some additional heuristics may assist in identifying a backup drive 41 | that is specified in other ways, they are untested and not guaranteed 42 | to work correctly. 43 | " 44 | } 45 | 46 | 47 | ############################################################################ 48 | ## PARSE & MAKE SENSE OF COMMAND LINE ## 49 | ############################################################################ 50 | if [ ! $# -eq 1 ]; then 51 | dispusage "Invalid number of arguments" && exit 52 | fi 53 | 54 | ## IF USER PROVIDED A VALID DEVICE, VOLUME NAME, OR MOUNT POINT, USE THAT 55 | diskutil list "${1}" > /dev/null 2>&1 56 | if [[ $? -eq 0 ]]; then 57 | MOUNTPOINT=`diskutil info ${1} | grep "Mount Point:" | sed 's/^ *Mount Point: *\(.*\)$/\1/'` 58 | if [ "${MOUNTPOINT}" == "" ]; then 59 | dispusage "No mount point for specified device ${1}. Perhaps it isn't mounted?" && exit 60 | fi 61 | 62 | if [ ! -d "${MOUNTPOINT}/Backups.backupdb/" ]; then 63 | dispusage "No Backups.backupdb directory found at ${MOUNTPOINT}/Backups.backupdb/" && exit 64 | fi 65 | 66 | BACKUPPATH=`find ${MOUNTPOINT}/Backups.backupdb -type d -maxdepth 1 -xattrname com.apple.backupd.HostUUID -print -quit` 67 | if [ "${BACKUPPATH}" == "" ]; then 68 | dispusage "No suitable backups within ${MOUNTPOINT}/Backups.backupdb!" && exit 69 | fi 70 | SPECIFIED="${1}" 71 | 72 | ## OTHERWISE, THE USER MAY HAVE PROVIDED A BACKUPS.BACKUPDB PATH 73 | elif [[ -d "${1}" ]] && [[ $(stat -f %R ${1}) =~ Backups.backupdb$ ]]; then 74 | BACKUPPATH=`find $(stat -f %R ${1}) -type d -mindepth 1 -maxdepth 1 -xattrname com.apple.backupd.HostUUID -print -quit` 75 | if [ "${BACKUPPATH}" == "" ]; then 76 | dispusage "No suitable backups within $(stat -f %R ${1})!" && exit 77 | fi 78 | SPECIFIED=$(stat -f %R ${1}) 79 | 80 | ## OR MAYBE EVEN THE DIRECTORY WITHIN IT 81 | elif [[ -d "${1}" ]] && [[ $(stat -f %R ${1}) =~ Backups.backupdb\/.+$ ]]; then 82 | BACKUPPATH=`find $(stat -f %R ${1}) -type d -maxdepth 0 -xattrname com.apple.backupd.HostUUID -print -quit` 83 | if [ "${BACKUPPATH}" == "" ]; then 84 | dispusage "No suitable backups at $(stat -f %R ${1})!" && exit 85 | fi 86 | SPECIFIED=$(stat -f %R ${1}) 87 | 88 | ## OTHERWISE WE'RE SCREWED 89 | else 90 | dispusage "${1} is not a valid device, volume, or Backups.backupdb path." && exit 91 | fi 92 | 93 | 94 | ############################################################################ 95 | ## GET NECESSARY SYSTEM INFORMATION ## 96 | ############################################################################ 97 | MODEL=`ioreg -d2 -k IOPlatformUUID | awk -F\" '/"model"/{print $(NF-1)}'` 98 | UUID=`ioreg -d2 -k IOPlatformUUID | awk -F\" '/"IOPlatformUUID"/{print $(NF-1)}'` 99 | MAC=`ifconfig en0 | awk '/ether/{print $2}'` 100 | #UUIDHEX=`printf '%s\0' ${UUID} | xxd -p -c37` 101 | #MACHEX=`printf '%s\0' ${MAC} | xxd -p` 102 | 103 | KERNELVER=`uname -a | sed 's/.*Version \([0-9][0-9]*\).*/\1/g'` 104 | if [ $KERNELVER -lt 20 ]; then 105 | SIMONSAYS="sudo /System/Library/Extensions/TMSafetyNet.kext/Contents/Helpers/bypass" 106 | else 107 | SIMONSAYS="sudo" 108 | fi 109 | 110 | 111 | ############################################################################ 112 | ## CONFIRM ACTION INFORMATION ## 113 | ############################################################################ 114 | printf "\n\ 115 | Specified Backup: %s\n\ 116 | Backup Location: %s\n\ 117 | Computer Model: %s\n\ 118 | Host UUID: %s\n\ 119 | MAC Address: %s\n\n" "${SPECIFIED}" "${BACKUPPATH}" "${MODEL}" "${UUID}" "${MAC}" 120 | 121 | printf "This backup drive is currently matched to the following computer:\n" 122 | printf " ModelID: %s\n" "$(xattr -p 'com.apple.backupd.ModelID' "${BACKUPPATH}")" 123 | printf " MAC Address: %s\n" "$(xattr -p 'com.apple.backupd.BackupMachineAddress' "${BACKUPPATH}")" 124 | printf " Host UUID: %s\n\n" "$(xattr -p 'com.apple.backupd.HostUUID' "${BACKUPPATH}")" 125 | 126 | printf "\ 127 | Preparing to run the following commands:\n\ 128 | %s xattr -w 'com.apple.backupd.ModelID' \"%-36s\" \"%s\"\n\ 129 | %s xattr -w 'com.apple.backupd.BackupMachineAddress' \"%-36s\" \"%s\"\n\ 130 | %s xattr -w 'com.apple.backupd.HostUUID' \"%-36s\" \"%s\"\n\ 131 | %s tmutil inheritbackup \"%s\"\n\ 132 | \n" "${SIMONSAYS}" "${MODEL}" "${BACKUPPATH}" \ 133 | "${SIMONSAYS}" "${MAC}" "${BACKUPPATH}" \ 134 | "${SIMONSAYS}" "${UUID}" "${BACKUPPATH}" \ 135 | "${SIMONSAYS}" "${BACKUPPATH}" 136 | 137 | if [ ! "$(basename "${BACKUPPATH}")" == "$(scutil --get ComputerName)" ]; then 138 | printf "#############################################################################\n" 139 | printf "## W A R N I N G W A R N I N G W A R N I N G ##\n" 140 | printf "#############################################################################\n" 141 | printf "\n" 142 | printf "The Backup Location DOES NOT MATCH your computer name.\n" 143 | printf "\n" 144 | printf " Backup Location: %s\n" "${BACKUPPATH}" 145 | printf " Backup Computer Name: %s\n" "$(basename "${BACKUPPATH}")" 146 | printf " Current Computer Name: %s\n" "$(scutil --get ComputerName)" 147 | printf "\n" 148 | printf "Only proceed if you are very certain of what you're doing!\n" 149 | printf "Even if successful, the Time Machine restore UI will be adversely affected.\n" 150 | else 151 | printf "Does everything look right?\n" 152 | fi 153 | 154 | 155 | select response in "Import Time Machine backup" "ABORT ABORT ABORT!"; do 156 | if [ "${response}" == "Import Time Machine backup" ]; then 157 | "${SIMONSAYS}" xattr -w 'com.apple.backupd.ModelID' "${MODEL}" "${BACKUPPATH}" 158 | "${SIMONSAYS}" xattr -w 'com.apple.backupd.BackupMachineAddress' "${MAC}" "${BACKUPPATH}" 159 | "${SIMONSAYS}" xattr -w 'com.apple.backupd.HostUUID' "${UUID}" "${BACKUPPATH}" 160 | "${SIMONSAYS}" tmutil inheritbackup "${BACKUPPATH}" 161 | if [ ! $? -eq 0 ]; then 162 | printf "\nOperation failed. Backup history not imported.\n\n" 163 | else 164 | printf "\nOperation completed.\n\n" 165 | printf "This backup drive has been matched to the following computer:\n" 166 | printf " ModelID: %s\n" "$(xattr -p 'com.apple.backupd.ModelID' "${BACKUPPATH}")" 167 | printf " MAC Address: %s\n" "$(xattr -p 'com.apple.backupd.BackupMachineAddress' "${BACKUPPATH}")" 168 | printf " Host UUID: %s\n\n" "$(xattr -p 'com.apple.backupd.HostUUID' "${BACKUPPATH}")" 169 | fi 170 | break 171 | else 172 | printf "\nOperation aborted. No action has been taken.\n\n" 173 | break 174 | fi 175 | done 176 | --------------------------------------------------------------------------------