#!/usr/bin/env bash # # PDF OCR Hotfolder — Update-Script # # Aktualisiert Code und venv unter /opt/pdf-ocr-hotfolder/ sowie die # systemd Template-Unit. Danach werden alle Instanzen neu gestartet, die # vorher laufen sollten. Config-Dateien unter /etc/pdf-ocr-hotfolder/ # bleiben unveraendert. # # Ueberlebt Debian-Major-Upgrades (12 -> 13 -> 14): stimmt die Python-Version # der venv nicht mehr mit dem System-Python ueberein oder ist der Interpreter # der venv gar nicht mehr da, wird die venv neu gebaut (ganz oder gar nicht). # # Aufruf: # sudo ./update.sh # normales Update # sudo ./update.sh --rebuild-venv # venv-Neubau erzwingen (nach dist-upgrade) # set -Eeuo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*"; } log_step() { echo -e "\n${BLUE}==>${NC} $*"; } # Pfade sind ueberschreibbar, damit die Funktionen dieses Skripts isoliert # gegen eine Fake-Umgebung getestet werden koennen (siehe LIB_ONLY unten). : "${INSTALL_DIR:=/opt/pdf-ocr-hotfolder}" : "${CONFIG_DIR:=/etc/pdf-ocr-hotfolder}" : "${SYSTEMD_DIR:=/etc/systemd/system}" : "${BACKUP_DIR:=/var/backups/pdf-ocr-hotfolder}" : "${TAR_ROOT:=/}" : "${VERIFY_WAIT:=6}" : "${BACKUP_KEEP:=5}" SERVICE_TEMPLATE="pdf-ocr-hotfolder@.service" UNIT_GLOB='pdf-ocr-hotfolder@*.service' LXC_DROPIN_DIR="$SYSTEMD_DIR/${SERVICE_TEMPLATE}.d" LXC_DROPIN="$LXC_DROPIN_DIR/lxc-compat.conf" REBUILD_VENV=0 # per --rebuild-venv erzwungen VENV_REBUILT=0 # wurde tatsaechlich neu gebaut? APT_WARN=0 # apt-Sync hat gemeckert LXC_SYNCED=0 BACKUP_FILE="" PRIMARY_USER="pdfocr" TOUCHED=0 # 1, sobald auf der Platte etwas getauscht wurde UNITS_ALL=() # alle bekannten Instanz-Units PREV_OK=() # liefen vorher sauber -> muessen nachher laufen PREV_BROKEN=() # waren vorher kaputt -> sollen laufen, gelten aber nicht als Erfolg PREV_STOPPED=() # waren bewusst gestoppt -> bleiben gestoppt CFG_WARN=() # Instanzen mit Config-Warnungen (Exit 1) CFG_ERR=() # Instanzen mit Config-Fehlern (Exit 2) STARTED_OK=() STARTED_FAIL=() usage() { cat < 13), wenn das System-Python eine neue Version bekommen hat. -h, --help Diese Hilfe. Ohne Option prueft das Skript selbst, ob die venv noch zum System-Python passt, und baut sie bei Bedarf neu. EOF } # ============================================================ # Abbruch-Behandlung # ============================================================ CLEANUP_DONE=0 # Startet bei einem Abbruch die vorher laufenden Instanzen wieder und sagt # laut, was passiert ist. abort_handler() { local rc="${1:-1}" line="${2:-?}" if [ "$CLEANUP_DONE" -ne 0 ]; then exit "$rc" fi CLEANUP_DONE=1 trap - ERR INT TERM echo log_error "═══════════════════════════════════════════════════════════════" log_error "UPDATE ABGEBROCHEN (Exit-Code $rc, Zeile $line)." if [ "$TOUCHED" -eq 1 ]; then log_error "Der Stand auf der Platte kann halb aktualisiert sein." else log_error "Es wurde noch nichts getauscht — der alte Stand ist unveraendert." fi log_error "═══════════════════════════════════════════════════════════════" local -a to_restart=() [ "${#PREV_OK[@]}" -gt 0 ] && to_restart+=("${PREV_OK[@]}") [ "${#PREV_BROKEN[@]}" -gt 0 ] && to_restart+=("${PREV_BROKEN[@]}") if [ "${#to_restart[@]}" -gt 0 ]; then log_warn "Starte die vorher laufenden Instanzen wieder..." local unit for unit in "${to_restart[@]}"; do if systemctl start "$unit" >/dev/null 2>&1; then log_info " wieder gestartet: $unit" else log_error " Start fehlgeschlagen: $unit (journalctl -u $unit -n 50)" fi done else log_warn "Es liefen vorher keine Instanzen — nichts wieder zu starten." fi if [ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ]; then log_warn "Backup vor dem Update: $BACKUP_FILE" log_warn "Rollback: tar -xzf '$BACKUP_FILE' -C / && systemctl daemon-reload" else log_warn "Es wurde noch KEIN Backup geschrieben." fi log_warn "Aeltere Backups: $BACKUP_DIR" exit "$rc" } die() { log_error "$*"; abort_handler 1 "${BASH_LINENO[0]:-?}"; } # ============================================================ # Python / venv # ============================================================ # major.minor des uebergebenen Interpreters; leer, wenn er nicht laeuft. py_mm() { local py="$1" "$py" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || true } # major.minor aus pyvenv.cfg (version = / version_info =); leer, wenn unlesbar. pyvenv_cfg_mm() { local cfg="$1/pyvenv.cfg" [ -f "$cfg" ] || return 0 sed -n 's/^[[:space:]]*version\(_info\)\?[[:space:]]*=[[:space:]]*\([0-9]\+\.[0-9]\+\).*/\2/p' "$cfg" | head -n1 } # Prueft die venv gegen das aktuelle System-Python. # Setzt VENV_ISSUES (Array) und gibt 0 zurueck, wenn alles passt. VENV_ISSUES=() venv_is_healthy() { local venv="$1" local sys_mm venv_mm cfg_mm VENV_ISSUES=() if [ ! -d "$venv" ]; then VENV_ISSUES+=("venv-Verzeichnis fehlt: $venv") return 1 fi if [ ! -x "$venv/bin/python" ]; then VENV_ISSUES+=("$venv/bin/python fehlt oder ist nicht ausfuehrbar") return 1 fi venv_mm="$(py_mm "$venv/bin/python")" if [ -z "$venv_mm" ]; then VENV_ISSUES+=("$venv/bin/python laeuft nicht (toter Symlink nach einem Distributions-Upgrade?)") return 1 fi sys_mm="$(py_mm "$(command -v python3 || echo /usr/bin/python3)")" if [ -z "$sys_mm" ]; then VENV_ISSUES+=("System-python3 laeuft nicht — venv-Pruefung nicht moeglich") return 1 fi if [ "$venv_mm" != "$sys_mm" ]; then VENV_ISSUES+=("venv haengt an Python $venv_mm, das System liefert Python $sys_mm") return 1 fi cfg_mm="$(pyvenv_cfg_mm "$venv")" if [ -n "$cfg_mm" ] && [ "$cfg_mm" != "$venv_mm" ]; then VENV_ISSUES+=("pyvenv.cfg nennt Python $cfg_mm, der Interpreter meldet $venv_mm") return 1 fi return 0 } # Installiert die Requirements und uebersetzt pip-Fehler in eine Ansage, mit # der man etwas anfangen kann (typisch: Pin passt nicht mehr zum Python). pip_install_requirements() { local venv="$1" out rc py_mm_now py_mm_now="$(py_mm "$venv/bin/python")" out="$("$venv/bin/pip" install --upgrade pip 2>&1)" && rc=0 || rc=$? if [ "$rc" -ne 0 ]; then log_warn "pip liess sich nicht aktualisieren (weiter mit der vorhandenen Version):" printf '%s\n' "$out" | tail -n 5 fi out="$("$venv/bin/pip" install --upgrade -r "$INSTALL_DIR/requirements.txt" 2>&1)" && rc=0 || rc=$? if [ "$rc" -eq 0 ]; then return 0 fi echo log_error "Installation der Requirements fehlgeschlagen (pip Exit $rc)." printf '%s\n' "$out" | tail -n 25 echo local bad bad="$(printf '%s\n' "$out" | sed -n \ -e 's/.*[Nn]o matching distribution found for \([^ ]*\).*/\1/p' \ -e 's/.*Could not find a version that satisfies the requirement \([^ ]*\).*/\1/p' \ -e 's/.*Failed building wheel for \([^ ]*\).*/\1/p' \ -e 's/.*error: subprocess-exited-with-error.*building \([^ ]*\).*/\1/p' \ | head -n1)" if [ -n "$bad" ]; then log_error "Gescheitertes Paket: $bad" fi log_error "Sehr wahrscheinliche Ursache: eine in requirements.txt fest gepinnte" log_error "Version gibt es fuer Python ${py_mm_now:-?} nicht (mehr)." log_error "Naechster Schritt: requirements.txt anheben (passende Version fuer" log_error "Python ${py_mm_now:-?} eintragen) und update.sh --rebuild-venv erneut laufen lassen." return 1 } # Baut die venv neu: alte wegsichern, neue bauen, Requirements installieren, # und erst bei Erfolg die alte entfernen. Scheitert etwas, wird die alte # zurueckgerollt und hart abgebrochen. rebuild_venv() { local venv="$INSTALL_DIR/venv" local saved sys_mm saved="$INSTALL_DIR/venv.old-$(date +%Y%m%d-%H%M%S)" sys_mm="$(py_mm "$(command -v python3 || echo /usr/bin/python3)")" log_step "venv wird neu gebaut (System-Python ${sys_mm:-?})" if [ -d "$venv" ]; then mv "$venv" "$saved" || die "Alte venv liess sich nicht nach $saved verschieben." log_info "Alte venv gesichert: $saved" else saved="" log_info "Keine alte venv vorhanden — es wird frisch gebaut." fi if ! python3 -m venv "$venv"; then log_error "python3 -m venv ist fehlgeschlagen." log_error "Fehlt das Paket python3-venv? -> apt-get install -y python3-venv" rm -rf "$venv" if [ -n "$saved" ]; then mv "$saved" "$venv" && log_warn "Alte venv wurde zurueckgerollt: $venv" fi die "venv-Neubau abgebrochen." fi if ! pip_install_requirements "$venv"; then rm -rf "$venv" if [ -n "$saved" ]; then mv "$saved" "$venv" && log_warn "Alte venv wurde zurueckgerollt: $venv" log_warn "Sie haengt weiterhin an einem Python, das es so nicht mehr gibt —" log_warn "die Instanzen laufen damit nicht. Erst requirements.txt korrigieren." fi die "venv-Neubau abgebrochen — keine halb gefuellte venv zurueckgelassen." fi [ -n "$saved" ] && rm -rf "$saved" VENV_REBUILT=1 log_info "venv neu gebaut ✓ (Python ${sys_mm:-?})" } # ============================================================ # System-Pakete # ============================================================ # Holt die Paketliste aus install.sh (einzige Quelle) und installiert sie. # apt-get install ist idempotent; bereits vorhandene Pakete bleiben unberuehrt. # Nachinstallierte Tesseract-Sprachpakete werden NICHT angefasst (kein purge, # kein autoremove). sync_system_packages() { local block pkgs log_step "System-Pakete abgleichen" block="$(sed -n '/^# --- BEGIN apt-packages/,/^# --- END apt-packages/p' "$REPO_DIR/install.sh" 2>/dev/null || true)" if [ -z "$block" ] || ! printf '%s' "$block" | grep -q 'pdf_ocr_apt_packages()'; then log_warn "Paketliste in install.sh nicht gefunden — System-Pakete werden nicht abgeglichen." APT_WARN=1 return 0 fi eval "$block" if ! declare -F pdf_ocr_apt_packages >/dev/null; then log_warn "pdf_ocr_apt_packages() liess sich nicht laden — uebersprungen." APT_WARN=1 return 0 fi mapfile -t pkgs < <(pdf_ocr_apt_packages) if [ "${#pkgs[@]}" -eq 0 ]; then log_warn "Paketliste ist leer — uebersprungen." APT_WARN=1 return 0 fi log_info "Pakete: ${pkgs[*]}" if ! apt-get update -qq; then log_warn "'apt-get update' fehlgeschlagen (kein Netz / kein Mirror?)." APT_WARN=1 fi if apt-get install -y --no-install-recommends "${pkgs[@]}"; then log_info "System-Pakete ok ✓ (vorhandene Sprachpakete bleiben unangetastet)" else log_warn "Mindestens ein System-Paket liess sich nicht installieren." log_warn "Falls eine neue Version ein neues Paket braucht, laufen die Instanzen evtl. nicht." APT_WARN=1 fi } # ============================================================ # Instanzen erfassen # ============================================================ unit_state() { systemctl is-active "$1" 2>/dev/null || true; } unit_failed() { systemctl is-failed "$1" 2>/dev/null || true; } unit_restarts() { local n n="$(systemctl show -p NRestarts --value "$1" 2>/dev/null || true)" case "$n" in (''|*[!0-9]*) echo 0 ;; (*) echo "$n" ;; esac } # Sammelt alle Instanz-Units: geladene (aktiv, activating, failed), per # list-unit-files enabled, und alle, fuer die eine Config existiert. collect_instances() { local -A seen=() local unit name line state failed cfg while read -r line; do unit="$(printf '%s' "$line" | awk '{print $1}')" unit="${unit#●}" [ -n "$unit" ] || continue case "$unit" in pdf-ocr-hotfolder@.service) continue ;; pdf-ocr-hotfolder@?*.service) seen["$unit"]=1 ;; esac done < <(systemctl list-units --all --no-legend --plain "$UNIT_GLOB" 2>/dev/null || true) while read -r line; do unit="$(printf '%s' "$line" | awk '{print $1}')" [ -n "$unit" ] || continue case "$unit" in pdf-ocr-hotfolder@.service) continue ;; pdf-ocr-hotfolder@?*.service) seen["$unit"]=1 ;; esac done < <(systemctl list-unit-files --no-legend --plain "$UNIT_GLOB" 2>/dev/null || true) shopt -s nullglob for cfg in "$CONFIG_DIR"/*.toml; do name="$(basename "$cfg" .toml)" [ -n "$name" ] || continue seen["pdf-ocr-hotfolder@${name}.service"]=1 done shopt -u nullglob UNITS_ALL=(); PREV_OK=(); PREV_BROKEN=(); PREV_STOPPED=() [ "${#seen[@]}" -gt 0 ] || return 0 mapfile -t UNITS_ALL < <(printf '%s\n' "${!seen[@]}" | sort) for unit in "${UNITS_ALL[@]}"; do state="$(unit_state "$unit")" failed="$(unit_failed "$unit")" case "$state" in active) if [ "$failed" = "failed" ]; then PREV_BROKEN+=("$unit") else PREV_OK+=("$unit") fi ;; activating|reloading|deactivating|failed) PREV_BROKEN+=("$unit") ;; *) if [ "$failed" = "failed" ]; then PREV_BROKEN+=("$unit") else PREV_STOPPED+=("$unit") fi ;; esac done } report_instances() { if [ "${#PREV_OK[@]}" -gt 0 ]; then log_info "Lief vorher sauber (${#PREV_OK[@]}): ${PREV_OK[*]}" else log_info "Keine sauber laufende Instanz gefunden." fi if [ "${#PREV_BROKEN[@]}" -gt 0 ]; then log_warn "War vorher KAPUTT (${#PREV_BROKEN[@]}): ${PREV_BROKEN[*]}" log_warn " (failed oder Crash-Loop/activating — wird mit gestartet, gilt aber" log_warn " erst als Erfolg, wenn sie nach dem Update wirklich laeuft.)" fi if [ "${#PREV_STOPPED[@]}" -gt 0 ]; then log_info "Bewusst gestoppt, bleibt gestoppt (${#PREV_STOPPED[@]}): ${PREV_STOPPED[*]}" fi } stop_instances() { local -a units=() [ "${#PREV_OK[@]}" -gt 0 ] && units+=("${PREV_OK[@]}") [ "${#PREV_BROKEN[@]}" -gt 0 ] && units+=("${PREV_BROKEN[@]}") [ "${#units[@]}" -gt 0 ] || { log_info "Nichts zu stoppen."; return 0; } log_info "Stoppe Instanzen: ${units[*]}" local unit for unit in "${units[@]}"; do systemctl stop "$unit" >/dev/null 2>&1 || log_warn " stop fehlgeschlagen: $unit" done } # ============================================================ # Verifikation nach dem Start # ============================================================ # Wartet mindestens VERIFY_WAIT Sekunden und prueft danach is-active, # is-failed und den Restart-Zaehler. Ein Crash-Loop (Type=simple meldet # sofort "active") faellt damit auf. verify_unit() { local unit="$1" local r0 r1 state failed waited=0 r0="$(unit_restarts "$unit")" while [ "$waited" -lt "$VERIFY_WAIT" ]; do sleep 1 waited=$((waited + 1)) [ "$(unit_state "$unit")" = "failed" ] && break done state="$(unit_state "$unit")" failed="$(unit_failed "$unit")" r1="$(unit_restarts "$unit")" if [ "$state" != "active" ]; then log_error " ❌ $unit — Status '$state' nach ${waited}s" return 1 fi if [ "$failed" = "failed" ]; then log_error " ❌ $unit — systemd meldet 'failed'" return 1 fi if [ "$r1" -gt "$r0" ]; then log_error " ❌ $unit — Crash-Loop (NRestarts $r0 -> $r1 in ${waited}s)" return 1 fi return 0 } start_instances() { local -a units=() [ "${#PREV_OK[@]}" -gt 0 ] && units+=("${PREV_OK[@]}") [ "${#PREV_BROKEN[@]}" -gt 0 ] && units+=("${PREV_BROKEN[@]}") STARTED_OK=(); STARTED_FAIL=() [ "${#units[@]}" -gt 0 ] || { log_info "Keine Instanz zu starten."; return 0; } local unit for unit in "${units[@]}"; do # Alten failed-Zustand raeumen, damit is-failed/NRestarts danach # wirklich etwas ueber diesen Start aussagen. systemctl reset-failed "$unit" >/dev/null 2>&1 || true systemctl start "$unit" >/dev/null 2>&1 || log_warn " start meldete einen Fehler: $unit" if verify_unit "$unit"; then log_info " ✅ $unit laeuft (nach ${VERIFY_WAIT}s stabil)" STARTED_OK+=("$unit") else log_error " Logs: journalctl -u $unit -n 50 --no-pager" STARTED_FAIL+=("$unit") fi done } # ============================================================ # Config-Pruefung (CLI des Python-Teils) # ============================================================ # Nutzt: venv/bin/python -m pdf_ocr_hotfolder --check-config --config # Exit 0 = sauber, 1 = Warnungen, 2 = Fehler. # Kennt der installierte Code das Subkommando nicht, wird die Pruefung # uebersprungen (Warnung), das Update laeuft aber weiter. check_all_configs() { local py="$INSTALL_DIR/venv/bin/python" local cfg name out rc local -a cfgs=() log_step "Configs pruefen (--check-config)" CFG_WARN=(); CFG_ERR=() if [ ! -x "$py" ]; then log_warn "venv-Python nicht ausfuehrbar — Config-Pruefung uebersprungen." return 0 fi shopt -s nullglob cfgs=("$CONFIG_DIR"/*.toml) shopt -u nullglob if [ "${#cfgs[@]}" -eq 0 ]; then log_info "Keine Instanz-Configs unter $CONFIG_DIR." return 0 fi for cfg in "${cfgs[@]}"; do name="$(basename "$cfg" .toml)" out="$(cd "$INSTALL_DIR" && "$py" -m pdf_ocr_hotfolder --check-config --config "$cfg" 2>&1)" && rc=0 || rc=$? case "$rc" in 0) log_info " ✅ $name — Config ok" [ -n "$out" ] && printf '%s\n' "$out" ;; 1) log_warn " ⚠ $name — Warnungen:" printf '%s\n' "$out" CFG_WARN+=("$name") ;; 2) # argparse beendet sich bei unbekannten Optionen ebenfalls mit 2 — # das ist kein Config-Fehler, sondern aelterer Code. if printf '%s' "$out" | grep -qiE 'unrecognized arguments|invalid choice|no such option|unknown option|unrecognized option'; then log_warn "Der installierte Code kennt --check-config nicht (aeltere Version)." log_warn "Config-Pruefung wird uebersprungen — Update laeuft weiter." CFG_WARN=(); CFG_ERR=() return 0 fi log_error " ❌ $name — Config-FEHLER:" printf '%s\n' "$out" CFG_ERR+=("$name") ;; *) log_warn " ⚠ $name — --check-config lieferte unerwarteten Exit-Code $rc; ignoriert." [ -n "$out" ] && printf '%s\n' "$out" | tail -n 10 ;; esac done } # ============================================================ # Backup # ============================================================ # Pfad relativ zu TAR_ROOT (fuer tar -C "$TAR_ROOT"). strip_root() { local p="$1" root="${TAR_ROOT%/}" [ -n "$root" ] && p="${p#"$root"}" printf '%s' "${p#/}" } # Sichert Code, Instanz-Configs, Template-Unit, Drop-ins und ein pip freeze # der alten venv. Datenverzeichnisse (/var/lib/...) bleiben bewusst draussen. # Das Archiv enthaelt Klartext-Passwoerter aus den Instanz-Configs und wird # deshalb root-only (0600) abgelegt. create_backup() { local stage tmp d local -a items=() log_step "Backup erstellen" mkdir -p "$BACKUP_DIR" || die "Backup-Verzeichnis $BACKUP_DIR nicht anlegbar." chmod 700 "$BACKUP_DIR" 2>/dev/null || true stage="$(mktemp -d)" || die "mktemp -d fehlgeschlagen." { echo "# pip freeze der alten venv vor dem Update" echo "# Zeitpunkt: $(date -Is)" echo "# Version: ${OLD_VERSION:-unknown} -> ${NEW_VERSION:-unknown}" if [ -x "$INSTALL_DIR/venv/bin/pip" ]; then "$INSTALL_DIR/venv/bin/pip" freeze 2>/dev/null || echo "# pip freeze fehlgeschlagen (venv defekt?)" else echo "# keine funktionierende venv vorhanden" fi } > "$stage/pip-freeze.txt" [ -d "$INSTALL_DIR" ] && items+=("$(strip_root "$INSTALL_DIR")") [ -d "$CONFIG_DIR" ] && items+=("$(strip_root "$CONFIG_DIR")") [ -f "$SYSTEMD_DIR/$SERVICE_TEMPLATE" ] && items+=("$(strip_root "$SYSTEMD_DIR/$SERVICE_TEMPLATE")") shopt -s nullglob for d in "$SYSTEMD_DIR"/pdf-ocr-hotfolder@*.service.d; do items+=("$(strip_root "$d")") done shopt -u nullglob if [ "${#items[@]}" -eq 0 ]; then rm -rf "$stage" die "Nichts zu sichern gefunden — das sieht nach einer kaputten Installation aus." fi tmp="$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz" local old_umask; old_umask="$(umask)" umask 077 if ! tar -czf "$tmp" \ --exclude="$(strip_root "$INSTALL_DIR")/venv" \ --exclude="$(strip_root "$INSTALL_DIR")/venv.old-*" \ --exclude='*/__pycache__' \ --exclude='*.pyc' \ -C "$TAR_ROOT" "${items[@]}" \ -C "$stage" pip-freeze.txt; then umask "$old_umask" rm -f "$tmp" rm -rf "$stage" die "Backup fehlgeschlagen (volle Platte?). Es wurde NICHTS veraendert." fi umask "$old_umask" rm -rf "$stage" chmod 600 "$tmp" || true chown root:root "$tmp" 2>/dev/null || true BACKUP_FILE="$tmp" log_info "Backup: $tmp ($(du -h "$tmp" 2>/dev/null | awk '{print $1}'))" log_info " Enthalten: Code, $CONFIG_DIR, Template-Unit, Drop-ins, pip-freeze.txt" log_info " NICHT enthalten: venv und die Datenverzeichnisse (/var/lib/pdf-ocr-hotfolder)" log_warn " Achtung: die Instanz-Configs enthalten Klartext-Passwoerter" log_warn " (SMTP/Nextcloud/SFTP) — Archiv ist deshalb root-only (0600)." rotate_backups } # Behaelt die letzten $BACKUP_KEEP Archive, loescht aeltere. rotate_backups() { local -a all=() old=() shopt -s nullglob mapfile -t all < <(printf '%s\n' "$BACKUP_DIR"/backup-*.tar.gz | sort) shopt -u nullglob [ "${#all[@]}" -gt "$BACKUP_KEEP" ] || return 0 old=("${all[@]:0:$(( ${#all[@]} - BACKUP_KEEP ))}") local f for f in "${old[@]}"; do rm -f "$f" && log_info " Altes Backup entfernt: $(basename "$f")" done log_info " Rotation: ${BACKUP_KEEP} Backups behalten, ${#old[@]} entfernt." } # ============================================================ # systemd # ============================================================ install_units() { log_step "systemd-Units aktualisieren" cp "$REPO_DIR/systemd/$SERVICE_TEMPLATE" "$SYSTEMD_DIR/$SERVICE_TEMPLATE" log_info "Template-Unit aktualisiert ✓" # Issue #4 redux: existiert das LXC-Drop-in, muss es mit der Template-Unit # mitwachsen — sonst reisst ein neu ergaenzter Hardening-Schalter alle # Container-Instanzen (Error 226/NAMESPACE). if [ -f "$LXC_DROPIN" ]; then if [ -f "$REPO_DIR/systemd/lxc-compat.conf" ]; then cp "$REPO_DIR/systemd/lxc-compat.conf" "$LXC_DROPIN" LXC_SYNCED=1 log_info "LXC-Drop-in nachgezogen: $LXC_DROPIN ✓" else log_warn "systemd/lxc-compat.conf fehlt im Repo — Drop-in bleibt alt." fi elif systemd-detect-virt --container -q 2>/dev/null; then log_warn "Container-Umgebung erkannt, aber kein LXC-Drop-in installiert." log_warn "Bei Error 226/NAMESPACE: install.sh erneut laufen lassen oder" log_warn " cp '$REPO_DIR/systemd/lxc-compat.conf' '$LXC_DROPIN'" fi systemctl daemon-reload } # ============================================================ # LIB_ONLY: bis hierher nur Definitionen. Mit # PDF_OCR_UPDATE_LIB_ONLY=1 source ./update.sh # laesst sich alles oben isoliert testen, ohne dass etwas passiert. # ============================================================ if [ "${PDF_OCR_UPDATE_LIB_ONLY:-0}" = "1" ]; then # shellcheck disable=SC2317 # 'exit' greift nur, wenn das Skript nicht gesourct wurde return 0 2>/dev/null || exit 0 fi # ============================================================ # Main # ============================================================ while [ "$#" -gt 0 ]; do case "$1" in --rebuild-venv) REBUILD_VENV=1 ;; -h|--help) usage; exit 0 ;; *) log_error "Unbekannte Option: $1"; echo; usage; exit 1 ;; esac shift done if [ "${EUID}" -ne 0 ]; then log_error "Bitte als root ausfuehren: sudo ./update.sh" exit 1 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "$SCRIPT_DIR/pdf_ocr_hotfolder/__init__.py" ]; then REPO_DIR="$SCRIPT_DIR" elif [ -f "$INSTALL_DIR/.repo_path" ]; then REPO_DIR="$(cat "$INSTALL_DIR/.repo_path")" [ -d "$REPO_DIR" ] || { log_error "Gespeicherter Repo-Pfad existiert nicht: $REPO_DIR"; exit 1; } else log_error "Repo nicht gefunden. update.sh aus dem Repo ausfuehren." exit 1 fi [ -d "$INSTALL_DIR" ] || { log_error "Installation nicht gefunden. Erst install.sh ausfuehren."; exit 1; } OLD_VERSION="$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo unknown)" NEW_VERSION="$(cat "$REPO_DIR/VERSION" 2>/dev/null || echo unknown)" echo echo "==========================================" echo " PDF OCR Hotfolder — Update" echo "==========================================" log_info "Repo: $REPO_DIR" log_info "Install: $INSTALL_DIR" log_info "Version: $OLD_VERSION → $NEW_VERSION" echo # Ab hier kann ein Abbruch Instanzen gestoppt zuruecklassen. trap 'abort_handler $? $LINENO' ERR trap 'abort_handler 130 $LINENO' INT trap 'abort_handler 143 $LINENO' TERM # --- Instanzen erfassen --- log_step "Instanzen erfassen" collect_instances report_instances # --- Eigentuemer des Codes merken (vor dem venv-Neubau) --- if [ -d "$INSTALL_DIR/venv" ]; then PRIMARY_USER="$(stat -c '%U' "$INSTALL_DIR/venv" 2>/dev/null || echo pdfocr)" else PRIMARY_USER="$(stat -c '%U' "$INSTALL_DIR" 2>/dev/null || echo pdfocr)" fi [ "$PRIMARY_USER" = "root" ] && PRIMARY_USER="pdfocr" # --- System-Pakete (auch beim Update, nicht nur bei install.sh) --- sync_system_packages # --- venv-Gesundheit pruefen (vor dem Stoppen, damit man es frueh sieht) --- log_step "venv pruefen" NEED_REBUILD=0 if [ "$REBUILD_VENV" -eq 1 ]; then log_info "--rebuild-venv gesetzt: venv wird in jedem Fall neu gebaut." NEED_REBUILD=1 elif venv_is_healthy "$INSTALL_DIR/venv"; then log_info "venv ok ✓ (Python $(py_mm "$INSTALL_DIR/venv/bin/python"))" else log_warn "venv passt nicht mehr:" for issue in "${VENV_ISSUES[@]}"; do log_warn " - $issue" done log_warn "Typische Ursache: Debian-Major-Upgrade (z.B. 12 -> 13). Die venv haengt" log_warn "am alten Interpreter, systemd quittiert das mit 203/EXEC." log_warn "-> venv wird neu gebaut." NEED_REBUILD=1 fi # --- Ab hier wird angefasst --- log_step "Instanzen stoppen" stop_instances create_backup log_step "Code aktualisieren" TOUCHED=1 rm -rf "$INSTALL_DIR/pdf_ocr_hotfolder" cp -r "$REPO_DIR/pdf_ocr_hotfolder" "$INSTALL_DIR/" cp "$REPO_DIR/requirements.txt" "$INSTALL_DIR/" cp "$REPO_DIR/VERSION" "$INSTALL_DIR/" cp "$REPO_DIR/config.example.toml" "$INSTALL_DIR/" echo "$REPO_DIR" > "$INSTALL_DIR/.repo_path" log_info "Code auf $NEW_VERSION ✓" log_step "Dependencies aktualisieren" if [ "$NEED_REBUILD" -eq 1 ]; then rebuild_venv else pip_install_requirements "$INSTALL_DIR/venv" || die "Dependencies liessen sich nicht aktualisieren." log_info "Dependencies ok ✓" fi install_units log_step "Berechtigungen setzen" # Eigentuemer des Codes bleibt der primaere User (pdfocr); Instanzen laufen # ggf. als anderer User, lesen aber nur den Code. chown -R "$PRIMARY_USER":"$PRIMARY_USER" "$INSTALL_DIR" log_info "Eigentuemer: $PRIMARY_USER" check_all_configs log_step "Instanzen starten" start_instances # ============================================================ # Zusammenfassung: Ist gegen Soll # ============================================================ trap - ERR INT TERM RC=0 echo echo "==========================================" echo " Zusammenfassung" echo "==========================================" log_info "Version: $OLD_VERSION → $NEW_VERSION" log_info "Backup: ${BACKUP_FILE:-keines}" [ "$VENV_REBUILT" -eq 1 ] && log_info "venv: neu gebaut (Python $(py_mm "$INSTALL_DIR/venv/bin/python"))" [ "$LXC_SYNCED" -eq 1 ] && log_info "LXC: Drop-in nachgezogen" SOLL=$(( ${#PREV_OK[@]} + ${#PREV_BROKEN[@]} )) log_info "Soll: $SOLL Instanz(en) sollen laufen | Ist: ${#STARTED_OK[@]} laufen" if [ "${#PREV_STOPPED[@]}" -gt 0 ]; then log_info "Gestoppt gelassen: ${PREV_STOPPED[*]}" fi # Vorher kaputte Instanzen, die auch jetzt nicht laufen: klar benennen. FIXED=(); STILL_BROKEN=() for unit in "${PREV_BROKEN[@]:-}"; do [ -n "$unit" ] || continue if printf '%s\n' "${STARTED_OK[@]:-}" | grep -qxF "$unit"; then FIXED+=("$unit") else STILL_BROKEN+=("$unit") fi done [ "${#FIXED[@]}" -gt 0 ] && log_info "Vorher kaputt, laeuft jetzt: ${FIXED[*]}" if [ "${#STILL_BROKEN[@]}" -gt 0 ]; then log_error "Vorher kaputt und immer noch kaputt: ${STILL_BROKEN[*]}" RC=1 fi VORHER_OK_JETZT_NICHT=() for unit in "${PREV_OK[@]:-}"; do [ -n "$unit" ] || continue printf '%s\n' "${STARTED_OK[@]:-}" | grep -qxF "$unit" || VORHER_OK_JETZT_NICHT+=("$unit") done if [ "${#VORHER_OK_JETZT_NICHT[@]}" -gt 0 ]; then log_error "REGRESSION — lief vorher, laeuft jetzt nicht: ${VORHER_OK_JETZT_NICHT[*]}" log_error "Rollback: tar -xzf '${BACKUP_FILE:-}' -C / && systemctl daemon-reload" RC=1 fi if [ "${#CFG_ERR[@]}" -gt 0 ]; then log_error "Config-FEHLER (Exit 2) bei: ${CFG_ERR[*]}" log_error " Diese Instanzen gelten NICHT als erfolgreich aktualisiert." RC=1 fi if [ "${#CFG_WARN[@]}" -gt 0 ]; then log_warn "Config-Warnungen (Exit 1) bei: ${CFG_WARN[*]}" log_warn " Kein Abbruchgrund, aber bitte nachsehen:" for name in "${CFG_WARN[@]}"; do log_warn " $INSTALL_DIR/venv/bin/python -m pdf_ocr_hotfolder --check-config --config $CONFIG_DIR/$name.toml" done fi if [ "$APT_WARN" -eq 1 ]; then log_warn "System-Pakete konnten nicht vollstaendig abgeglichen werden (siehe oben)." fi echo if [ "$RC" -eq 0 ]; then log_info "Update auf $NEW_VERSION abgeschlossen ✓" else log_error "Update abgeschlossen, aber mit Problemen (siehe oben)." fi exit "$RC"