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).
This commit is contained in:
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# tmimport
|
||||||
|
Time Machine Importer
|
||||||
|
|
||||||
|
tmimport.sh <backup drive>
|
||||||
|
|
||||||
|
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.
|
||||||
87
dirdedupe.sh
Executable file
87
dirdedupe.sh
Executable file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# BITSC LICENSE NOTICE (MODIFIED ISC LICENSE)
|
||||||
|
#
|
||||||
|
# DIRECTORY DE-DUPLICATION ("Dirty Dupe")
|
||||||
|
#
|
||||||
|
# Copyright (c) 2022 Lee Ockert <torstenvl@gmail.com>
|
||||||
|
# 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
|
||||||
|
|
||||||
40
tmdiskenum.sh
Executable file
40
tmdiskenum.sh
Executable file
@ -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}"
|
||||||
154
tmimport.sh
Executable file
154
tmimport.sh
Executable file
@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# BITSC LICENSE NOTICE (MODIFIED ISC LICENSE)
|
||||||
|
#
|
||||||
|
# TIME MACHINE IMPORTER
|
||||||
|
#
|
||||||
|
# Copyright (c) 2022 Lee Ockert <torstenvl@gmail.com>
|
||||||
|
# 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} <backup drive>
|
||||||
|
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user