#!/bin/bash # archivdms Updater — baut aus dem lokalen Quellverzeichnis neu (KEIN git pull: # archivdms hat kein Remote-Repository). Rsynct den aktuellen Stand nach # /opt/archivdms, baut Go-Backend + Next.js-Frontend und startet die Dienste # neu. Wird von install.sh für den Erst-Deploy aufgerufen und kann danach bei # jedem neuen lokalen Codestand erneut ausgeführt werden. # # Aufruf (auf dem Zielserver als root, Quellcode liegt bereits lokal vor): # sudo bash update.sh # ARCHIVDMS_SRC=/opt/archivdms-src sudo bash update.sh set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' log() { echo -e "${GREEN}[OK]${NC} $*"; } info() { echo -e "${BLUE}[..]${NC} $*"; } warn() { echo -e "${YELLOW}[!!]${NC} $*"; } die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "Bitte als root ausführen: sudo bash update.sh" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ARCHIVDMS_SRC="${ARCHIVDMS_SRC:-$SCRIPT_DIR}" [[ -f "$ARCHIVDMS_SRC/go.mod" ]] || die "Kein Go-Quellcode unter ARCHIVDMS_SRC gefunden ($ARCHIVDMS_SRC/go.mod fehlt)" INSTALL_DIR="/opt/archivdms" BUILD_DIR="/opt/archivdms/_build" FRONTEND_DIR="/opt/archivdms/web" BIN_DIR="/opt/archivdms/bin" CONFIG_DIR="/etc/archivdms" DMS_USER="archivdms" echo "" echo " ╔══════════════════════════════════════╗" echo " ║ archivdms Updater ║" echo " ╚══════════════════════════════════════╝" echo "" info "Quellverzeichnis: $ARCHIVDMS_SRC" # ── sudo-Rechte für Dienststeuerung (Backfill für Bestandsinstallationen) ─── provision_archivdms_sudoers() { local dms_user="archivdms" command -v sudo >/dev/null 2>&1 || apt-get install -y -qq sudo local services=("archivdms" "manticore" "postgresql" "nginx") local tmpfile tmpfile="$(mktemp)" { echo "# Verwaltet von install.sh/update.sh — nicht manuell editieren," echo "# Änderungen gehen beim nächsten Deploy verloren." for svc in "${services[@]}"; do echo "${dms_user} ALL=(root) NOPASSWD: /usr/bin/systemctl start ${svc}.service" echo "${dms_user} ALL=(root) NOPASSWD: /usr/bin/systemctl stop ${svc}.service" echo "${dms_user} ALL=(root) NOPASSWD: /usr/bin/systemctl restart ${svc}.service" echo "${dms_user} ALL=(root) NOPASSWD: /usr/bin/systemctl enable ${svc}.service" echo "${dms_user} ALL=(root) NOPASSWD: /usr/bin/systemctl disable ${svc}.service" done } > "$tmpfile" if visudo -c -f "$tmpfile" >/dev/null 2>&1; then install -m 0440 -o root -g root "$tmpfile" /etc/sudoers.d/archivdms log "sudo-Rechte für Dienststeuerung aktuell (/etc/sudoers.d/archivdms)" else warn "Generierte sudoers-Regel ist ungültig — /etc/sudoers.d/archivdms NICHT verändert" fi rm -f "$tmpfile" } provision_archivdms_sudoers # ── Go: Version sicherstellen (OS-versions-agnostisch) ────────────────────── GO_REQUIRED="1.26.0" export PATH="$PATH:/usr/local/go/bin" _go_ok=0 if command -v go >/dev/null 2>&1; then _go_installed=$(go version 2>/dev/null | grep -oP 'go\K[0-9]+\.[0-9]+' | head -1) _go_major=${_go_installed%%.*} _go_minor=${_go_installed#*.} if [[ "$_go_major" -gt 1 ]] || [[ "$_go_major" -eq 1 && "$_go_minor" -ge 26 ]]; then _go_ok=1 fi fi if [[ $_go_ok -eq 0 ]]; then info "Go < 1.26 oder nicht gefunden — installiere Go 1.26.5 von upstream..." _go_arch="amd64" [[ "$(uname -m)" == "aarch64" ]] && _go_arch="arm64" curl -fsSL "https://dl.google.com/go/go1.26.5.linux-${_go_arch}.tar.gz" | tar -C /usr/local -xz ln -sf /usr/local/go/bin/go /usr/local/bin/go ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt log "Go $(go version | awk '{print $3}') installiert" else log "Go $(go version | awk '{print $3}') OK" fi # ── Sonstige Voraussetzungen ───────────────────────────────────────────────── command -v node >/dev/null || die "node nicht gefunden — Node.js >=20 installieren (siehe install.sh)" command -v npm >/dev/null || die "npm nicht gefunden" # ── Manticore Search prüfen / installieren ─────────────────────────────────── # Details zum Repo-/Versions-Vorgehen: siehe install.sh bzw. archivmail/update.sh. _MANTICORE_VERSION_FALLBACK="27.1.5-26061911-5a1cf9399" _mc_installed_version="$(dpkg-query -W -f='${Version}' manticore 2>/dev/null || true)" _mc_arch="amd64" [[ "$(uname -m)" == "aarch64" ]] && _mc_arch="arm64" _mc_latest_from_repo() { local base="https://repo.manticoresearch.com/repository/manticoresearch_bookworm" local packages block version filename packages="$(curl -fsSL --max-time 15 "${base}/dists/bookworm/main/binary-${_mc_arch}/Packages" 2>/dev/null)" || return 1 [[ -n "$packages" ]] || return 1 block="$(printf '%s\n' "$packages" | awk 'BEGIN{RS="\n\n"} {n=split($0,l,"\n"); if (l[1]=="Package: manticore") b=$0} END{print b}')" [[ -n "$block" ]] || return 1 version="$(printf '%s\n' "$block" | awk -F': ' '/^Version:/{print $2; exit}')" filename="$(printf '%s\n' "$block" | awk -F': ' '/^Filename:/{print $2; exit}')" [[ -n "$version" && -n "$filename" ]] || return 1 printf '%s|%s\n' "$version" "$filename" } _mc_target_version="$_MANTICORE_VERSION_FALLBACK" _mc_target_url="https://repo.manticoresearch.com/repository/manticoresearch_bookworm/dists/bookworm/main/binary-${_mc_arch}/manticore_${_MANTICORE_VERSION_FALLBACK}_${_mc_arch}.deb" if _mc_latest="$(_mc_latest_from_repo)"; then _mc_target_version="${_mc_latest%%|*}" _mc_target_url="https://repo.manticoresearch.com/repository/manticoresearch_bookworm/${_mc_latest#*|}" info "Manticore Search: aktuelle Repo-Version ${_mc_target_version} ermittelt" else warn "Manticore-Repo-Metadaten nicht abrufbar — falle auf bekannten Stand ${_MANTICORE_VERSION_FALLBACK} zurück" fi _mc_fetch_and_install() { wget -q -O /tmp/manticore.deb "$_mc_target_url" || true if [[ ! -f /tmp/manticore.deb ]]; then warn "Manticore Search konnte nicht heruntergeladen werden — bitte manuell installieren (https://manticoresearch.com/install/)" return 1 fi dpkg -i /tmp/manticore.deb 2>/dev/null || apt-get install -f -y -qq 2>/dev/null || true rm -f /tmp/manticore.deb return 0 } if [[ -z "$_mc_installed_version" ]]; then info "Manticore Search nicht gefunden — installiere Version ${_mc_target_version}..." _mc_fetch_and_install && log "Manticore Search installiert" elif dpkg --compare-versions "$_mc_installed_version" lt "$_mc_target_version"; then info "Manticore Search ${_mc_installed_version} gefunden, aktualisiere auf ${_mc_target_version}..." systemctl stop manticore 2>/dev/null || true if _mc_fetch_and_install; then log "Manticore Search aktualisiert: ${_mc_installed_version} → ${_mc_target_version}" else warn "Upgrade fehlgeschlagen — vorherige Version bleibt installiert, Dienst wird trotzdem neu gestartet" fi else log "Manticore Search ${_mc_installed_version} bereits aktuell (>= ${_mc_target_version})" fi if systemctl list-unit-files manticore.service >/dev/null 2>&1; then systemctl enable manticore 2>/dev/null || true systemctl is-active --quiet manticore || systemctl start manticore 2>/dev/null || warn "Manticore-Dienst konnte nicht gestartet werden" systemctl is-active --quiet manticore && log "Manticore Search läuft" fi # ── OCR-Tools (tesseract + poppler) ────────────────────────────────────────── # Optional: ohne diese Tools läuft der Server weiter, OCR wird übersprungen # (siehe cmd/archivdms/main.go — exec.LookPath-Warnung statt Abbruch). if ! command -v tesseract >/dev/null 2>&1 || ! command -v pdftotext >/dev/null 2>&1; then info "Installiere OCR-Tools (tesseract, poppler-utils)..." apt-get install -y -qq tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng poppler-utils 2>/dev/null \ || warn "OCR-Tools konnten nicht installiert werden — OCR wird deaktiviert" fi if command -v tesseract >/dev/null 2>&1 && command -v pdftotext >/dev/null 2>&1; then log "OCR-Tools verfügbar (tesseract $(tesseract --version 2>&1 | head -1 | awk '{print $2}'))" fi # ── Quellcode ins Build-Verzeichnis rsyncen (KEIN git pull) ───────────────── # archivdms hat kein Remote-Repository — der einzige "Sync"-Mechanismus ist # eine rsync-Kopie des lokal bereitgestellten Quellverzeichnisses. --delete # hält den Build-Baum deckungsgleich mit ARCHIVDMS_SRC (keine Leichen von # vorherigen Ständen); node_modules/.next/_build werden ausgeschlossen, damit # nicht versehentlich große, plattformabhängige Artefakte hin- und herkopiert # werden — die werden hier ohnehin frisch gebaut. info "Synchronisiere Quellcode nach ${BUILD_DIR}..." mkdir -p "$BUILD_DIR" rsync -a --delete \ --exclude '.git' \ --exclude 'node_modules' \ --exclude '.next' \ --exclude '_build' \ --exclude '*.log' \ "$ARCHIVDMS_SRC"/ "$BUILD_DIR"/ log "Quellcode synchronisiert" # ── Go Backend bauen ───────────────────────────────────────────────────────── info "Baue Go Backend..." cd "$BUILD_DIR" go mod tidy && go mod download CGO_ENABLED=0 go build -buildvcs=false -o "$BUILD_DIR/archivdms-new" ./cmd/archivdms/ log "Go Backend gebaut" # ── Next.js Frontend bauen ─────────────────────────────────────────────────── info "Installiere Node-Abhängigkeiten..." # `npm ci` verlangt ein vorhandenes package-lock.json — fehlt es (z.B. weil das # Projekt noch nie mit `npm install` initialisiert wurde), fällt das sonst mit # EUSAGE hart durch. Erstinstallation daher über `npm install` (schreibt das # Lockfile), danach ist `npm ci` bei Folge-Updates der schnellere, reproduzierbare # Normalfall. if [[ -f package-lock.json ]]; then npm ci --prefer-offline 2>/dev/null || npm ci else warn "Kein package-lock.json gefunden — führe npm install statt npm ci aus (Erstinstallation)" npm install fi log "Node-Abhängigkeiten installiert" info "Baue Next.js Frontend..." npm run build log "Frontend gebaut" # ── Dienste stoppen ────────────────────────────────────────────────────────── info "Stoppe Dienste..." systemctl stop archivdms-web 2>/dev/null || warn "archivdms-web nicht aktiv" systemctl stop archivdms 2>/dev/null || warn "archivdms nicht aktiv" # ── Dateien einspielen ─────────────────────────────────────────────────────── info "Spiele Backend ein..." mkdir -p "$BIN_DIR" cp "$BUILD_DIR/archivdms-new" "$BIN_DIR/archivdms" chmod +x "$BIN_DIR/archivdms" chown "$DMS_USER:$DMS_USER" "$BIN_DIR/archivdms" ln -sf "$BIN_DIR/archivdms" /usr/local/bin/archivdms log "Backend eingespielt: $BIN_DIR/archivdms" info "Spiele Cron-Job ein..." if [[ -f "$BUILD_DIR/deploy/cron.d/archivdms-reminders" ]]; then cp "$BUILD_DIR/deploy/cron.d/archivdms-reminders" /etc/cron.d/archivdms-reminders chmod 644 /etc/cron.d/archivdms-reminders log "Cron-Job eingespielt (/etc/cron.d/archivdms-reminders)" else warn "deploy/cron.d/archivdms-reminders nicht im Quellcode gefunden — Cron-Job nicht aktualisiert" fi if [[ -f "$BUILD_DIR/deploy/cron.d/archivdms-classify-retrain" ]]; then cp "$BUILD_DIR/deploy/cron.d/archivdms-classify-retrain" /etc/cron.d/archivdms-classify-retrain chmod 644 /etc/cron.d/archivdms-classify-retrain log "Cron-Job eingespielt (/etc/cron.d/archivdms-classify-retrain)" else warn "deploy/cron.d/archivdms-classify-retrain nicht im Quellcode gefunden — Cron-Job nicht aktualisiert" fi info "Spiele OCR-Hilfsskripte ein..." if [[ -f "$BUILD_DIR/internal/ocr/scripts/hough_deskew.py" ]]; then mkdir -p "$INSTALL_DIR/scripts" cp "$BUILD_DIR/internal/ocr/scripts/hough_deskew.py" "$INSTALL_DIR/scripts/hough_deskew.py" chmod 755 "$INSTALL_DIR/scripts/hough_deskew.py" chown "$DMS_USER:$DMS_USER" "$INSTALL_DIR/scripts/hough_deskew.py" log "Hough-Deskew-Skript eingespielt (${INSTALL_DIR}/scripts/hough_deskew.py)" else warn "internal/ocr/scripts/hough_deskew.py nicht im Quellcode gefunden — Hough-Deskew-Skript nicht aktualisiert" fi info "Spiele Frontend ein (standalone)..." # next.config.ts setzt output: "standalone" — der Next.js-Build spiegelt den # absoluten Build-Pfad; server.js liegt daher unter # .next/standalone//server.js, nicht an einer festen Stelle. STANDALONE_SERVER=$(find "$BUILD_DIR/.next/standalone" -maxdepth 3 -name "server.js" | head -1) [[ -n "$STANDALONE_SERVER" ]] || die "server.js nicht im standalone-Build gefunden" STANDALONE_ROOT=$(dirname "$STANDALONE_SERVER") rm -rf "$FRONTEND_DIR" mkdir -p "$FRONTEND_DIR" rsync -a "$STANDALONE_ROOT/" "$FRONTEND_DIR/" mkdir -p "$FRONTEND_DIR/.next/static" rsync -a --delete "$BUILD_DIR/.next/static/" "$FRONTEND_DIR/.next/static/" if [[ -d "$BUILD_DIR/public" ]]; then rsync -a "$BUILD_DIR/public/" "$FRONTEND_DIR/public/" fi chown -R "$DMS_USER:$DMS_USER" "$FRONTEND_DIR" log "Frontend eingespielt" # ── systemd Units synchronisieren ──────────────────────────────────────────── # install.sh schreibt die Unit-Dateien nur beim Erst-Setup. Ohne diesen # Schritt bleiben bereits installierte Server auf dem alten Stand eingefroren, # selbst wenn sich die Unit-Definition in install.sh später ändert. Bei jedem # Update-Lauf neu schreiben + reload, damit Änderungen automatisch nachgezogen # werden. info "Synchronisiere systemd Units..." STORE_BASE="/var/lib/archivdms" LOG_DIR="/var/log/archivdms" SSL_DIR="/etc/ssl/archivdms" DMS_USER="archivdms" cat > /etc/systemd/system/archivdms.service << UNIT [Unit] Description=archivdms GoBD-DMS Daemon (API + eingebetteter SFTP-Server) After=network.target postgresql.service manticore.service Wants=manticore.service Requires=postgresql.service [Service] Type=simple User=${DMS_USER} Group=${DMS_USER} ExecStart=${INSTALL_DIR}/bin/archivdms serve -config ${CONFIG_DIR}/config.yml Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal SyslogIdentifier=archivdms NoNewPrivileges=true ProtectSystem=strict ReadWritePaths=${STORE_BASE} ${LOG_DIR} ReadOnlyPaths=${CONFIG_DIR} ${SSL_DIR} [Install] WantedBy=multi-user.target UNIT cat > /etc/systemd/system/archivdms-web.service << UNIT [Unit] Description=archivdms Next.js Frontend After=network.target archivdms.service [Service] Type=simple User=${DMS_USER} Group=${DMS_USER} WorkingDirectory=${INSTALL_DIR}/web ExecStart=/usr/bin/node server.js Environment=NODE_ENV=production Environment=PORT=3000 Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal SyslogIdentifier=archivdms-web [Install] WantedBy=multi-user.target UNIT systemctl daemon-reload systemctl enable archivdms archivdms-web --quiet log "systemd Units synchronisiert" # ── Dienste starten ────────────────────────────────────────────────────────── info "Starte Dienste..." systemctl start archivdms systemctl start archivdms-web log "Dienste gestartet" # ── Status prüfen ──────────────────────────────────────────────────────────── BACKEND_OK=0 FRONTEND_OK=0 info "Prüfe Backend-Health..." for i in $(seq 1 15); do if systemctl is-active --quiet archivdms; then BACKEND_OK=1 break fi sleep 1 done info "Prüfe Frontend-Health (Port 3000)..." for i in $(seq 1 15); do if systemctl is-active --quiet archivdms-web && curl -fsS -o /dev/null --max-time 2 http://127.0.0.1:3000/ 2>/dev/null; then FRONTEND_OK=1 break fi sleep 1 done echo "" echo " ┌──────────────────────────────────────┐" [[ $BACKEND_OK -eq 1 ]] && echo " │ Backend ✓ läuft │" \ || echo " │ Backend ✗ nicht aktiv │" [[ $FRONTEND_OK -eq 1 ]] && echo " │ Frontend ✓ läuft │" \ || echo " │ Frontend ✗ nicht aktiv │" echo " └──────────────────────────────────────┘" echo "" [[ $BACKEND_OK -eq 1 && $FRONTEND_OK -eq 1 ]] && log "Update abgeschlossen." \ || warn "Ein oder mehrere Dienste sind nicht aktiv. Prüfe: journalctl -u archivdms -u archivdms-web"