Files
archivdms/update.sh
T
patrick 9a24ea29e1 FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
2026-08-11 21:27:53 +02:00

380 lines
17 KiB
Bash

#!/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/<build-dir>/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"