migrate all backups scripts into new Repo called "Brücke"
This commit is contained in:
1 parent
46e13a4dd5
commit
44674bdc38
32 files changed
-1515
No files matched your search
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.backup-recordings-to-bfds</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>/Users/rob/code/lokale-scripte/macos/Backup-Recordings/backup-recordings-to-bfds.sh</string>
|
||||
</array>
|
||||
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>12</integer>
|
||||
<key>Minute</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/rob/code/lokale-scripte/macos/Backup-Recordings/run-backup-recordings_output.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/rob/code/lokale-scripte/macos/Backup-Recordings/run-backup-recordings_error.log</string>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Logfile definieren
|
||||
LOGFILE="$HOME/Library/Logs/backup-recordings.log"
|
||||
MAX_LOG_SIZE=$((3 * 1024 * 1024)) # 3MB in Bytes
|
||||
|
||||
# Funktion zur Überprüfung und Rotation des Logfiles
|
||||
rotate_log() {
|
||||
if [ -f "$LOGFILE" ]; then
|
||||
local size=$(stat -f%z "$LOGFILE" 2>/dev/null || echo 0)
|
||||
if [ "$size" -ge "$MAX_LOG_SIZE" ]; then
|
||||
# Behalte nur die letzten 50% des Logs
|
||||
local keep_lines=$(($(wc -l < "$LOGFILE") / 2))
|
||||
tail -n "$keep_lines" "$LOGFILE" > "${LOGFILE}.tmp"
|
||||
mv "${LOGFILE}.tmp" "$LOGFILE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Funktion für Log-Ausgaben mit Zeitstempel
|
||||
log() {
|
||||
local message="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
echo "$message" | tee -a "$LOGFILE"
|
||||
}
|
||||
|
||||
# Funktion für macOS Benachrichtigungen
|
||||
notify() {
|
||||
local subtitle="$1"
|
||||
local message="$2"
|
||||
osascript -e "display notification \"$message\" with title \"Backup Recordings\" subtitle \"$subtitle\""
|
||||
}
|
||||
|
||||
# Funktion zum Formatieren von Bytes in menschenlesbare Größe
|
||||
format_bytes() {
|
||||
local bytes=$1
|
||||
if [ "$bytes" -lt 1024 ]; then
|
||||
echo "${bytes} B"
|
||||
elif [ "$bytes" -lt 1048576 ]; then
|
||||
awk "BEGIN {printf \"%.1f KB\", $bytes/1024}"
|
||||
elif [ "$bytes" -lt 1073741824 ]; then
|
||||
awk "BEGIN {printf \"%.1f MB\", $bytes/1048576}"
|
||||
else
|
||||
awk "BEGIN {printf \"%.2f GB\", $bytes/1073741824}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Log rotieren falls nötig
|
||||
rotate_log
|
||||
|
||||
# Quelle und Ziel definieren
|
||||
SOURCE="/Users/rob/Recordings/Work/00_MacOS/"
|
||||
SERVER="bfds"
|
||||
REMOTE_PATH="/mnt/bfds-nix/rob/recordings/Progress/Work/00_MacOS/"
|
||||
TARGET="$SERVER:$REMOTE_PATH"
|
||||
|
||||
# Prüfen, ob SSH-Verbindung zum Server möglich ist
|
||||
log "Prüfe SSH-Verbindung zu $SERVER..."
|
||||
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$SERVER" "exit" 2>/dev/null; then
|
||||
log "Fehler: SSH-Verbindung zu $SERVER fehlgeschlagen"
|
||||
notify "Fehler" "Server $SERVER nicht erreichbar!"
|
||||
exit 1
|
||||
fi
|
||||
log "SSH-Verbindung erfolgreich"
|
||||
|
||||
# Prüfen, ob das Remote-Zielverzeichnis existiert
|
||||
log "Prüfe Remote-Zielverzeichnis..."
|
||||
if ! ssh "$SERVER" "test -d \"$REMOTE_PATH\""; then
|
||||
log "Fehler: Remote-Verzeichnis existiert nicht: $REMOTE_PATH"
|
||||
notify "Fehler" "Zielverzeichnis nicht gefunden!\n$REMOTE_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prüfen, ob Schreibrechte vorhanden sind
|
||||
if ! ssh "$SERVER" "test -w \"$REMOTE_PATH\""; then
|
||||
log "Fehler: Keine Schreibrechte für Remote-Verzeichnis: $REMOTE_PATH"
|
||||
notify "Fehler" "Keine Schreibrechte!\n$REMOTE_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Remote-Verzeichnis ist bereit für Backup"
|
||||
|
||||
# Sicherung starten
|
||||
log "Starte rsync..."
|
||||
RSYNC_OUTPUT=$(mktemp)
|
||||
log "Befehl: rsync -av --stats --no-owner --no-group \"$SOURCE\" > \"$RSYNC_OUTPUT\" \"$TARGET\""
|
||||
rsync -av --stats --no-owner --no-group "$SOURCE" "$TARGET" > "$RSYNC_OUTPUT" 2>&1
|
||||
|
||||
# Rsync-Output komplett ins Logfile schreiben
|
||||
cat "$RSYNC_OUTPUT" >> "$LOGFILE"
|
||||
|
||||
# -a → Archive-Modus (erhält Zeitstempel, Links, etc.)
|
||||
# -v → Verbose
|
||||
# --stats → Statistiken anzeigen
|
||||
# --no-owner → Besitzer nicht übertragen
|
||||
# --no-group → Gruppe nicht übertragen
|
||||
|
||||
# Statistiken auslesen - rsync gibt mehrere Formate aus, teste beide
|
||||
TOTAL_FILES=$(grep -E "Number of (regular )?files transferred:" "$RSYNC_OUTPUT" | awk '{print $NF}' | tr -d ',')
|
||||
if [ -z "$TOTAL_FILES" ]; then
|
||||
# Alternativer Versuch
|
||||
TOTAL_FILES=$(grep "files transferred:" "$RSYNC_OUTPUT" | awk '{print $(NF)}' | tr -d ',')
|
||||
fi
|
||||
|
||||
# Dateigröße in Bytes auslesen
|
||||
# macOS rsync gibt aus: "Total transferred file size: 86137390 B"
|
||||
TOTAL_BYTES=$(grep "Total transferred file size:" "$RSYNC_OUTPUT" | awk '{print $5}')
|
||||
# Konvertiere String in Integer
|
||||
TOTAL_BYTES=$((TOTAL_BYTES))
|
||||
|
||||
# Formatiere Größe in menschenlesbares Format
|
||||
TOTAL_SIZE=$(format_bytes "$TOTAL_BYTES")
|
||||
|
||||
# Fallback falls keine Werte gefunden wurden
|
||||
[ -z "$TOTAL_FILES" ] && TOTAL_FILES="0"
|
||||
|
||||
# Temporäre Datei löschen
|
||||
rm -f "$RSYNC_OUTPUT"
|
||||
|
||||
# Setze Besitzer und Rechte auf dem Server
|
||||
log "Setze Besitzer und Rechte..."
|
||||
ssh "$SERVER" "chown -R smbuser:smbuser \"$REMOTE_PATH\" && chmod -R 777 \"$REMOTE_PATH\" && find \"$REMOTE_PATH\" -type f -exec chmod 666 {} \;" 2>&1 | tee -a "$LOGFILE"
|
||||
|
||||
# Erfolgsmeldung ausgeben
|
||||
log "Backup abgeschlossen: $TOTAL_FILES Dateien, $TOTAL_SIZE übertragen"
|
||||
|
||||
# Optional: Notification für macOS anzeigen
|
||||
notify "Abgeschlossen" "$TOTAL_FILES Dateien übertragen ($TOTAL_SIZE)"
|
||||
@@ -1,253 +0,0 @@
|
||||
#!/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
|
||||
|
||||
# Run-Protokoll: pro Lauf eine JSON-Zeile, aus der das Dashboard den letzten
|
||||
# durchgehenden Run (alle Freigaben fehlerfrei) abliest.
|
||||
RUN_LOG_FILE="${RUN_LOG_FILE:-}"
|
||||
RUN_START="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
SHARE_RESULTS=""
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Ergebnis einer Freigabe merken: ok, source_missing, unreadable oder rsync_<code>
|
||||
record_share() {
|
||||
SHARE_RESULTS="${SHARE_RESULTS:+$SHARE_RESULTS,}\"$1\":\"$2\""
|
||||
}
|
||||
|
||||
# Haengt am Ende jedes Laufs (auch bei Fehler oder Abbruch) eine JSON-Zeile an
|
||||
# ~/Library/Logs/bfds-offsite-backup.jsonl an. "complete" ist nur wahr, wenn das
|
||||
# Script mit 0 endete und jede Freigabe "ok" war.
|
||||
write_run_record() {
|
||||
local rc="$1" end ok total="${#SHARES[@]}" complete=false home
|
||||
end="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
ok="$(printf '%s' "$SHARE_RESULTS" | grep -o ':"ok"' | wc -l | tr -d ' ')"
|
||||
[ "$rc" -eq 0 ] && [ "$ok" -eq "$total" ] && complete=true
|
||||
if [ -z "$RUN_LOG_FILE" ]; then
|
||||
home="$HOME"
|
||||
if [ -n "${SUDO_USER:-}" ]; then
|
||||
home="$(dscl . -read "/Users/$SUDO_USER" NFSHomeDirectory 2>/dev/null | awk '{print $2}')"
|
||||
[ -n "$home" ] || home="$HOME"
|
||||
fi
|
||||
RUN_LOG_FILE="$home/Library/Logs/bfds-offsite-backup.jsonl"
|
||||
fi
|
||||
mkdir -p "$(dirname "$RUN_LOG_FILE")" 2>/dev/null
|
||||
printf '{"start":"%s","end":"%s","exit_code":%s,"complete":%s,"shares":{%s}}\n' \
|
||||
"$RUN_START" "$end" "$rc" "$complete" "$SHARE_RESULTS" >> "$RUN_LOG_FILE" \
|
||||
|| { log "WARNUNG: Run-Protokoll konnte nicht geschrieben werden: $RUN_LOG_FILE"; return 0; }
|
||||
# Unter sudo gehoert die Datei sonst root
|
||||
if [ -n "${SUDO_USER:-}" ]; then chown "$SUDO_USER" "$RUN_LOG_FILE" 2>/dev/null || true; fi
|
||||
}
|
||||
|
||||
# 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 'rc=$?; unmount_volume; write_run_record "$rc"' 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
|
||||
record_share "$share" source_missing
|
||||
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
|
||||
record_share "$share" unreadable
|
||||
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
|
||||
record_share "$share" "rsync_$rc"
|
||||
else
|
||||
record_share "$share" ok
|
||||
fi
|
||||
}
|
||||
|
||||
for share in "${SHARES[@]}"; do
|
||||
run_rsync "$share"
|
||||
done
|
||||
|
||||
log "Backupprozess beendet"
|
||||
exit "$EXIT_CODE"
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Erstellt ~/Applications/BFDS Offsite Backup.app, damit das Backup per
|
||||
# Spotlight ("bfds offsite backup") gestartet werden kann. Die App oeffnet
|
||||
# ein Terminal-Fenster und startet dort launch-backup.sh (Header, dann sudo-Start)
|
||||
# (Passwort-Abfragen und Ausgabe brauchen ein Terminal).
|
||||
# Kompatibel mit dem mit macOS ausgelieferten Bash 3.2.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APP_NAME="${APP_NAME:-BFDS Offsite Backup}"
|
||||
APP_DIR="${APP_DIR:-$HOME/Applications}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BACKUP_SCRIPT="$SCRIPT_DIR/launch-backup.sh"
|
||||
APP_PATH="$APP_DIR/$APP_NAME.app"
|
||||
|
||||
[ -x "$BACKUP_SCRIPT" ] || { echo "FEHLER: $BACKUP_SCRIPT nicht ausfuehrbar" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$APP_DIR"
|
||||
rm -rf "$APP_PATH"
|
||||
|
||||
# Pfad fuer AppleScript-String und Shell quoten
|
||||
SHELL_CMD="$(printf '%q' "$BACKUP_SCRIPT")"
|
||||
SHELL_CMD="${SHELL_CMD//\\/\\\\}"
|
||||
SHELL_CMD="${SHELL_CMD//\"/\\\"}"
|
||||
|
||||
/usr/bin/osacompile -o "$APP_PATH" <<EOF
|
||||
set wasRunning to application "Terminal" is running
|
||||
tell application "Terminal"
|
||||
activate
|
||||
if wasRunning then
|
||||
do script "$SHELL_CMD"
|
||||
else
|
||||
-- Terminal oeffnet beim Start selbst ein Fenster: dieses mitverwenden
|
||||
repeat 50 times
|
||||
if (count of windows) > 0 then exit repeat
|
||||
delay 0.1
|
||||
end repeat
|
||||
if (count of windows) > 0 then
|
||||
do script "$SHELL_CMD" in window 1
|
||||
else
|
||||
do script "$SHELL_CMD"
|
||||
end if
|
||||
end if
|
||||
end tell
|
||||
EOF
|
||||
|
||||
echo "Installiert: $APP_PATH"
|
||||
echo "Spotlight: \"bfds offsite backup\" eintippen (Indexierung kann kurz dauern)."
|
||||
|
||||
# Standardmaessig heisst die App intern "applet" (so erscheint sie auch in den
|
||||
# Datenschutz-Listen). Ausfuehrbare Datei, Bundle-Name und -ID umbenennen und neu signieren.
|
||||
PLIST="$APP_PATH/Contents/Info.plist"
|
||||
EXEC_NAME="BFDS-Offsite-Backup"
|
||||
mv "$APP_PATH/Contents/MacOS/applet" "$APP_PATH/Contents/MacOS/$EXEC_NAME"
|
||||
PB=/usr/libexec/PlistBuddy
|
||||
"$PB" -c "Set :CFBundleExecutable $EXEC_NAME" "$PLIST"
|
||||
"$PB" -c "Set :CFBundleName $APP_NAME" "$PLIST"
|
||||
"$PB" -c "Add :CFBundleIdentifier string de.streber24.bfds-offsite-backup" "$PLIST" 2>/dev/null \
|
||||
|| "$PB" -c "Set :CFBundleIdentifier de.streber24.bfds-offsite-backup" "$PLIST"
|
||||
"$PB" -c "Add :CFBundleDisplayName string $APP_NAME" "$PLIST" 2>/dev/null \
|
||||
|| "$PB" -c "Set :CFBundleDisplayName $APP_NAME" "$PLIST"
|
||||
/usr/bin/codesign --force --deep --sign - "$APP_PATH"
|
||||
|
||||
# Voller Festplattenzugriff laesst sich nicht per Script erteilen (macOS schuetzt
|
||||
# die Freigabe, nur MDM oder der Nutzer selbst kann sie setzen). Deshalb: Einstellungen
|
||||
# oeffnen und die App im Finder markieren, zum Hineinziehen in die Liste.
|
||||
echo
|
||||
echo "Bitte jetzt einmalig 'Voller Festplattenzugriff' fuer die App erteilen:"
|
||||
echo " App aus dem Finder-Fenster in die Liste ziehen (oder + klicken) und Schalter aktivieren."
|
||||
open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
|
||||
open -R "$APP_PATH"
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Startet das Backup aus der Spotlight-App: zeigt zuerst einen Willkommens-
|
||||
# Header samt Hinweis auf die sudo-Passwortabfrage, dann das eigentliche Script.
|
||||
# Kompatibel mit dem mit macOS ausgelieferten Bash 3.2.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
clear
|
||||
cat <<'BANNER'
|
||||
#####################################################
|
||||
## ##
|
||||
## BFDS OFFSITE BACKUP ##
|
||||
## ##
|
||||
## NAS-Freigaben -> verschluesselte Platte ##
|
||||
## ##
|
||||
#####################################################
|
||||
|
||||
Willkommen!
|
||||
|
||||
1. Zuerst wird jetzt Dein MAC-PASSWORT (sudo) abgefragt,
|
||||
damit das Programm gestartet werden kann.
|
||||
Beim Tippen erscheinen keine Zeichen.
|
||||
2. Danach folgt das Passwort der VeraCrypt-Festplatte.
|
||||
|
||||
BANNER
|
||||
|
||||
sudo "$SCRIPT_DIR/backup-bfds-storage-offsite.sh"
|
||||
status=$?
|
||||
|
||||
echo
|
||||
read -r -n 1 -s -p "Fertig (Exit-Code $status). Taste druecken zum Schliessen... "
|
||||
echo
|
||||
exit "$status"
|
||||
@@ -1,96 +0,0 @@
|
||||
# Paperless-ngx-Backup von TrueNAS auf macOS
|
||||
|
||||
Das Script ist fuer die auf `bfds` erkannte Installation vorbereitet:
|
||||
|
||||
- TrueNAS 25.04.2.6
|
||||
- TrueNAS-App `paperless-ngx` 1.6.37
|
||||
- Paperless-ngx 3.0.2
|
||||
- Container `ix-paperless-ngx-paperless-1`
|
||||
|
||||
Es pflegt einen dauerhaften Paperless-Export und synchronisiert ihn ungezippt
|
||||
auf den Mac. Nach dem ersten vollstaendigen Lauf werden nur neue, geaenderte
|
||||
oder geloeschte Dateien abgeglichen. Es entstehen keine Archive und keine
|
||||
taeglichen Vollkopien.
|
||||
|
||||
Der lokale Stand befindet sich hier:
|
||||
|
||||
```text
|
||||
~/Backups/paperless-ngx/current/
|
||||
├── backup-info.txt
|
||||
├── paperless-export/
|
||||
│ ├── manifest.json
|
||||
│ ├── metadata.json
|
||||
│ ├── archive/
|
||||
│ ├── originals/
|
||||
│ └── thumbnails/
|
||||
└── truenas-app-config/
|
||||
```
|
||||
|
||||
`paperless-export` enthaelt Dokumente, Thumbnails, Metadaten, Benutzer und die
|
||||
exportierten Datenbankinhalte. `truenas-app-config` enthaelt zusaetzlich die
|
||||
TrueNAS-App-Einstellungen und die Konfiguration frueherer App-Versionen.
|
||||
|
||||
## Testen und ausfuehren
|
||||
|
||||
```bash
|
||||
./backup-paperless-ngx.sh --check
|
||||
./backup-paperless-ngx.sh
|
||||
```
|
||||
|
||||
`--check` prueft SSH, Container und Mounts. Ausserdem wird ein kleiner
|
||||
temporaerer `document_exporter --data-only`-Lauf ausgefuehrt und dessen
|
||||
Manifest kontrolliert. Der Testexport wird sofort wieder entfernt.
|
||||
|
||||
Alle Werte lassen sich ueber `~/.config/paperless-backup/config` aendern;
|
||||
siehe `config.example`. Voraussetzung ist, dass
|
||||
`ssh -o BatchMode=yes bfds true` ohne Passwortdialog funktioniert.
|
||||
|
||||
## Taeglich um 03:00 Uhr ausfuehren
|
||||
|
||||
```bash
|
||||
./install-launchagent.sh
|
||||
```
|
||||
|
||||
Der Installer kopiert das Script nach
|
||||
`~/Library/Application Support/PaperlessBackup`, legt bei Bedarf die
|
||||
Konfiguration an und registriert einen LaunchAgent. Ein manueller Lauf des
|
||||
installierten Jobs ist mit folgendem Befehl moeglich:
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$(id -u)/de.rob.paperless-ngx-backup
|
||||
```
|
||||
|
||||
Logs stehen in `~/Library/Logs/paperless-ngx-backup.log` sowie in den beiden
|
||||
`paperless-ngx-launchd.*.log`-Dateien.
|
||||
|
||||
## Wiederherstellung
|
||||
|
||||
1. Eine frische Paperless-Instanz mit derselben Paperless-Version wie in
|
||||
`backup-info.txt` bereitstellen.
|
||||
2. Den Inhalt von `current/paperless-export` in das Export-Volume der neuen
|
||||
Instanz kopieren und dort `document_importer` ausfuehren.
|
||||
3. `current/truenas-app-config` dient als Referenz fuer Ports, Storage,
|
||||
Umgebungswerte und Secrets. Diese Dateien nicht blind in eine laufende
|
||||
TrueNAS-App zurueckkopieren.
|
||||
|
||||
Der Paperless-Exporter sichert keine API-Tokens; sie muessen nach einem Import
|
||||
neu erzeugt werden.
|
||||
|
||||
## Verhalten und Sicherheit
|
||||
|
||||
Das ist ein Spiegel des aktuellen Paperless-Stands. Wird ein Dokument in
|
||||
Paperless geloescht, wird es beim naechsten erfolgreichen Lauf auch aus diesem
|
||||
Export entfernt. Fuer historische Staende sollte der Ordner zusaetzlich durch
|
||||
Time Machine oder ein anderes versionierendes Backup gesichert werden.
|
||||
|
||||
Der Export enthaelt alle Dokumente und moegliche Klartext-Secrets aus der
|
||||
TrueNAS-App-Konfiguration. Das uebergeordnete Zielverzeichnis ist nur fuer den
|
||||
aktuellen macOS-Benutzer zugaenglich. FileVault sollte aktiviert sein.
|
||||
|
||||
Auf dem TrueNAS-Apps-Pool bleibt ebenfalls ein ungezipptes Exportverzeichnis
|
||||
`macos-backup-current` bestehen. Es wird von Paperless bei jedem Lauf
|
||||
inkrementell aktualisiert.
|
||||
|
||||
Bereits mit einer aelteren Scriptversion erzeugte `.tar.gz`-Archive werden
|
||||
nicht automatisch geloescht. Sie koennen nach erfolgreicher Kontrolle des
|
||||
neuen `current`-Ordners manuell entfernt werden.
|
||||
@@ -1,290 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Unkomprimierter, inkrementell aktualisierter Paperless-Export auf diesen Mac.
|
||||
# Kompatibel mit dem mit macOS ausgelieferten Bash 3.2.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin"
|
||||
export PATH
|
||||
|
||||
SERVER="${SERVER:-bfds}"
|
||||
APP_NAME="${APP_NAME:-paperless-ngx}"
|
||||
PAPERLESS_CONTAINER="${PAPERLESS_CONTAINER:-ix-paperless-ngx-paperless-1}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-$HOME/Backups/paperless-ngx}"
|
||||
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-10}"
|
||||
NOTIFY="${NOTIFY:-1}"
|
||||
|
||||
CONFIG_FILE="${PAPERLESS_BACKUP_CONFIG:-$HOME/.config/paperless-backup/config}"
|
||||
LOG_FILE="${PAPERLESS_BACKUP_LOG:-$HOME/Library/Logs/paperless-ngx-backup.log}"
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# Die Datei ist eine benutzereigene Shell-Konfiguration (KEY=WERT).
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
SSH=(/usr/bin/ssh -o BatchMode=yes -o ConnectTimeout="$SSH_CONNECT_TIMEOUT")
|
||||
RSYNC_RSH="/usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=$SSH_CONNECT_TIMEOUT"
|
||||
APP_CONFIG_DIR="/mnt/.ix-apps/app_configs/$APP_NAME"
|
||||
REMOTE_CURRENT_DIR="/usr/src/paperless/export/macos-backup-current"
|
||||
LOCAL_CURRENT_DIR="$BACKUP_DIR/current"
|
||||
|
||||
LOCK_DIR="${TMPDIR:-/tmp}/paperless-ngx-backup-${UID}.lock"
|
||||
LOCAL_CHECK_DIR=""
|
||||
REMOTE_EXPORT_DIR=""
|
||||
REMOTE_EXPORT_HOST_DIR=""
|
||||
BACKUP_SUCCEEDED=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Aufruf: backup-paperless-ngx.sh [--check | --help]
|
||||
|
||||
ohne Option Lokalen Paperless-Export inkrementell aktualisieren
|
||||
--check Zusaetzlich einen temporaeren Datenbankexport testen
|
||||
--help Diese Hilfe anzeigen
|
||||
|
||||
Optionale Konfiguration: ~/.config/paperless-backup/config
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
local message
|
||||
message="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
printf '%s\n' "$message" | /usr/bin/tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
warn() {
|
||||
log "WARNUNG: $*"
|
||||
}
|
||||
|
||||
notify() {
|
||||
[ "$NOTIFY" = "1" ] || return 0
|
||||
/usr/bin/osascript - "$1" "$2" >/dev/null 2>&1 <<'APPLESCRIPT' || true
|
||||
on run argv
|
||||
display notification (item 2 of argv) with title "Paperless-ngx Backup" subtitle (item 1 of argv)
|
||||
end run
|
||||
APPLESCRIPT
|
||||
}
|
||||
|
||||
die() {
|
||||
log "FEHLER: $*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
trap - EXIT INT TERM
|
||||
set +e
|
||||
|
||||
if [ -n "$REMOTE_EXPORT_DIR" ]; then
|
||||
case "$REMOTE_EXPORT_DIR" in
|
||||
/usr/src/paperless/export/macos-backup-check-*)
|
||||
"${SSH[@]}" "$SERVER" \
|
||||
"docker exec '$PAPERLESS_CONTAINER' rm -rf -- '$REMOTE_EXPORT_DIR'" \
|
||||
>/dev/null 2>&1 || true
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ -n "$LOCAL_CHECK_DIR" ] && [ -d "$LOCAL_CHECK_DIR" ]; then
|
||||
case "$LOCAL_CHECK_DIR" in
|
||||
"$BACKUP_DIR"/.check.*) /bin/rm -rf -- "$LOCAL_CHECK_DIR" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
/bin/rmdir "$LOCK_DIR" >/dev/null 2>&1 || true
|
||||
|
||||
if [ "$exit_code" -ne 0 ] && [ "$BACKUP_SUCCEEDED" -ne 1 ]; then
|
||||
notify "Fehlgeschlagen" "Details: $LOG_FILE"
|
||||
fi
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
validate_configuration() {
|
||||
case "$SERVER" in
|
||||
*[!A-Za-z0-9_.:@-]*|'') die "Ungueltiger SERVER-Wert: $SERVER" ;;
|
||||
esac
|
||||
case "$APP_NAME" in
|
||||
*[!A-Za-z0-9_.-]*|'') die "Ungueltiger APP_NAME-Wert: $APP_NAME" ;;
|
||||
esac
|
||||
case "$PAPERLESS_CONTAINER" in
|
||||
*[!A-Za-z0-9_.-]*|'') die "Ungueltiger Containername: $PAPERLESS_CONTAINER" ;;
|
||||
esac
|
||||
case "$SSH_CONNECT_TIMEOUT" in
|
||||
*[!0-9]*|'') die "SSH_CONNECT_TIMEOUT muss eine ganze Zahl sein" ;;
|
||||
esac
|
||||
case "$BACKUP_DIR" in
|
||||
/*) ;;
|
||||
*) die "BACKUP_DIR muss ein absoluter Pfad sein" ;;
|
||||
esac
|
||||
[ "$BACKUP_DIR" != "/" ] || die "Das Wurzelverzeichnis darf nicht BACKUP_DIR sein"
|
||||
}
|
||||
|
||||
check_remote() {
|
||||
local running
|
||||
|
||||
log "Pruefe SSH-Verbindung zu $SERVER ..."
|
||||
"${SSH[@]}" "$SERVER" true >/dev/null 2>&1 || \
|
||||
die "SSH-Verbindung fehlgeschlagen (fuer launchd ist ein SSH-Key ohne Rueckfrage erforderlich)"
|
||||
|
||||
running=$("${SSH[@]}" "$SERVER" \
|
||||
"docker inspect --format='{{.State.Running}}' '$PAPERLESS_CONTAINER' 2>/dev/null") || \
|
||||
die "Paperless-Container $PAPERLESS_CONTAINER wurde nicht gefunden"
|
||||
[ "$running" = "true" ] || die "Paperless-Container laeuft nicht"
|
||||
|
||||
"${SSH[@]}" "$SERVER" "test -d '$APP_CONFIG_DIR' && test -r '$APP_CONFIG_DIR'" || \
|
||||
die "TrueNAS-App-Konfiguration ist nicht lesbar: $APP_CONFIG_DIR"
|
||||
|
||||
REMOTE_EXPORT_HOST_DIR=$("${SSH[@]}" "$SERVER" \
|
||||
"docker inspect --format='{{range .Mounts}}{{if eq .Destination \"/usr/src/paperless/export\"}}{{.Source}}{{end}}{{end}}' '$PAPERLESS_CONTAINER'")
|
||||
case "$REMOTE_EXPORT_HOST_DIR" in
|
||||
/mnt/.ix-apps/docker/volumes/*/_data) ;;
|
||||
*) die "Export-Mount des Paperless-Containers konnte nicht sicher ermittelt werden" ;;
|
||||
esac
|
||||
|
||||
# document_exporter wird vom Container-Wrapper von root auf den Benutzer
|
||||
# paperless heruntergestuft. Daher muessen Check und Ordnererstellung mit
|
||||
# genau diesem Benutzer erfolgen.
|
||||
"${SSH[@]}" "$SERVER" \
|
||||
"docker exec --user paperless:paperless '$PAPERLESS_CONTAINER' test -w /usr/src/paperless/export" || \
|
||||
die "Paperless-Exportverzeichnis ist nicht beschreibbar"
|
||||
|
||||
log "Paperless-Container und TrueNAS-Speicherorte sind bereit"
|
||||
}
|
||||
|
||||
write_backup_info() {
|
||||
local info_file="$1"
|
||||
|
||||
{
|
||||
printf 'Backup created on Mac: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
printf 'SSH server: %s\n' "$SERVER"
|
||||
printf 'TrueNAS app name: %s\n' "$APP_NAME"
|
||||
printf 'Paperless container: %s\n' "$PAPERLESS_CONTAINER"
|
||||
"${SSH[@]}" "$SERVER" "printf 'TrueNAS version: '; cat /etc/version; printf '\nContainer image (Paperless version): '; docker inspect --format='{{.Config.Image}}' '$PAPERLESS_CONTAINER'"
|
||||
} > "$info_file"
|
||||
}
|
||||
|
||||
check_capacity() {
|
||||
local source_kb remote_current_kb local_current_kb
|
||||
local remote_needed_kb local_needed_kb remote_free_kb local_free_kb
|
||||
|
||||
source_kb=$("${SSH[@]}" "$SERVER" \
|
||||
"du -sk /mnt/.ix-apps/app_mounts/'$APP_NAME'/media /mnt/.ix-apps/app_mounts/'$APP_NAME'/data 2>/dev/null | awk '{s+=\$1} END {print s+0}'") || source_kb=0
|
||||
remote_free_kb=$("${SSH[@]}" "$SERVER" \
|
||||
"df -Pk '$REMOTE_EXPORT_HOST_DIR' | awk 'NR==2 {print \$4}'") || remote_free_kb=0
|
||||
local_free_kb=$(/bin/df -Pk "$BACKUP_DIR" | /usr/bin/awk 'NR==2 {print $4}')
|
||||
remote_current_kb=$("${SSH[@]}" "$SERVER" \
|
||||
"du -sk '$REMOTE_EXPORT_HOST_DIR/macos-backup-current' 2>/dev/null | awk '{print \$1+0}'") || remote_current_kb=0
|
||||
local_current_kb=$(/usr/bin/du -sk "$LOCAL_CURRENT_DIR/paperless-export" 2>/dev/null | /usr/bin/awk '{print $1+0}') || local_current_kb=0
|
||||
|
||||
if [ "$source_kb" -gt 0 ]; then
|
||||
log "Geschaetzte Paperless-Datenmenge: $((source_kb / 1024)) MB"
|
||||
remote_needed_kb=$((source_kb - remote_current_kb))
|
||||
local_needed_kb=$((source_kb - local_current_kb))
|
||||
[ "$remote_needed_kb" -gt 0 ] || remote_needed_kb=0
|
||||
[ "$local_needed_kb" -gt 0 ] || local_needed_kb=0
|
||||
[ "$remote_free_kb" -gt "$remote_needed_kb" ] || \
|
||||
warn "Auf dem TrueNAS-Apps-Pool koennte der Platz fuer den Export knapp sein"
|
||||
[ "$local_free_kb" -gt "$local_needed_kb" ] || \
|
||||
warn "Auf dem Mac koennte der Platz fuer den lokalen Export knapp sein"
|
||||
fi
|
||||
}
|
||||
|
||||
check_document_exporter() {
|
||||
local check_id check_host_dir
|
||||
|
||||
check_id="$(date '+%Y-%m-%d_%H-%M-%S')-$$"
|
||||
REMOTE_EXPORT_DIR="/usr/src/paperless/export/macos-backup-check-$check_id"
|
||||
check_host_dir="$REMOTE_EXPORT_HOST_DIR/macos-backup-check-$check_id"
|
||||
LOCAL_CHECK_DIR=$(/usr/bin/mktemp -d "$BACKUP_DIR/.check.XXXXXX")
|
||||
|
||||
log "Teste document_exporter und inkrementelle Aktualisierung ..."
|
||||
if ! "${SSH[@]}" "$SERVER" \
|
||||
"docker exec --user paperless:paperless '$PAPERLESS_CONTAINER' mkdir -p '$REMOTE_EXPORT_DIR' && docker exec '$PAPERLESS_CONTAINER' document_exporter --no-progress-bar --data-only --compare-json --delete --use-folder-prefix '$REMOTE_EXPORT_DIR' && docker exec '$PAPERLESS_CONTAINER' document_exporter --no-progress-bar --data-only --compare-json --delete --use-folder-prefix '$REMOTE_EXPORT_DIR' && docker exec '$PAPERLESS_CONTAINER' test -s '$REMOTE_EXPORT_DIR/manifest.json'" \
|
||||
2>&1 | /usr/bin/tee -a "$LOG_FILE"; then
|
||||
die "Test des document_exporter ist fehlgeschlagen"
|
||||
fi
|
||||
|
||||
log "Teste inkrementelle rsync-Uebertragung auf den Mac ..."
|
||||
/usr/bin/rsync -a --delete --no-owner --no-group -e "$RSYNC_RSH" \
|
||||
"$SERVER:$check_host_dir/" "$LOCAL_CHECK_DIR/" \
|
||||
>> "$LOG_FILE" 2>&1 || die "Test der rsync-Uebertragung ist fehlgeschlagen"
|
||||
[ -s "$LOCAL_CHECK_DIR/manifest.json" ] || \
|
||||
die "Der rsync-Test hat kein gueltiges Manifest uebertragen"
|
||||
|
||||
"${SSH[@]}" "$SERVER" \
|
||||
"docker exec '$PAPERLESS_CONTAINER' rm -rf -- '$REMOTE_EXPORT_DIR'" \
|
||||
>/dev/null 2>&1 || die "Temporaerer Testexport konnte nicht entfernt werden"
|
||||
REMOTE_EXPORT_DIR=""
|
||||
/bin/rm -rf -- "$LOCAL_CHECK_DIR"
|
||||
LOCAL_CHECK_DIR=""
|
||||
log "document_exporter, Aktualisierung, Manifest und rsync erfolgreich getestet"
|
||||
}
|
||||
|
||||
main() {
|
||||
local mode="backup" export_size remote_current_host
|
||||
|
||||
case "${1:-}" in
|
||||
'') ;;
|
||||
--check) mode="check" ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
*) usage >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
case "$BACKUP_DIR" in
|
||||
/*) ;;
|
||||
*) printf 'FEHLER: BACKUP_DIR muss ein absoluter Pfad sein\n' >&2; exit 1 ;;
|
||||
esac
|
||||
[ "$BACKUP_DIR" != "/" ] || { printf 'FEHLER: Ungueltiger BACKUP_DIR\n' >&2; exit 1; }
|
||||
|
||||
/bin/mkdir -p "$(dirname "$LOG_FILE")" "$BACKUP_DIR"
|
||||
/bin/chmod 700 "$BACKUP_DIR" 2>/dev/null || true
|
||||
validate_configuration
|
||||
|
||||
if ! /bin/mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
die "Ein Paperless-Backup laeuft bereits (Lock: $LOCK_DIR)"
|
||||
fi
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
check_remote
|
||||
if [ "$mode" = "check" ]; then
|
||||
check_document_exporter
|
||||
write_backup_info /dev/stdout
|
||||
log "Pruefung erfolgreich; temporaerer Testexport entfernt und kein Backup auf dem Mac gespeichert"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_capacity
|
||||
/bin/mkdir -p "$LOCAL_CURRENT_DIR/paperless-export" "$LOCAL_CURRENT_DIR/truenas-app-config"
|
||||
remote_current_host="$REMOTE_EXPORT_HOST_DIR/macos-backup-current"
|
||||
|
||||
log "Aktualisiere den dauerhaften Paperless-Export ..."
|
||||
if ! "${SSH[@]}" "$SERVER" \
|
||||
"docker exec --user paperless:paperless '$PAPERLESS_CONTAINER' mkdir -p '$REMOTE_CURRENT_DIR' && docker exec '$PAPERLESS_CONTAINER' document_exporter --no-progress-bar --compare-json --delete --use-folder-prefix '$REMOTE_CURRENT_DIR'" \
|
||||
2>&1 | /usr/bin/tee -a "$LOG_FILE"; then
|
||||
die "document_exporter ist fehlgeschlagen"
|
||||
fi
|
||||
|
||||
log "Synchronisiere neue und geaenderte Dokumente auf den Mac ..."
|
||||
/usr/bin/rsync -a --delete --no-owner --no-group -e "$RSYNC_RSH" \
|
||||
"$SERVER:$remote_current_host/" "$LOCAL_CURRENT_DIR/paperless-export/" \
|
||||
>> "$LOG_FILE" 2>&1 || die "Uebertragung des Paperless-Exports ist fehlgeschlagen"
|
||||
[ -s "$LOCAL_CURRENT_DIR/paperless-export/manifest.json" ] || \
|
||||
die "Der lokale Export enthaelt kein gueltiges manifest.json"
|
||||
|
||||
log "Sichere die TrueNAS-App-Konfiguration ..."
|
||||
/usr/bin/rsync -a --delete --no-owner --no-group -e "$RSYNC_RSH" \
|
||||
"$SERVER:$APP_CONFIG_DIR/" "$LOCAL_CURRENT_DIR/truenas-app-config/" \
|
||||
>> "$LOG_FILE" 2>&1 || die "Uebertragung der App-Konfiguration ist fehlgeschlagen"
|
||||
|
||||
write_backup_info "$LOCAL_CURRENT_DIR/backup-info.txt"
|
||||
export_size=$(/usr/bin/du -sh "$LOCAL_CURRENT_DIR" | /usr/bin/awk '{print $1}')
|
||||
BACKUP_SUCCEEDED=1
|
||||
log "Backup erfolgreich aktualisiert: $LOCAL_CURRENT_DIR ($export_size)"
|
||||
notify "Erfolgreich aktualisiert" "$LOCAL_CURRENT_DIR – $export_size"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,12 +0,0 @@
|
||||
# Diese Datei nach ~/.config/paperless-backup/config kopieren und anpassen.
|
||||
# Die Standardwerte passen bereits zur untersuchten Installation auf "bfds".
|
||||
|
||||
SERVER="bfds"
|
||||
APP_NAME="paperless-ngx"
|
||||
PAPERLESS_CONTAINER="ix-paperless-ngx-paperless-1"
|
||||
BACKUP_DIR="$HOME/Backups/paperless-ngx"
|
||||
|
||||
# 1 = macOS-Mitteilung bei Erfolg/Fehler, 0 = keine Mitteilung.
|
||||
NOTIFY=1
|
||||
|
||||
SSH_CONNECT_TIMEOUT=10
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
INSTALL_DIR="$HOME/Library/Application Support/PaperlessBackup"
|
||||
CONFIG_DIR="$HOME/.config/paperless-backup"
|
||||
PLIST="$HOME/Library/LaunchAgents/de.rob.paperless-ngx-backup.plist"
|
||||
LOG_DIR="$HOME/Library/Logs"
|
||||
UID_VALUE=$(id -u)
|
||||
|
||||
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$(dirname "$PLIST")" "$LOG_DIR"
|
||||
install -m 700 "$SOURCE_DIR/backup-paperless-ngx.sh" "$INSTALL_DIR/backup-paperless-ngx.sh"
|
||||
|
||||
if [ ! -f "$CONFIG_DIR/config" ]; then
|
||||
install -m 600 "$SOURCE_DIR/config.example" "$CONFIG_DIR/config"
|
||||
echo "Konfiguration angelegt: $CONFIG_DIR/config"
|
||||
else
|
||||
echo "Vorhandene Konfiguration bleibt unveraendert: $CONFIG_DIR/config"
|
||||
fi
|
||||
|
||||
cat > "$PLIST" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>de.rob.paperless-ngx-backup</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$INSTALL_DIR/backup-paperless-ngx.sh</string>
|
||||
</array>
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>3</integer>
|
||||
<key>Minute</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>ProcessType</key>
|
||||
<string>Background</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>$LOG_DIR/paperless-ngx-launchd.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>$LOG_DIR/paperless-ngx-launchd.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
chmod 600 "$PLIST"
|
||||
plutil -lint "$PLIST"
|
||||
launchctl bootout "gui/$UID_VALUE" "$PLIST" >/dev/null 2>&1 || true
|
||||
launchctl bootstrap "gui/$UID_VALUE" "$PLIST"
|
||||
|
||||
echo "LaunchAgent installiert: taeglich um 03:00 Uhr"
|
||||
echo "Vor dem ersten echten Lauf bitte pruefen:"
|
||||
echo " $INSTALL_DIR/backup-paperless-ngx.sh --check"
|
||||
Reference in new issue
Block a user