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.
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user