commit 636cd274dbb62bedf05f01329ec7b20085130c16 Author: Lee Ockert Date: Tue Jul 19 03:32:37 2022 -0400 Initial Commit This initial commit includes a directory deduplicator (for thinning out Time Machine snapshots) and an automatic tool to detect and apply the necessary metadata for a successful tmutil inherit (needed when you want to use a backup drive with a new computer, for example). diff --git a/README.md b/README.md new file mode 100644 index 0000000..52f51a0 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# tmimport + Time Machine Importer + + tmimport.sh + + Time Machine Importer modifies the metadata of a backup drive to match the + current computer's model and unique identifiers (primary MAC address and + hardware platform UUID/provisioning UDID) and attempts to 'inherit' the + backup history. + + The backup drive should be specified by disk or volume name (or path) or + the mount point of the backup drive. If Time Machine Importer cannot find + an appropriate disk volume or mount point, it will check to see if the + specified directory is a valid Backups.backupdb location (or a machine + directory under one). + + A future version may attempt to detect HFS+ or APFS partitions serving as + backup drives, and, if a backup drive is specified as a device path, it + will automagically choose the correct backup path. + +# dirdedupe + Directory De-Duplication ("Dirty Dupe") + + dirdedupe.sh [--execute] masterdir shadowdir + + For each file in shadowdir, replace it with a hard link to the matching file + (if any) in masterdir. A file will be considered a match if, and only if, it + shares the same file name, relative path, and contents. + + OPTIONS + + --execute Actually remove and link duplicate files. By default, this + program runs in test mode. \ No newline at end of file diff --git a/dirdedupe.sh b/dirdedupe.sh new file mode 100755 index 0000000..6c79693 --- /dev/null +++ b/dirdedupe.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +# BITSC LICENSE NOTICE (MODIFIED ISC LICENSE) +# +# DIRECTORY DE-DUPLICATION ("Dirty Dupe") +# +# 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 TO PRINT USAGE INSTRUCTIONS ## +############################################################################ +printusage() { + echo " +DIRECTORY DE-DUPLICATION (\"Dirty Dupe\") + +${0} [--execute] masterdir shadowdir + +For each file in shadowdir, replace it with a hard link to the matching file +(if any) in masterdir. A file will be considered a match if, and only if, it +shares the same file name, relative path, and contents. + +OPTIONS + + --execute Actually remove and link duplicate files. By default, this + program runs in test mode. +" +} + + +############################################################################ +## PARSE & MAKE SENSE OF COMMAND LINE ## +############################################################################ + +# CHECK FOR THE EXECUTE FLAG (DEFAULT IS TESTING-ONLY MODE) +REALLYRUN=0 +if [ "${1}" == "--execute" ]; then + REALLYRUN=1 + shift +fi + +# MAKE SURE WE HAVE THE RIGHT NUMBER OF ARGUMENTS AND THEY'RE VALID +if [ ! $# -eq 2 ]; then + echo && echo "Wrong number of arguments!" && printusage && exit +else + masterdir=$1 + shadowdir=$2 + if [ ! -d "${masterdir}" ]; then + echo && echo "${masterdir} is not a directory!" && printusage && exit + elif [ ! -d "${shadowdir}" ]; then + echo && echo "${shadowdir} is not a directory!" && printusage && exit + fi +fi + + +############################################################################ +## HARDLINK THE DUPLICATES (OR NOT) ## +############################################################################ +find "${shadowdir}" -print0 | while read -d $'\0' shadowfile +do + if [ -f "${shadowfile}" ]; then + masterfile="${shadowfile/#${shadowdir}/${masterdir}}" + if [ -f "${masterfile}" ]; then + cmp -s "${masterfile}" "${shadowfile}" + if [ $? -eq 0 ]; then + if [ $REALLYRUN -gt 0 ]; then + echo "Linking \"${masterfile}\" <-- \"${shadowfile}\"" + ln -Pf "${masterfile}" "${shadowfile}" + else + echo "NOT linking \"${masterfile}\" <-- \"${shadowfile}\"" + fi + fi + fi + fi +done + +exit + diff --git a/tmdiskenum.sh b/tmdiskenum.sh new file mode 100755 index 0000000..8ae47de --- /dev/null +++ b/tmdiskenum.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + + +function storebackupdrivelist() { + + # Search for external drives + local ext_drv_srch=`diskutil list | grep external` + local ext_drv_list=`diskutil list | grep external | sed 's/.*\(disk[0-9][0-9]*\).*/\1/g'` + + # Then, for each one... + for drivename in $ext_drv_list; do + # Get the HFS partitions + local ext_hfs_ptns=`diskutil list ${drivename} | grep "Apple_HFS" | grep "${drivename}" | cut -w -f7` + # And get their volume names, mount points, and mount status + for partitionname in $ext_hfs_ptns; do + local volumename=`diskutil info /dev/${partitionname} | grep "Volume Name:" | cut -w -f4` + local mountpoint=`diskutil info /dev/${partitionname} | grep "Mount Point:" | cut -w -f4` + if [ mountpoint == "" ]; then + mountpoint="(none)" + local mountstatus="NO" + else + local mountstatus="YES" + fi + if [ ${#drives[@]} -eq 0 ]; then + drives=("${partitionname}") + else + drives=("${drives[@]}","${partitionname}") + fi + drives=("${drives[@]}","${volumename}") + drives=("${drives[@]}","${mountstatus}") + drives=("${drives[@]}","${mountpoint}") + drives=("${drives[@]}","EOL") + done + done +} + +declare -a drives +storebackupdrivelist drives + +echo "drives = ${drives}" \ No newline at end of file diff --git a/tmimport.sh b/tmimport.sh new file mode 100755 index 0000000..2ffceba --- /dev/null +++ b/tmimport.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash + +# BITSC LICENSE NOTICE (MODIFIED ISC LICENSE) +# +# TIME MACHINE IMPORTER +# +# 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 Importer" + echo + echo "\ +Usage: ${0} + +Time Machine Importer modifies the metadata of a backup drive to match the +current computer's model and unique identifiers (primary MAC address and +hardware platform UUID/provisioning UDID), attempts to associate the primary +disk with the backup, and attempts to 'inherit' the backup history. + +The backup drive should be specified by volume name or mount point. If the +backup drive is specified by the disk device name or path, Time Machine +Importer will try to find an HFS+ partition on it. If there is one, or if a +partition name or path is specified, Time Machine Importer will attempt to +find the mount point and check for the existence of a Backups.backupdb +folder. + +" +} + + +############################################################################ +## PARSE & MAKE SENSE OF COMMAND LINE ## +############################################################################ +if [ ! $# -eq 1 ]; then + dispusage "Invalid number of arguments" && exit +fi + +## IF USER PROVIDED A VALID DEVICE, VOLUME NAME, OR MOUNT POINT, USE THAT +diskutil list "${1}" > /dev/null 2>&1 +if [[ $? -eq 0 ]]; then + MOUNTPOINT=`diskutil info ${1} | grep "Mount Point:" | sed 's/^ *Mount Point: *\(.*\)$/\1/'` + if [ "${MOUNTPOINT}" == "" ]; then + dispusage "No mount point for specified device ${1}. Perhaps it isn't mounted?" && exit + # TODO: Should check to see if it CONTAINS any disks with a Time Machine role... + fi + + if [ ! -d "${MOUNTPOINT}/Backups.backupdb/" ]; then + dispusage "No Backups.backupdb directory found at ${MOUNTPOINT}/Backups.backupdb/" && exit + fi + + BACKUPPATH=`find ${MOUNTPOINT}/Backups.backupdb -type d -maxdepth 1 -xattrname com.apple.backupd.HostUUID -print -quit` + if [ "${BACKUPPATH}" == "" ]; then + dispusage "No suitable backups within ${MOUNTPOINT}/Backups.backupdb!" && exit + fi + SPECIFIED="${1}" + +## OTHERWISE, THE USER MAY HAVE PROVIDED A BACKUPS.BACKUPDB PATH +elif [[ -d "${1}" ]] && [[ $(stat -f %R ${1}) =~ Backups.backupdb$ ]]; then + BACKUPPATH=`find $(stat -f %R ${1}) -type d -maxdepth 1 -xattrname com.apple.backupd.HostUUID -print -quit` + if [ "${BACKUPPATH}" == "" ]; then + dispusage "No suitable backups within $(stat -f %R ${1})!" && exit + fi + SPECIFIED=$(stat -f %R ${1}) + +## OR MAYBE EVEN THE DIRECTORY WITHIN IT +elif [[ -d "${1}" ]] && [[ $(stat -f %R ${1}) =~ Backups.backupdb\/.+$ ]]; then + BACKUPPATH=`find $(stat -f %R ${1}) -type d -maxdepth 0 -xattrname com.apple.backupd.HostUUID -print -quit` + if [ "${BACKUPPATH}" == "" ]; then + dispusage "No suitable backups at $(stat -f %R ${1})!" && exit + fi + SPECIFIED=$(stat -f %R ${1}) + +## OTHERWISE WE'RE SCREWED +else + dispusage "${1} is not a valid device, volume, or Backups.backupdb path." && exit +fi + + +############################################################################ +## GET NECESSARY SYSTEM INFORMATION ## +############################################################################ +MODEL=`ioreg -d2 -k IOPlatformUUID | awk -F\" '/"model"/{print $(NF-1)}'` +UUID=`ioreg -d2 -k IOPlatformUUID | awk -F\" '/"IOPlatformUUID"/{print $(NF-1)}'` +#UUIDHEX=`printf '%s\0' ${UUID} | xxd -p -c37` +MAC=`ifconfig en0 | awk '/ether/{print $2}'` +#MACHEX=`printf '%s\0' ${MAC} | xxd -p` + +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\ +Specified Backup: %s\n\ +Backup Location: %s\n\ +Computer Model: %s\n\ +Host UUID: %s\n\ +MAC Address: %s\n\n" "${SPECIFIED}" "${BACKUPPATH}" "${MODEL}" "${UUID}" "${MAC}" + +printf "Attributes are now:\n" +printf " %s\t\t\t%s\n" "com.apple.backupd.ModelID" "$(xattr -p 'com.apple.backupd.ModelID' "${BACKUPPATH}")" +printf " %s\t%s\n" "com.apple.backupd.BackupMachineAddress" "$(xattr -p 'com.apple.backupd.BackupMachineAddress' "${BACKUPPATH}")" +printf " %s\t\t\t%s\n\n" "com.apple.backupd.HostUUID" "$(xattr -p 'com.apple.backupd.HostUUID' "${BACKUPPATH}")" + +printf "\ +Preparing to run the following commands:\n\ + %s xattr -w 'com.apple.backupd.ModelID' \"%-36s\" \"%s\"\n\ + %s xattr -w 'com.apple.backupd.BackupMachineAddress' \"%-36s\" \"%s\"\n\ + %s xattr -w 'com.apple.backupd.HostUUID' \"%-36s\" \"%s\"\n\ + %s tmutil inheritbackup \"%s\"\n\ + \n" "${SIMONSAYS}" "${MODEL}" "${BACKUPPATH}" \ + "${SIMONSAYS}" "${MAC}" "${BACKUPPATH}" \ + "${SIMONSAYS}" "${UUID}" "${BACKUPPATH}" \ + "${SIMONSAYS}" "${BACKUPPATH}" + +printf "\nDoes everything look right?\n\n" + +select response in "Apply Time Machine Magic" "ABORT ABORT ABORT!"; do + if [ "${response}" == "Apply Time Machine Magic" ]; then + "${SIMONSAYS}" xattr -w 'com.apple.backupd.ModelID' "${MODEL}" "${BACKUPPATH}" + "${SIMONSAYS}" xattr -w 'com.apple.backupd.BackupMachineAddress' "${MAC}" "${BACKUPPATH}" + "${SIMONSAYS}" xattr -w 'com.apple.backupd.HostUUID' "${UUID}" "${BACKUPPATH}" + "${SIMONSAYS}" tmutil inheritbackup "${BACKUPPATH}" + printf "\nOperation completed.\n\n" + printf "Attributes are now:\n" + printf " %s\t\t\t%s\n" "com.apple.backupd.ModelID" "$(xattr -p 'com.apple.backupd.ModelID' "${BACKUPPATH}")" + printf " %s\t%s\n" "com.apple.backupd.BackupMachineAddress" "$(xattr -p 'com.apple.backupd.BackupMachineAddress' "${BACKUPPATH}")" + printf " %s\t\t\t%s\n\n" "com.apple.backupd.HostUUID" "$(xattr -p 'com.apple.backupd.HostUUID' "${BACKUPPATH}")" + break + else + printf "\nOperation aborted. No action has been taken.\n\n" + break + fi +done