migrate all backups scripts into new Repo called "Brücke"

This commit is contained in:
trnq1ll0 committed 2026-11-02 01:21:35 +01:00
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"
-96
View File
@@ -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 "$@"
-12
View File
@@ -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"