Files
lokale-scripte/macos/backup-paperless-ngx/backup-paperless-ngx.sh
T

291 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "$@"