Files
pdf-ocr-hotfolder/install.sh
T
techadmin 3e24aa2ecd feat: Wiederaufnahme aus working/, --check-config, feldtauglicher Updater (v0.6.0)
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>
2026-09-22 22:04:04 +02:00

514 lines
20 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.
#!/usr/bin/env bash
#
# PDF OCR Hotfolder — Installer / Instanz-Manager für Debian 12/13
#
# Basis-Installation erfolgt einmalig (Code, venv, systemd-Template-Unit).
# Danach werden Hotfolder-Instanzen verwaltet:
# - Beim Erstlauf: mindestens eine Instanz wird angelegt
# - Beim Folgelauf: bestehende Instanzen werden erkannt; neue können ergänzt werden
#
# Unterstützt lokale System-User und AD-User mit lokaler UID (SSSD/Winbind).
#
set -euo 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} $*"; }
if [ "${EUID}" -ne 0 ]; then
log_error "Bitte als root ausführen: sudo ./install.sh"
exit 1
fi
INSTALL_DIR="/opt/pdf-ocr-hotfolder"
CONFIG_DIR="/etc/pdf-ocr-hotfolder"
DATA_ROOT="/var/lib/pdf-ocr-hotfolder"
SERVICE_TEMPLATE="pdf-ocr-hotfolder@.service"
DEFAULT_USER="pdfocr"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$SCRIPT_DIR"
if [ ! -f "$REPO_DIR/pdf_ocr_hotfolder/__init__.py" ]; then
log_error "Repo-Layout nicht erkannt. install.sh aus dem Repo ausführen."
exit 1
fi
# ============================================================
# System-Pakete — einzige Quelle der Wahrheit
# ============================================================
# Der folgende Block wird von update.sh aus dieser Datei herausgeschnitten
# (sed auf die BEGIN/END-Marken) und dort ausgewertet, damit ein Update
# neue Pakete nachzieht. Die Marken und der Funktionsname duerfen sich
# deshalb nicht aendern, ohne update.sh anzupassen.
# --- BEGIN apt-packages (wird von update.sh extrahiert) ---
pdf_ocr_apt_packages() {
cat <<'PKGLIST'
python3
python3-venv
python3-pip
tesseract-ocr
tesseract-ocr-deu
tesseract-ocr-eng
ghostscript
qpdf
unpaper
pngquant
icc-profiles-free
ca-certificates
curl
PKGLIST
}
# --- END apt-packages ---
# Prueft, ob die venv noch zum aktuellen System-Python passt.
# Zwei Faelle: (1) der Interpreter der venv laeuft gar nicht mehr (toter
# Symlink nach einem Distributions-Upgrade, systemd: 203/EXEC), (2) er laeuft
# noch, ist aber eine andere Version als das System-Python (Debian 12 -> 13).
# Beides heisst: neu bauen. Keine Versionsnummer ist hier hartcodiert.
venv_is_healthy() {
local venv="$1" venv_mm sys_mm
[ -x "$venv/bin/python" ] || return 1
venv_mm="$("$venv/bin/python" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || true)"
[ -n "$venv_mm" ] || return 1
sys_mm="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || true)"
[ -n "$sys_mm" ] || return 1
[ "$venv_mm" = "$sys_mm" ]
}
# ============================================================
# Basis-Installation (idempotent)
# ============================================================
install_base() {
log_step "System-Pakete installieren"
local -a PKGS
mapfile -t PKGS < <(pdf_ocr_apt_packages)
apt-get update -qq
apt-get install -y --no-install-recommends "${PKGS[@]}"
log_info "System-Pakete ok ✓"
# Ghostscript-Versions-Check (Issue #3 + Issue #6)
if command -v gs >/dev/null 2>&1; then
GS_VER="$(gs --version 2>/dev/null || echo 0.0)"
log_info "Ghostscript: $GS_VER"
case "$GS_VER" in
10.0.0|10.00.0|10.01.*|10.02.0)
echo
log_warn "═══════════════════════════════════════════════════════════════"
log_warn "Ghostscript $GS_VER ist vom PDF/A-Bug betroffen (10.0.0–10.02.0)."
log_warn "Mit pdfa_level + skip_text=true kann ocrmypdf KEINE PDFs verarbeiten."
log_warn "═══════════════════════════════════════════════════════════════"
echo
# Prüfe ob Debian bookworm (12) — Backports anbieten
if grep -q 'bookworm' /etc/os-release 2>/dev/null; then
read -r -p "Ghostscript via bookworm-backports upgraden? [J/n]: " UPGRADE_GS
UPGRADE_GS="${UPGRADE_GS:-J}"
if [[ "$UPGRADE_GS" =~ ^[JjYy]$ ]]; then
log_info "Aktiviere bookworm-backports..."
if ! grep -q 'bookworm-backports' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null; then
echo 'deb http://deb.debian.org/debian bookworm-backports main' \
> /etc/apt/sources.list.d/bookworm-backports.list
apt-get update -qq
fi
apt-get install -y -t bookworm-backports ghostscript
GS_VER_NEW="$(gs --version 2>/dev/null || echo '?')"
log_info "Ghostscript aktualisiert: $GS_VER → $GS_VER_NEW ✓"
else
log_warn "Workaround: In der Config [ocr].pdfa_level = \"\" setzen (Default ab v0.2.2)"
fi
else
log_warn "Kein Debian bookworm erkannt — manuelles Upgrade nötig."
log_warn "Workaround: In der Config [ocr].pdfa_level = \"\" setzen (Default ab v0.2.2)"
fi
echo
;;
esac
fi
# LXC/Container-Erkennung (Issue #4)
if systemd-detect-virt --container -q 2>/dev/null; then
VIRT_TYPE="$(systemd-detect-virt --container 2>/dev/null || echo 'container')"
log_warn "Container-Umgebung erkannt ($VIRT_TYPE)."
log_warn "systemd-Hardening kann in Containern fehlschlagen (Error 226/NAMESPACE)."
read -r -p "LXC-Kompatibilitäts-Drop-in installieren? [J/n]: " LXC_FIX
LXC_FIX="${LXC_FIX:-J}"
if [[ "$LXC_FIX" =~ ^[JjYy]$ ]]; then
local LXC_DROPIN_DIR="/etc/systemd/system/pdf-ocr-hotfolder@.service.d"
mkdir -p "$LXC_DROPIN_DIR"
cp "$REPO_DIR/systemd/lxc-compat.conf" "$LXC_DROPIN_DIR/lxc-compat.conf"
systemctl daemon-reload
log_info "LXC-Kompatibilitäts-Drop-in installiert ✓"
fi
fi
log_step "Default-User '$DEFAULT_USER' prüfen"
if id "$DEFAULT_USER" &>/dev/null; then
log_info "'$DEFAULT_USER' existiert bereits"
else
adduser --system --group --home "$DATA_ROOT" --shell /usr/sbin/nologin "$DEFAULT_USER"
log_info "System-User '$DEFAULT_USER' angelegt ✓"
fi
log_step "Verzeichnisse anlegen"
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$DATA_ROOT"
chown root:"$DEFAULT_USER" "$CONFIG_DIR"
chmod 750 "$CONFIG_DIR"
log_step "Code kopieren"
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_step "Python venv"
# Eine vorhandene, aber kaputte venv (z.B. nach Debian 12 -> 13) wird
# weggesichert und neu gebaut — ein reines "-d"-Vorhandensein reicht nicht.
if [ -d "$INSTALL_DIR/venv" ] && ! venv_is_healthy "$INSTALL_DIR/venv"; then
local VENV_SAVED
VENV_SAVED="$INSTALL_DIR/venv.old-$(date +%Y%m%d-%H%M%S)"
log_warn "Vorhandene venv passt nicht mehr zum System-Python (Distributions-Upgrade?)."
log_warn "Sie wird gesichert nach: $VENV_SAVED"
mv "$INSTALL_DIR/venv" "$VENV_SAVED"
fi
if [ ! -d "$INSTALL_DIR/venv" ]; then
python3 -m venv "$INSTALL_DIR/venv"
fi
"$INSTALL_DIR/venv/bin/pip" install --upgrade pip -q
if ! "$INSTALL_DIR/venv/bin/pip" install -r "$INSTALL_DIR/requirements.txt" -q; then
log_error "Requirements liessen sich nicht installieren."
log_error "Wahrscheinlich passt eine in requirements.txt gepinnte Version nicht"
log_error "zu Python $(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || echo '?')."
exit 1
fi
log_info "venv ok ✓"
log_step "systemd Template-Unit installieren"
cp "$REPO_DIR/systemd/$SERVICE_TEMPLATE" "/etc/systemd/system/$SERVICE_TEMPLATE"
systemctl daemon-reload
log_info "Template-Unit installiert ✓"
chown -R "$DEFAULT_USER":"$DEFAULT_USER" "$INSTALL_DIR"
}
# ============================================================
# Instanz-Verwaltung
# ============================================================
list_instances() {
find "$CONFIG_DIR" -maxdepth 1 -name '*.toml' -type f 2>/dev/null \
| sed 's|.*/||; s|\.toml$||' \
| sort
}
show_existing_instances() {
local instances
mapfile -t instances < <(list_instances)
if [ "${#instances[@]}" -eq 0 ]; then
log_info "Keine bestehenden Instanzen gefunden."
return
fi
echo
log_info "Bestehende Instanzen:"
for name in "${instances[@]}"; do
local active
active=$(systemctl is-active "pdf-ocr-hotfolder@${name}.service" 2>/dev/null || echo inactive)
printf " • %-30s [%s]\n" "$name" "$active"
done
echo
}
# Liest den Wert eines Keys (erste Zuweisung am Zeilenanfang) aus einer Config
config_value() {
local file="$1" key="$2"
sed -n "s|^${key}[[:space:]]*=[[:space:]]*\"\(.*\)\"[[:space:]]*$|\1|p" "$file" | head -n1
}
# Maskiert Sonderzeichen, damit ein Pfad gefahrlos in eine sed-Ersetzung darf
# (Trennzeichen '|', Rueckverweis '&', Backslash).
sed_escape_repl() {
printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'
}
# Prueft jeden Tesseract-Sprachcode gegen die installierten Sprachdateien und
# bietet fehlende Pakete zur Installation an.
# Rueckgabe: 0 = alle Sprachen verfuegbar (oder Pruefung nicht moeglich),
# 1 = mindestens eine Sprache fehlt weiterhin.
ensure_tesseract_langs() {
local langs="$1"
local raw installed code pkg answer rc=0
local -a codes
if ! command -v tesseract >/dev/null 2>&1; then
log_warn "tesseract ist nicht aufrufbar — Sprachpruefung wird uebersprungen."
log_warn "Eingabe '$langs' wird unveraendert uebernommen."
return 0
fi
if ! raw="$(tesseract --list-langs 2>/dev/null)"; then
log_warn "'tesseract --list-langs' schlug fehl — Sprachpruefung wird uebersprungen."
log_warn "Eingabe '$langs' wird unveraendert uebernommen."
return 0
fi
installed="$(printf '%s\n' "$raw" | grep -vi '^List of available' || true)"
IFS='+' read -r -a codes <<< "$langs"
for code in "${codes[@]}"; do
[ -n "$code" ] || continue
if printf '%s\n' "$installed" | grep -qxF "$code"; then
log_info "Sprache '$code' ist installiert ✓"
continue
fi
pkg="tesseract-ocr-${code//_/-}"
log_warn "Sprache '$code' ist nicht installiert (Paket: $pkg)."
read -r -p "Paket '$pkg' jetzt installieren? [J/n]: " answer
answer="${answer:-J}"
if [[ "$answer" =~ ^[JjYy]$ ]]; then
if ! apt-get install -y --no-install-recommends "$pkg"; then
log_error "Paket '$pkg' liess sich nicht installieren."
elif tesseract --list-langs 2>/dev/null | grep -qxF "$code"; then
log_info "Paket '$pkg' installiert ✓"
continue
else
log_error "Paket '$pkg' ist da, aber tesseract kennt '$code' weiterhin nicht."
fi
fi
log_warn "Ohne die Sprachdatei '$code' scheitert das OCR bei JEDER Datei."
rc=1
done
return $rc
}
create_instance() {
echo
read -r -p "Instanz-Name (nur a-z, 0-9, -): " INST
if [[ ! "$INST" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
log_error "Ungültiger Name. Abbruch."
return 1
fi
if [ -f "$CONFIG_DIR/$INST.toml" ]; then
log_error "Instanz '$INST' existiert bereits. Abbruch."
return 1
fi
local default_base="$DATA_ROOT/$INST"
read -r -p "Basis-Pfad für Daten [$default_base]: " BASE
BASE="${BASE:-$default_base}"
read -r -p "Service-User [$DEFAULT_USER]: " SVC_USER
SVC_USER="${SVC_USER:-$DEFAULT_USER}"
local SVC_GROUP
if id "$SVC_USER" &>/dev/null; then
SVC_GROUP="$(id -gn "$SVC_USER")"
log_info "User '$SVC_USER' existiert (Gruppe: $SVC_GROUP)"
else
log_warn "User '$SVC_USER' existiert nicht."
read -r -p "Lokal als System-User anlegen? [J/n]: " CREATE_USER
CREATE_USER="${CREATE_USER:-J}"
if [[ "$CREATE_USER" =~ ^[JjYy]$ ]]; then
adduser --system --group --home "$BASE" --shell /usr/sbin/nologin "$SVC_USER"
SVC_GROUP="$SVC_USER"
log_info "User '$SVC_USER' angelegt ✓"
else
log_error "User muss existieren (z.B. via AD/SSSD). Abbruch."
return 1
fi
fi
# --- OCR-Sprachen ---
echo
log_info "Tesseract-Sprachen — gelten NUR fuer diese Instanz '$INST'."
log_info "Jede zusaetzliche Sprache kostet Laufzeit und verschlechtert zugleich"
log_info "die Erkennung — also so eng wie moeglich waehlen (z.B. nur 'deu')."
local LANGS
while true; do
read -r -p "Tesseract-Sprachen [deu+eng]: " LANGS
LANGS="${LANGS:-deu+eng}"
if [[ ! "$LANGS" =~ ^[a-z]{3}(_[A-Za-z]+)?(\+[a-z]{3}(_[A-Za-z]+)?)*$ ]]; then
log_error "Ungueltiges Format. Erwartet: Sprachcodes mit '+' verbunden,"
log_error "z.B. 'deu', 'deu+eng' oder 'chi_sim+eng'."
continue
fi
if ensure_tesseract_langs "$LANGS"; then
break
fi
log_warn "Bitte Sprachen erneut angeben (fehlende Sprache einfach weglassen)."
echo
done
# --- Original archivieren? ---
echo
local ORIG_MODE="delete"
local ARCHIVE_DIR=""
local ARCHIVE_ANS
read -r -p "Original nach erfolgreichem OCR archivieren? [j/N]: " ARCHIVE_ANS
ARCHIVE_ANS="${ARCHIVE_ANS:-N}"
if [[ "$ARCHIVE_ANS" =~ ^[JjYy]$ ]]; then
ORIG_MODE="archive"
local default_archive="$BASE/archive"
while true; do
read -r -p "Archiv-Verzeichnis [$default_archive]: " ARCHIVE_DIR
ARCHIVE_DIR="${ARCHIVE_DIR:-$default_archive}"
if [[ "$ARCHIVE_DIR" != /* ]]; then
log_error "Bitte einen absoluten Pfad angeben (beginnt mit '/')."
continue
fi
# Das Archiv darf keines der Arbeitsverzeichnisse sein: im Eingang
# wuerde das Original endlos neu aufgegriffen, in den uebrigen
# kollidiert es mit der Verarbeitung.
case "${ARCHIVE_DIR%/}" in
"$BASE/incoming"|"$BASE/outgoing"|"$BASE/working"|"$BASE/error")
log_error "Das Archiv darf nicht incoming/outgoing/working/error sein."
continue
;;
esac
break
done
else
log_info "Original wird nach erfolgreichem OCR geloescht (original_on_success = \"delete\")."
fi
log_info "Lege Datenverzeichnisse unter $BASE an..."
mkdir -p "$BASE"/{incoming,outgoing,working,error}
if [ -n "$ARCHIVE_DIR" ]; then
mkdir -p "$ARCHIVE_DIR"
fi
chown -R "$SVC_USER":"$SVC_GROUP" "$BASE"
# Innerhalb von $BASE erledigt das chown -R oben schon alles; nur ein Archiv
# ausserhalb braucht eigenes mkdir/chown.
if [ -n "$ARCHIVE_DIR" ] && [[ "$ARCHIVE_DIR" != "$BASE"/* ]] && [ "$ARCHIVE_DIR" != "$BASE" ]; then
chown -R "$SVC_USER":"$SVC_GROUP" "$ARCHIVE_DIR"
log_info "Archiv-Verzeichnis $ARCHIVE_DIR angelegt (liegt ausserhalb von $BASE)"
fi
log_info "Erstelle Config $CONFIG_DIR/$INST.toml..."
# Verankerte Ausdruecke (Zeilenanfang + Key + '='), damit die deutschen
# Kommentarzeilen ueber den Keys unangetastet bleiben.
local ESC_BASE ESC_ARCHIVE ESC_LANGS
ESC_BASE="$(sed_escape_repl "$BASE")"
ESC_ARCHIVE="$(sed_escape_repl "$ARCHIVE_DIR")"
ESC_LANGS="$(sed_escape_repl "$LANGS")"
sed \
-e "s|^incoming[[:space:]]*=.*|incoming = \"$ESC_BASE/incoming\"|" \
-e "s|^outgoing[[:space:]]*=.*|outgoing = \"$ESC_BASE/outgoing\"|" \
-e "s|^working[[:space:]]*=.*|working = \"$ESC_BASE/working\"|" \
-e "s|^error[[:space:]]*=.*|error = \"$ESC_BASE/error\"|" \
-e "s|^languages[[:space:]]*=.*|languages = \"$ESC_LANGS\"|" \
-e "s|^original_on_success[[:space:]]*=.*|original_on_success = \"$ORIG_MODE\"|" \
-e "s|^archive_dir[[:space:]]*=.*|archive_dir = \"$ESC_ARCHIVE\"|" \
"$INSTALL_DIR/config.example.toml" > "$CONFIG_DIR/$INST.toml"
chown root:"$SVC_GROUP" "$CONFIG_DIR/$INST.toml"
chmod 640 "$CONFIG_DIR/$INST.toml"
# Erzeugte Config gegenpruefen: tragen die drei Keys wirklich die Auswahl?
local CFG_OK=1 got key want
for key in languages original_on_success archive_dir; do
case "$key" in
languages) want="$LANGS" ;;
original_on_success) want="$ORIG_MODE" ;;
archive_dir) want="$ARCHIVE_DIR" ;;
esac
got="$(config_value "$CONFIG_DIR/$INST.toml" "$key")"
if [ "$got" != "$want" ]; then
log_error "Config-Pruefung: $key ist \"$got\", erwartet \"$want\""
CFG_OK=0
fi
done
if [ "$CFG_OK" -eq 1 ]; then
log_info "Config-Pruefung ok ✓ (languages / original_on_success / archive_dir)"
else
log_warn "Bitte $CONFIG_DIR/$INST.toml von Hand nachziehen."
fi
# Drop-in für abweichenden Service-User
if [ "$SVC_USER" != "$DEFAULT_USER" ]; then
local DROPIN_DIR="/etc/systemd/system/pdf-ocr-hotfolder@${INST}.service.d"
mkdir -p "$DROPIN_DIR"
cat > "$DROPIN_DIR/user.conf" <<EOF
[Service]
User=$SVC_USER
Group=$SVC_GROUP
EOF
log_info "Drop-in für User '$SVC_USER' erstellt"
fi
systemctl daemon-reload
systemctl enable --now "pdf-ocr-hotfolder@${INST}.service"
sleep 1
if systemctl is-active --quiet "pdf-ocr-hotfolder@${INST}.service"; then
log_info "✅ Instanz '$INST' läuft"
else
log_warn "Instanz '$INST' läuft nicht. Logs: journalctl -u pdf-ocr-hotfolder@${INST}"
fi
echo
echo " Config: $CONFIG_DIR/$INST.toml"
echo " Eingang: $BASE/incoming"
echo " Ausgang: $BASE/outgoing"
echo " User: $SVC_USER ($SVC_GROUP)"
if [ "$CFG_OK" -eq 1 ]; then
echo " Sprachen: $LANGS"
if [ "$ORIG_MODE" = "archive" ]; then
echo " Archiv: $ARCHIVE_DIR"
fi
fi
echo
}
# ============================================================
# Main
# ============================================================
echo
echo "=========================================="
echo " PDF OCR Hotfolder — Installer"
echo "=========================================="
# Auch eine vorhandene, aber kaputte venv loest die Basis-Installation aus
# (sonst wuerde install.sh nach einem Distributions-Upgrade nichts reparieren).
if [ ! -d "$INSTALL_DIR/venv" ] || [ ! -f "/etc/systemd/system/$SERVICE_TEMPLATE" ]; then
log_step "Basis-Installation"
install_base
elif ! venv_is_healthy "$INSTALL_DIR/venv"; then
log_warn "Basis-Installation vorhanden, aber die venv passt nicht mehr zum System-Python."
log_step "Basis-Installation wird zur Reparatur erneut ausgefuehrt"
install_base
else
log_info "Basis-Installation bereits vorhanden ($INSTALL_DIR)"
log_info "Überspringe Basis-Setup (nutze update.sh für Code-Updates)"
fi
show_existing_instances
# Erste Instanz ist Pflicht, wenn noch keine vorhanden
mapfile -t existing < <(list_instances)
if [ "${#existing[@]}" -eq 0 ]; then
log_info "Lege erste Hotfolder-Instanz an."
create_instance || true
fi
while true; do
read -r -p "Weitere Instanz anlegen? [j/N]: " MORE
MORE="${MORE:-N}"
if [[ "$MORE" =~ ^[JjYy]$ ]]; then
create_instance || true
else
break
fi
done
echo
echo "=========================================="
echo " Fertig"
echo "=========================================="
show_existing_instances
echo " Logs: journalctl -u pdf-ocr-hotfolder@<instanz> -f"
echo " Neustart: systemctl restart pdf-ocr-hotfolder@<instanz>"
echo " Update: sudo ./update.sh"
echo