3e24aa2ecd
Datenverlust behoben: - Nach hartem Stopp blieb das Original in working/ liegen und wurde nie wieder angefasst (_scan_existing sah nur incoming/). Es wird jetzt beim Start an Ort und Stelle wieder aufgegriffen, mit Kollisionsschutz gegen gleichnamige neue Scans; angefangene __ocr_-Fragmente werden geloescht. - TimeoutStopSec 30 -> 300, damit laufendes OCR zu Ende laufen darf. Config-Drift sichtbar gemacht: - Neues --check-config (Exit 0 sauber / 1 Warnungen / 2 Fehler), das update.sh vor dem Neustart ueber alle Instanz-Configs laufen laesst. - Warnungen fuer [ocr].timeout >= 900 (seit 0.4.0 pro SEITE) und gesetztes pdfa_level, beim Dienststart wie im Check. - Unbekannte Config-Keys werden nicht mehr still verworfen, sondern genannt. Updater feldtauglich: - venv-Health-Check erkennt toten Symlink UND Versions-Drift gegen das System-Python; --rebuild-venv als ausdruecklicher Weg nach einem Debian- Major-Upgrade. Neubau ist ganz-oder-gar-nicht mit Rollback. - apt-Pakete werden auch beim Update synchronisiert (Quelle: install.sh). - Instanz-Erfassung inkl. activating/failed, Verifikation prueft is-failed und NRestarts statt sleep 1 + is-active. - Backup enthaelt Configs, Unit, Drop-ins und pip-freeze.txt, liegt auf 0600 und rotiert auf 5; schlaegt es fehl, bricht das Update vorher ab. - ERR-Trap faehrt die vorher laufenden Instanzen wieder hoch. - lxc-compat.conf wird beim Update nachgezogen. - requirements.txt gepinnt (ocrmypdf 16.13.0, geprueft fuer Python 3.11+3.13). Doku in Installation / Update / OS-Upgrade aufgeteilt (docs/). 135 Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
861 lines
31 KiB
Bash
Executable File
861 lines
31 KiB
Bash
Executable File
#!/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 <<EOF
|
|
PDF OCR Hotfolder — Update
|
|
|
|
sudo ./update.sh [OPTIONEN]
|
|
|
|
Optionen:
|
|
--rebuild-venv Python-venv zwingend neu bauen und Requirements frisch
|
|
installieren. Das ist der Aufruf nach einem Debian-
|
|
Major-Upgrade (apt full-upgrade, z.B. 12 -> 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 <datei>
|
|
# 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:-<backup>}' -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"
|