add: backup Paperless NGX to MacOS

This commit is contained in:
trnq1ll0 committed 2026-08-21 06:51:31 +02:00
1 parent b9a32c92d3
commit f36e243da7
4 files changed
+455

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
# 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.
+290
View File
@@ -0,0 +1,290 @@
#!/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
@@ -0,0 +1,12 @@
# 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
+57
View File
@@ -0,0 +1,57 @@
#!/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"