Files
lokale-scripte/macos/backup-bfds-storage-offsite/backup-bfds-storage-offsite.sh
T

214 lines
7.5 KiB
Bash
Executable File

#!/bin/bash
# Sichert die Freigaben von bfds auf die OFFSITE-Platte (VeraCrypt-Partition).
# macOS-Portierung von robocopy-bfds-offsite/bfds-offsite-backup.cmd.
# Kompatibel mit dem mit macOS ausgelieferten Bash 3.2.
#
# Ablauf: VeraCrypt-Partition entschluesseln und einbinden, SMB-Freigaben von
# bfds per rsync kopieren, Partition wieder aushaengen.
#
# Voraussetzung: VeraCrypt (+ macFUSE) installiert.
# Optional vorab setzen: DEVICE=/dev/disk4s1 ./backup-bfds-storage-offsite.sh
PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin"
export PATH
# Schlaf verhindern, solange dieses Script laeuft (-w $$ endet mit dem Script).
# -i Idle-Sleep, -m Platten-Sleep, -s System-Sleep (nur am Netzteil wirksam).
# Der Bildschirm darf ausgehen.
caffeinate -i -m -s -w $$ &
SHARES=(
photos
rob
movies
music
peggy
ebooks
)
VOLUME_LABEL="${VOLUME_LABEL:-NASOFFSITE1}"
VERACRYPT="${VERACRYPT:-/Applications/VeraCrypt.app/Contents/MacOS/VeraCrypt}"
MOUNT_POINT="${MOUNT_POINT:-/Volumes/$VOLUME_LABEL}"
SOURCE_HOST="${SOURCE_HOST:-bfds}"
DEVICE="${DEVICE:-}"
MOUNTED_BY_US=0
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
die() {
log "FEHLER: $*"
exit 1
}
unmount_volume() {
[ "$MOUNTED_BY_US" = "1" ] || return 0
log "Partition wird ausgehaengt"
"$VERACRYPT" --text --dismount "$MOUNT_POINT" --non-interactive \
|| log "WARNUNG: Aushaengen fehlgeschlagen, bitte in VeraCrypt manuell trennen"
MOUNTED_BY_US=0
}
# Die Disk-Nummer wechselt (disk4, disk6, ...). Gesucht wird daher die
# Partition 1 auf externen, physischen Platten, die >= MIN_SIZE_GB gross sind
# und nicht von macOS eingebunden werden koennen (Typ "Microsoft Basic Data",
# verschluesselt). Nur bei genau einem Treffer wird sie verwendet.
MIN_SIZE_GB="${MIN_SIZE_GB:-3000}"
find_offsite_partition() {
local disk part bytes
local found=()
for disk in $(diskutil list external physical | awk '/^\/dev\/disk/ {gsub(/[:]/,"",$1); sub("/dev/","",$1); print $1}'); do
part="${disk}s1"
diskutil info "$part" >/dev/null 2>&1 || continue
diskutil list "$disk" | grep -q "Microsoft Basic Data.*${part}\$" || continue
bytes="$(diskutil info "$disk" | sed -n 's/.*Disk Size:.*(\([0-9]*\) Bytes).*/\1/p' | head -1)"
[ -n "$bytes" ] && [ "$bytes" -ge $((MIN_SIZE_GB * 1000000000)) ] || continue
# Schon eingebundene Partition (normales Dateisystem) ist nicht die gesuchte
[ -z "$(diskutil info "$part" | sed -n 's/^ *Mount Point: *\(.\+\)$/\1/p')" ] || continue
found+=("/dev/$part")
done
if [ "${#found[@]}" -ne 1 ]; then
log "FEHLER: Keine eindeutige OFFSITE-Partition gefunden (Treffer: ${#found[@]})" >&2
return 1
fi
log "OFFSITE-Partition erkannt: ${found[0]}" >&2
echo "${found[0]}"
}
trap 'unmount_volume' EXIT
trap 'log "Abgebrochen"; exit 130' INT TERM
# **************************************************
# Einbinden der VeraCrypt Partition
# **************************************************
[ -x "$VERACRYPT" ] || die "VeraCrypt nicht gefunden: $VERACRYPT"
if [ -d "$MOUNT_POINT" ] && mount | grep -q " on $MOUNT_POINT "; then
log "Festplatte $VOLUME_LABEL ist bereits eingebunden: $MOUNT_POINT"
else
if [ -z "$DEVICE" ]; then
DEVICE="$(find_offsite_partition)" || exit 1
fi
# "disk4s1" und "/dev/disk4s1" akzeptieren
case "$DEVICE" in
disk*) DEVICE="/dev/$DEVICE" ;;
esac
[ -e "$DEVICE" ] || die "Device nicht gefunden: $DEVICE"
read -r -s -p "Bitte geben Sie das Passwort fuer die Festplatte $VOLUME_LABEL ein: " VOLUME_PASSWORD
echo
log "Festplatte $VOLUME_LABEL wird entschluesselt und eingebunden"
"$VERACRYPT" --text --non-interactive --mount "$DEVICE" "$MOUNT_POINT" \
--password="$VOLUME_PASSWORD" --pim=0 --keyfiles="" --protect-hidden=no \
|| die "Einbinden mit VeraCrypt fehlgeschlagen"
unset VOLUME_PASSWORD
MOUNTED_BY_US=1
fi
[ -d "$MOUNT_POINT" ] || die "Festplatte $VOLUME_LABEL nicht gefunden unter $MOUNT_POINT"
# **************************************************
# Kopieren der SMB-Freigaben
# **************************************************
EXIT_CODE=0
PROBE_TIMEOUT="${PROBE_TIMEOUT:-20}"
RSYNC_TIMEOUT="${RSYNC_TIMEOUT:-60}"
RSYNC_RETRIES="${RSYNC_RETRIES:-3}"
# Fuehrt einen Befehl mit Zeitlimit (Sekunden) aus; macOS hat kein timeout.
# Ein haengender Prozess (z.B. ls auf toter SMB-Verbindung) wird nicht
# abgewartet, sondern abgeschossen und zurueckgelassen.
run_with_timeout() {
local secs="$1" pid ticks=0
shift
"$@" >/dev/null 2>&1 &
pid=$!
while kill -0 "$pid" 2>/dev/null; do
if [ "$ticks" -ge $((secs * 10)) ]; then
kill -9 "$pid" 2>/dev/null
return 124
fi
sleep 0.1
ticks=$((ticks + 1))
done
wait "$pid"
}
# Prueft, ob die Freigabe wirklich gemountet ist, Verzeichnisse listbar sind
# und sich Dateiinhalt lesen laesst (Listing allein kann aus dem Cache kommen).
probe_source() {
local dir="$1" file
mount | grep -q " on $dir (smbfs" || return 1
ls -A "$dir" >/dev/null || return 1
file="$(find "$dir" -maxdepth 3 -type f ! -name '.*' -print -quit)" || return 1
[ -z "$file" ] || head -c 1 "$file" >/dev/null || return 1
}
run_rsync() {
local share="$1"
local source="/Volumes/$share"
if [ ! -d "$source" ]; then
log "Binde SMB-Freigabe smb://$SOURCE_HOST/$share ein"
# Unter sudo als eigentlicher Benutzer oeffnen, damit dessen Keychain/Finder genutzt wird
if [ -n "${SUDO_USER:-}" ]; then
sudo -u "$SUDO_USER" open "smb://$SOURCE_HOST/$share"
else
open "smb://$SOURCE_HOST/$share"
fi
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
[ -d "$source" ] && break
sleep 1
done
fi
if [ ! -d "$source" ]; then
log "WARNUNG: Quelle nicht gefunden, bitte SMB-Freigabe oeffnen: smb://$SOURCE_HOST/$share"
EXIT_CODE=1
return
fi
log "Pruefe Lesbarkeit von $source (Timeout ${PROBE_TIMEOUT}s)"
if ! run_with_timeout "$PROBE_TIMEOUT" probe_source "$source"; then
log "WARNUNG: $source nicht lesbar (haengt oder Fehler), ueberspringe $share. Freigabe im Finder trennen und neu verbinden."
EXIT_CODE=1
return
fi
log "Sichere $share"
# -r Unterverzeichnisse inkl. leerer, -l Symlinks, -t Zeitstempel (wie /COPY:DAT),
# --partial setzt abgebrochene Uebertragungen fort (wie /z). Kein --delete:
# robocopy /E spiegelt ebenfalls nicht. --timeout bricht bei SMB-Haengern ab.
# Bei Abbruch (z.B. SMB-Timeout) bis zu RSYNC_RETRIES mal weiter; --partial und
# der Zeitstempelvergleich sorgen dafuer, dass Fertiges nicht neu kopiert wird.
local rc attempt=1
while :; do
rsync -rlt -h -v --progress --partial --stats --timeout="$RSYNC_TIMEOUT" --exclude=".DS_Store" --exclude="._.DS_Store" "$source/" "$MOUNT_POINT/$share/"
rc=$?
[ "$rc" -eq 0 ] && break
[ "$attempt" -lt "$RSYNC_RETRIES" ] || break
attempt=$((attempt + 1))
log "rsync fuer $share endete mit Code $rc, neuer Versuch $attempt/$RSYNC_RETRIES in 15s"
sleep 15
run_with_timeout "$PROBE_TIMEOUT" probe_source "$source" || { log "WARNUNG: $source weiterhin nicht lesbar"; break; }
done
if [ "$rc" -ne 0 ]; then
log "WARNUNG: rsync fuer $share endete mit Code $rc"
EXIT_CODE=1
fi
}
for share in "${SHARES[@]}"; do
run_rsync "$share"
done
log "Backupprozess beendet"
exit "$EXIT_CODE"