set -euf -o pipefail
function print_version {
printf "\
equine $(httm --version | cut -f2 -d' ')
" 1>&2
exit 0
}
function print_usage {
local equine="\e[31mequine\e[0m"
local httm="\e[31mhttm\e[0m"
printf "\
$equine is a script to mount APFS snapshots for use with $httm.
$equine has two modes: 1) mount and unmount of local APFS snapshots,
and 2) mount and unmount of remote APFS snapshots, located on a Time
Machine network volume (NAS).
This script is *not* for use with Time Machines which utilize direct
attached storage (DAS).
USAGE:
equine [OPTIONS]
OPTIONS:
--mount-local:
Attempt to mount your local Time Machine snapshots.
--unmount-local:
Attempt to unmount your local Time Machine snapshots.
--mount-remote:
Attempt to mount your remote Time Machine snapshots and the Time
Machine image file, and connect the server.
--unmount-remote:
Attempt to unmount your remote Time Machine snapshots and the Time
Machine image file, and disconnect the server.
--help:
Display this dialog.
--version:
Display script version.
" 1>&2
exit 1
}
print_err_exit() {
print_err "${@}"
exit 1
}
print_err() {
printf "%s\n" "Error: $*" 1>&2
}
function prep_exec {
[[ -n "$(
command -v plutil
exit 0
)" ]] || print_err_exit "'plutil' is required to execute 'equine'. Please check that 'plutil' is in your path."
[[ -n "$(
command -v tmutil
exit 0
)" ]] || print_err_exit "'tmutil' is required to execute 'equine'. Please check that 'tmutil' is in your path."
[[ -n "$(
command -v mount_apfs
exit 0
)" ]] || print_err_exit "'mount_apfs' is required to execute 'equine'. Please check that 'mount_apfs' is in your path."
[[ -n "$(
command -v mount_smbfs
exit 0
)" ]] || print_err_exit "'mount_smbfs' is required to execute 'equine'. Please check that 'mount_smbfs' is in your path."
[[ -n "$(
command -v mount
exit 0
)" ]] || print_err_exit "'mount' is required to execute 'equine'. Please check that 'mount' is in your path."
[[ -n "$(
command -v cut
exit 0
)" ]] || print_err_exit "'cut' is required to execute 'equine'. Please check that 'cut' is in your path."
[[ -n "$(
command -v grep
exit 0
)" ]] || print_err_exit "'grep' is required to execute 'equine'. Please check that 'grep' is in your path."
[[ -n "$(
command -v xargs
exit 0
)" ]] || print_err_exit "'xargs' is required to execute 'equine'. Please check that 'xargs' is in your path."
[[ -n "$(
command -v hdiutil
exit 0
)" ]] || print_err_exit "'hdiutil' is required to execute 'equine'. Please check that 'hdiutil' is in your path."
}
function mount_remote() {
local server="$( plutil -p /Library/Preferences/com.apple.TimeMachine.plist | grep "NetworkURL" | cut -d '"' -f4 )"
local mount_source="$( plutil -p /Library/Preferences/com.apple.TimeMachine.plist | grep "LastKnownVolumeName" | cut -d '"' -f4 )"
[[ -n "$server" ]] || print_err_exit "Could not determine server address, perhaps none is specified?"
[[ -n "$mount_source" ]] || print_err_exit "Could not determine mount source from server name"
local sys_defined_dir="$( find /Volumes/.timemachine -name "$mount_source" -print -quit )"
local dirname="/Volumes/$mount_source"
[[ -z "$sys_defined_dir" ]] || dirname="$sys_defined_dir"
if [[ "$( mount | grep -c "$dirname" )" -eq 0 ]]; then
printf "%s\n" "Connecting to remote Time Machine: $server ..."
[[ -d "$dirname" ]] || mkdir -p "$dirname" || \
print_err_exit "mkdir failed, likely because it was prevented by System Integrity Protection, may need to disable Filesystem Protections"
mount_smbfs -o nobrowse "$server" "$dirname" 2>/dev/null || true
else
printf "%s\n" "Skip connecting to remote server, as Time Machine already mounted ..."
fi
timeout 30s bash -c "until [[ "$( mount | grep -c "$dirname" )" -gt 0 ]]; do sleep 1; done" || \
print_err_exit "Wait for server to be mounted timed out. Quitting."
local image_name="$(plutil -p /Library/Preferences/com.apple.TimeMachine.plist | grep LocalizedDiskImageVolumeName | cut -d '"' -f4)"
[[ -n "$image_name" ]] || print_err_exit "Could not determine Time Machine disk image name, perhaps none is specified?"
if [[ "$( mount | grep -c "$image_name" )" -eq 0 ]]; then
printf "%s\n" "Mounting sparse bundle (this may include an fsck): $image_name ..."
local bundle_name="$( find "$dirname" -type d -iname "*.sparsebundle" -print -quit )"
[[ -n "$bundle_name" ]] || print_err_exit "Could not find sparsebundle in location specified"
hdiutil attach -readonly -nobrowse "$bundle_name" || print_err_exit "Attaching disk image failed. Quitting."
else
printf "%s\n" "Skip mounting sparse bundle, as $image_name appears to already be mounted ..."
fi
printf "%s\n" "Discovering backup locations (this can take a few seconds)..."
local backups="$( tmutil listbackups / )"
local device="$( mount | grep "$image_name" | cut -d' ' -f1 | tail -1 )"
local uuid="$( echo "$backups" | cut -d "/" -f4 | head -1 )"
[[ -n "$device" ]] || print_err_exit "Could not determine Time Machine device from image give"
[[ -n "$uuid" ]] || print_err_exit "Could not determine uuid from list of backup locations"
[[ "$( mount | grep -c "$image_name" )" -gt 0 ]] || print_err_exit "Time machine disk image did not mount"
[[ -d "/Volumes/.timemachine/$uuid" ]] || mkdir -p "/Volumes/.timemachine/$uuid" || \
print_err_exit "mkdir failed, likely because it was prevented by System Integrity Protection, may need to disable Filesystem Protections"
printf "%s\n" "Mounting snapshots..."
for snap in $( echo "$backups" | xargs basename ); do
[[ -d "/Volumes/.timemachine/$uuid/$snap" ]] || mkdir -p "/Volumes/.timemachine/$uuid/$snap" || \
print_err_exit "mkdir failed, likely because it was prevented by System Integrity Protection, may need to disable Filesystem Protections"
printf "%s\n" "Mounting snapshot "com.apple.TimeMachine.$snap" from "$device" at "/Volumes/.timemachine/$uuid/$snap""
[[ -d "/Volumes/.timemachine/$uuid/$snap" ]] && mount_apfs -o ro,nobrowse,noowners,noatime,nodev,nosuid -s "com.apple.TimeMachine.$snap" "$device" "/Volumes/.timemachine/$uuid/$snap" 2>/dev/null || true
done
}
function unmount_remote() {
printf "%s\n" "Unmounting any mounted snapshots...."
mount | grep "com.apple.TimeMachine.*.backup@" | cut -d' ' -f1 | xargs -I{} umount "{}" 2>/dev/null || true
local image_name="$(plutil -p /Library/Preferences/com.apple.TimeMachine.plist | grep LocalizedDiskImageVolumeName | cut -d '"' -f4)"
[[ -n "$image_name" ]] || print_err "Could not determine Time Machine disk image name, perhaps none is specified?"
local sub_device="$( mount | grep "$image_name" | cut -d' ' -f1 | tail -1 )"
local device="$( echo $sub_device | cut -d's' -f1 )"
[[ -n "$sub_device" ]] || print_err "Could not determine subdevice from disk image given"
[[ -n "$device" ]] || print_err "Could not determine device from disk image given"
local server="$( plutil -p /Library/Preferences/com.apple.TimeMachine.plist | grep "NetworkURL" | cut -d '"' -f4 )"
local mount_source="$( plutil -p /Library/Preferences/com.apple.TimeMachine.plist | grep "LastKnownVolumeName" | cut -d '"' -f4 )"
local sys_defined_dir="$( find /Volumes/.timemachine -name "$mount_source" -print -quit )"
local dirname="/Volumes/$mount_source"
[[ -z "$sys_defined_dir" ]] || dirname="$sys_defined_dir"
[[ "$(tmutil status | grep -c "Running = 0")" -gt 0 ]] || \
print_err_exit "Backup running. 'equine' will not unmount sparsebundle or server while backup is running. Quitting."
printf "%s\n" "Attempting to unmount Time Machine sparse bundle: $image_name ..."
[[ -z "$sub_device" ]] || diskutil unmount "$sub_device" 2>/dev/null || true
[[ -z "$device" ]] || diskutil unmountDisk "$device" 2>/dev/null || true
[[ -n "$server" ]] || print_err "Could not determine server, perhaps none is specified?"
[[ -n "$mount_source" ]] || print_err "Could not determine mount source from server name given"
printf "%s\n" "Attempting to unmount/disconnect from Time Machine server: $server ..."
[[ -z "$mount_source" ]] || diskutil unmount force "$dirname" 2>/dev/null || true
[[ -z "$mount_source" ]] || diskutil unmountDisk force "$dirname" 2>/dev/null || true
}
function mount_local() {
printf "%s\n" "Discovering backup locations (this can take a few seconds)..."
local backups="$( tmutil listlocalsnapshots /System/Volumes/Data | grep -v ':' )"
local mounts="$( mount )"
local device="$( echo "$mounts" | grep "/System/Volumes/Data " | cut -d' ' -f1 )"
local hostname="$( hostname )"
[[ -n "$device" ]] || print_err_exit "Could not determine Time Machine device from image give"
printf "%s\n" "Mounting snapshots..."
for snap in $( echo "$backups" ); do
if [[ $( echo "$mounts" | grep -c "$snap" ) -gt 0 ]]; then
continue
fi
local snap_uuid="$( echo $snap | cut -d'.' -f4 )"
[[ -d "/Volumes/com.apple.TimeMachine.localsnapshots/Backups.backupdb/$hostname/$snap_uuid/Data" ]] || \
mkdir -p "/Volumes/com.apple.TimeMachine.localsnapshots/Backups.backupdb/$hostname/$snap_uuid/Data" || \
print_err_exit "mkdir failed, likely because it was prevented by System Integrity Protection, may need to disable Filesystem Protections"
printf "%s\n" "Mounting snapshot "$snap" from "$device" at "/Volumes/com.apple.TimeMachine.localsnapshots/Backups.backupdb/$hostname/$snap_uuid/Data""
[[ -d "/Volumes/com.apple.TimeMachine.localsnapshots/Backups.backupdb/$hostname/$snap_uuid/Data" ]] && \
mount_apfs -o ro,nobrowse,noowners,noatime,nodev,nosuid -s "$snap" "$device" "/Volumes/com.apple.TimeMachine.localsnapshots/Backups.backupdb/$hostname/$snap_uuid/Data" 2>/dev/null || true
done
}
function unmount_local() {
[[ "$(tmutil status | grep -c "Running = 0")" -gt 0 ]] || \
print_err_exit "Backup running. 'equine' will not unmount local snapshots while backup is running. Quitting."
printf "%s\n" "Unmounting any mounted snapshots...."
mount | grep "com.apple.TimeMachine.*.local@" | cut -d' ' -f1 | xargs -I{} umount "{}" 2>/dev/null || true
}
function exec_equine() {
[[ $# -ge 1 ]] || print_usage
[[ "$1" != "-h" && "$1" != "--help" ]] || print_usage
[[ "$1" != "-V" && "$1" != "--version" ]] || print_version
[[ "$( uname )" == "Darwin" ]] || print_err_exit "This script requires you run it on MacOS"
[[ "$EUID" -eq 0 ]] || print_err_exit "This script requires you run it as root"
prep_exec
while [[ $# -ge 1 ]]; do
if [[ "$1" == "--mount-remote" ]]; then
mount_remote
break
elif [[ "$1" == "--unmount-remote" ]]; then
unmount_remote
break
elif [[ "$1" == "--mount-local" ]]; then
mount_local
break
elif [[ "$1" == "--unmount-local" ]]; then
unmount_local
break
else
print_err_exit "User must specify whether to mount or unmount the Time Machine volumes."
break
fi
done
}
exec_equine "${@}"