Files
archivdms/install.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

553 lines
27 KiB
Bash
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.
#!/bin/bash
# archivdms Installer — native Installation (kein Docker)
# Zielumgebung: Debian 13 (trixie), typischerweise ein unprivilegierter
# Proxmox-LXC-Container (funktioniert genauso auf VM/Bare-Metal — es gibt
# hier keinen LXC-spezifischen Zweig, siehe Abschnitt "LXC-Hinweise" unten).
#
# WICHTIG — kein Git-Remote: archivdms hat (Stand jetzt) kein Gitea/GitHub-
# Repository und soll dort auch nicht hochgeladen werden. Dieses Skript zieht
# den Quellcode daher NICHT per `git clone`/`git pull`, sondern erwartet, dass
# der Code bereits lokal auf dem Zielserver liegt (z.B. per rsync/scp vom
# Entwicklungsrechner kopiert). Quelle wird über die Umgebungsvariable
# ARCHIVDMS_SRC festgelegt; Default ist das Verzeichnis, in dem dieses Skript
# selbst liegt (dirname "$0") — das deckt den Normalfall ab, dass man
# install.sh direkt aus dem mitkopierten Projektverzeichnis heraus aufruft.
#
# Aufruf:
# sudo bash install.sh
# ARCHIVDMS_SRC=/opt/archivdms-src sudo bash install.sh
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
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 install.sh"
# ── OS-Prüfung: Debian 13 (trixie) ist der getestete Zielstand ──────────────
# Anders als beim Hard-Fail in archivmail/install.sh nur eine Warnung: Ein DMS
# in einem LXC-Container wird öfter mal auf einem leicht abweichenden Stand
# betrieben (z.B. Debian 12 während einer Migrationsphase) — ein Hard-Block
# hilft hier niemandem, wichtig ist nur, dass der Betreiber es weiß.
_os_id=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "")
_os_ver=$(. /etc/os-release 2>/dev/null && echo "$VERSION_ID" || echo "")
if [[ "$_os_id" == "debian" && "$_os_ver" == "13" ]]; then
log "Debian 13 (trixie) erkannt"
else
warn "Getestet ist Debian 13 (trixie) — erkannt: ${_os_id:-unbekannt} ${_os_ver:-?}. Fahre trotzdem fort."
fi
# ── LXC-Erkennung — rein informativ ─────────────────────────────────────────
# Ein reiner systemd-Service + PostgreSQL + Go-Binary + Next.js-Standalone-
# Setup braucht in einem unprivilegierten LXC-Container keinerlei
# Sonderbehandlung (kein Nested-Docker, kein FUSE, keine Kernel-Module nötig).
# Der einzige Punkt, an dem unprivilegierte LXC-Container gelegentlich
# anecken, ist wenn Software eigene Netzwerk-Namespaces oder rohe Sockets
# braucht — das ist hier nicht der Fall (der Go-Server bindet normale TCP-
# Ports, das reicht auch unprivilegiert).
if command -v systemd-detect-virt >/dev/null 2>&1; then
_virt="$(systemd-detect-virt 2>/dev/null || echo unknown)"
if [[ "$_virt" == "lxc" ]]; then
info "LXC-Container erkannt (systemd-detect-virt: lxc) — kein Sonderhandling nötig für dieses Setup"
else
info "Virtualisierung: ${_virt} (kein LXC — dieses Skript funktioniert unabhängig davon identisch)"
fi
fi
# ── Gemeinsame Variablen ─────────────────────────────────────────────────────
DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)}"
API_SECRET="${API_SECRET:-$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 64)}"
INSTALL_DIR="/opt/archivdms"
STORE_BASE="/var/lib/archivdms"
LOG_DIR="/var/log/archivdms"
CONFIG_DIR="/etc/archivdms"
SSL_DIR="/etc/ssl/archivdms"
DMS_USER="archivdms"
FQDN="$(hostname -f 2>/dev/null || hostname)"
# Quellverzeichnis: entweder explizit über ARCHIVDMS_SRC vorgegeben, sonst das
# Verzeichnis, in dem install.sh selbst liegt (funktioniert, wenn der Code
# per rsync/scp komplett auf den Server kopiert und von dort ausgeführt wird).
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) — ARCHIVDMS_SRC korrekt setzen oder Quellcode dorthin kopieren."
echo ""
echo " ╔══════════════════════════════════════════╗"
echo " ║ archivdms Installer (nativ) ║"
echo " ╚══════════════════════════════════════════╝"
echo ""
info "Hostname: $FQDN"
info "Quellverzeichnis: $ARCHIVDMS_SRC"
echo ""
# ── 1. Basispakete ───────────────────────────────────────────────────────────
# ca-certificates/gnupg/curl explizit zuerst: schlanke LXC-Templates (z.B.
# Proxmox' debian-13-standard) liefern die teils NICHT vorinstalliert, das
# Manticore-Repo (HTTPS + GPG-Key) würde sonst weiter unten fehlschlagen.
info "Installiere Basispakete..."
apt-get update -qq
apt-get install -y -qq \
ca-certificates gnupg curl wget \
postgresql nginx \
rsync logrotate openssl sudo cron \
tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng poppler-utils
log "Basispakete installiert"
# ── 1b. Node.js ≥20 (empfohlen 24.x LTS) ────────────────────────────────────
# Debians eigenes nodejs-Paket ist für ein 2026er Next.js 16 i.d.R. zu alt.
# NodeSource liefert aktuelle LTS-Zeilen für Debian/trixie.
NODE_MAJOR_REQUIRED=20
_node_ok=0
if command -v node >/dev/null 2>&1; then
_node_major="$(node -v | sed -E 's/^v([0-9]+).*/\1/')"
[[ "$_node_major" =~ ^[0-9]+$ && "$_node_major" -ge "$NODE_MAJOR_REQUIRED" ]] && _node_ok=1
fi
if [[ $_node_ok -eq 1 ]]; then
log "Node.js $(node -v) OK (>= v${NODE_MAJOR_REQUIRED})"
else
info "Installiere Node.js 24.x LTS von NodeSource..."
curl -fsSL https://deb.nodesource.com/setup_24.x | bash - >/dev/null 2>&1 \
|| warn "NodeSource-Setup-Skript fehlgeschlagen — versuche apt trotzdem"
apt-get install -y -qq nodejs
command -v node >/dev/null 2>&1 && log "Node.js $(node -v) installiert" \
|| die "Node.js-Installation fehlgeschlagen"
fi
# ── 1c. Go ≥1.26 von upstream (apt-Paket ist zu alt) ────────────────────────
# Analog archivmail: /usr/local/go statt apt, damit die Version unabhängig
# von der Debian-Paketierung und OS-Version aktuell gehalten werden kann.
GO_VERSION="1.26.5"
info "Installiere Go ${GO_VERSION} von upstream..."
if /usr/local/go/bin/go version 2>/dev/null | grep -q "go${GO_VERSION}"; then
log "Go ${GO_VERSION} bereits installiert"
else
_go_arch="amd64"
[[ "$(dpkg --print-architecture)" == "arm64" ]] && _go_arch="arm64"
curl -fsSL "https://dl.google.com/go/go${GO_VERSION}.linux-${_go_arch}.tar.gz" \
| tar -C /usr/local -xz
log "Go $(/usr/local/go/bin/go version | awk '{print $3}') installiert"
fi
export PATH="$PATH:/usr/local/go/bin"
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
# ── 1d. Manticore Search ≥27.x ───────────────────────────────────────────────
# Kein natives trixie-Repo — das bookworm-Paket läuft auch auf trixie/Debian
# 14+, da Manticore kein OS-spezifisches ABI verwendet (siehe archivmail
# update.sh für den ausführlichen Hintergrund). Version wird dynamisch aus der
# Packages-Datei des Repos gelesen; Fallback nur falls das Repo mal nicht
# erreichbar/parsbar ist.
_MANTICORE_VERSION_FALLBACK="27.1.5-26061911-5a1cf9399"
_mc_installed_version="$(dpkg-query -W -f='${Version}' manticore 2>/dev/null || true)"
_mc_arch="$(dpkg --print-architecture)"
_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
[[ -f /tmp/manticore.deb ]] || return 1
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 "Installiere Manticore Search ${_mc_target_version}..."
_mc_fetch_and_install && log "Manticore Search installiert" \
|| warn "Manticore Search konnte nicht installiert werden — siehe: https://manticoresearch.com/install/ (Suche ist im Code laut README noch nicht integriert, kein Hard-Fail)"
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
_mc_fetch_and_install \
&& log "Manticore Search aktualisiert: ${_mc_installed_version}${_mc_target_version}" \
|| warn "Upgrade fehlgeschlagen — vorherige Version bleibt installiert"
else
log "Manticore Search ${_mc_installed_version} bereits aktuell (>= ${_mc_target_version})"
fi
systemctl enable --now manticore 2>/dev/null || warn "Manticore-Dienst konnte nicht gestartet werden"
systemctl is-active --quiet manticore && log "Manticore Search läuft"
# ── 2. Systembenutzer ────────────────────────────────────────────────────────
# Dedizierter, nicht-interaktiver Service-User — der archivdms-Prozess läuft
# nie als root, im Unterschied etwa zu klassischen root-Cron-Setups.
info "Lege Systembenutzer '$DMS_USER' an..."
id "$DMS_USER" &>/dev/null \
&& log "Benutzer '$DMS_USER' existiert bereits" \
|| { useradd --system --shell /bin/false --home "$STORE_BASE" --create-home "$DMS_USER"; log "Benutzer angelegt"; }
# ── 2b. sudo-NOPASSWD-Whitelist für Dienststeuerung ─────────────────────────
# Analog archivmail PROJ-68: falls ein späteres Admin-UI Dienst-Restarts aus
# der Anwendung heraus auslösen soll, ist die Whitelist bereits vorbereitet.
# Validierung per `visudo -c` VOR dem Einspielen — eine kaputte sudoers-Datei
# würde sonst den gesamten sudo-Mechanismus auf dem Server lahmlegen.
# Idempotent: deterministischer Inhalt, überschreibt bei jedem Lauf.
provision_archivdms_sudoers() {
local dms_user="${1:-$DMS_USER}"
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 eingerichtet (/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
# ── 3. Verzeichnisstruktur ───────────────────────────────────────────────────
# Layout gemäß README.md "Storage-Struktur & OCR" / config/config.go
# (StorageConfig.BasePath + InboxPath/StorePath/OCRTmpPath). config.Load()
# legt diese Unterverzeichnisse beim Serverstart selbst per os.MkdirAll an
# (0750) — install.sh legt die Wurzel bereits mit korrektem Owner an, damit
# der archivdms-User (nicht root) sie beim ersten Start auch beschreiben kann.
info "Erstelle Verzeichnisstruktur..."
mkdir -p "$STORE_BASE/inbox" "$STORE_BASE/store" "$STORE_BASE/ocr-tmp"
mkdir -p "$CONFIG_DIR" "$LOG_DIR" "$INSTALL_DIR" "$SSL_DIR"
chown -R "$DMS_USER:$DMS_USER" "$STORE_BASE" "$LOG_DIR"
chmod 750 "$STORE_BASE" "$STORE_BASE/inbox" "$STORE_BASE/store" "$STORE_BASE/ocr-tmp"
log "Verzeichnisse erstellt ($STORE_BASE/{inbox,store,ocr-tmp})"
# ── 4. TLS-Zertifikat (selbstsigniert, Let's-Encrypt-Option siehe unten) ────
info "Erstelle selbstsigniertes TLS-Zertifikat..."
if [[ ! -f "$SSL_DIR/archivdms.crt" ]]; then
SERVER_IP="$(hostname -I | awk '{print $1}')"
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
-keyout "$SSL_DIR/archivdms.key" \
-out "$SSL_DIR/archivdms.crt" \
-subj "/CN=${FQDN}/O=archivdms/C=DE" \
-addext "subjectAltName=DNS:${FQDN},DNS:$(hostname -s),IP:${SERVER_IP}" \
2>/dev/null
chmod 640 "$SSL_DIR/archivdms.key"
chmod 644 "$SSL_DIR/archivdms.crt"
chown "root:$DMS_USER" "$SSL_DIR/archivdms.key"
log "TLS-Zertifikat erstellt: $SSL_DIR/archivdms.crt"
else
log "TLS-Zertifikat existiert bereits wird nicht überschrieben"
fi
# ── 5. PostgreSQL ─────────────────────────────────────────────────────────
info "Richte PostgreSQL ein..."
systemctl enable postgresql --quiet
systemctl start postgresql
# Falls config.yml schon existiert, DB-Passwort daraus übernehmen — verhindert
# Passwort-Mismatch zwischen laufender DB-Rolle und Config bei Re-Install.
if [[ -f "$CONFIG_DIR/config.yml" ]]; then
EXISTING_PW=$(grep -A6 '^database:' "$CONFIG_DIR/config.yml" | awk '/password:/{print $2}' | head -1 | tr -d '"')
[[ -n "$EXISTING_PW" ]] && DB_PASSWORD="$EXISTING_PW" && info "DB-Passwort aus vorhandener config.yml übernommen"
fi
su -c "psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='archivdms'\" | grep -q 1 \
&& psql -c \"ALTER USER archivdms WITH PASSWORD '$DB_PASSWORD'\" \
|| psql -c \"CREATE USER archivdms WITH PASSWORD '$DB_PASSWORD'\"" postgres
su -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='archivdms'\" | grep -q 1 || \
psql -c \"CREATE DATABASE archivdms OWNER archivdms\"" postgres
su -c "psql archivdms -c \"GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO archivdms;\"" postgres
su -c "psql archivdms -c \"GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO archivdms;\"" postgres
su -c "psql archivdms -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO archivdms;\"" postgres
su -c "psql archivdms -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO archivdms;\"" postgres
log "PostgreSQL eingerichtet"
# ── 6. Konfiguration (config.yml aus config.yml.example) ────────────────────
info "Erstelle Konfigurationsdatei..."
if [[ ! -f "$CONFIG_DIR/config.yml" ]]; then
[[ -f "$ARCHIVDMS_SRC/config/config.yml.example" ]] \
|| die "config/config.yml.example nicht gefunden unter $ARCHIVDMS_SRC"
sed \
-e "s/CHANGE_ME_TO_A_LONG_RANDOM_SECRET/$API_SECRET/g" \
-e "s/password: \"CHANGE_ME\"/password: \"$DB_PASSWORD\"/" \
-e "s/dms\.example\.com/$FQDN/" \
"$ARCHIVDMS_SRC/config/config.yml.example" \
> "$CONFIG_DIR/config.yml"
# storage.base_path und Log-Pfade auf die tatsächlich angelegten
# Verzeichnisse zeigen lassen (das Beispiel verwendet dieselben Defaults,
# sed hier trotzdem defensiv falls sich das Example mal ändert).
sed -i \
-e "s#base_path: \".*\"#base_path: \"$STORE_BASE\"#" \
-e "s#log_path: \".*\"#log_path: \"$LOG_DIR/audit.log\"#" \
-e "s#path: \"/var/log/archivdms/app.log\"#path: \"$LOG_DIR/app.log\"#" \
"$CONFIG_DIR/config.yml"
chmod 640 "$CONFIG_DIR/config.yml"
chown "root:$DMS_USER" "$CONFIG_DIR/config.yml"
log "config.yml erstellt: $CONFIG_DIR/config.yml"
else
log "config.yml existiert bereits wird nicht überschrieben"
fi
# ── 7. nginx Reverse-Proxy (mit Let's-Encrypt-Option) ───────────────────────
info "Konfiguriere nginx (HTTP → HTTPS + TLS)..."
cat > /etc/nginx/sites-available/archivdms << NGINX
# HTTP → HTTPS Redirect
server {
listen 80;
server_name ${FQDN} _;
return 301 https://\$host\$request_uri;
}
# HTTPS
server {
listen 443 ssl;
http2 on;
server_name ${FQDN} _;
ssl_certificate ${SSL_DIR}/archivdms.crt;
ssl_certificate_key ${SSL_DIR}/archivdms.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
client_max_body_size 512M;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
}
access_log /var/log/nginx/archivdms.access.log;
error_log /var/log/nginx/archivdms.error.log;
}
NGINX
ln -sf /etc/nginx/sites-available/archivdms /etc/nginx/sites-enabled/archivdms
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl enable nginx --quiet
systemctl restart nginx
log "nginx konfiguriert"
# Let's-Encrypt-Option: nur ein Hinweis, kein automatischer Zwang zu einer
# öffentlichen Domain — viele DMS-Installationen laufen intern ohne
# öffentlich auflösbaren FQDN, wo certbot ohnehin fehlschlagen würde.
if command -v certbot >/dev/null 2>&1; then
info "certbot bereits installiert — Let's-Encrypt-Zertifikat manuell holen mit:"
echo " certbot --nginx -d ${FQDN}"
else
info "Let's-Encrypt-Zertifikat gewünscht? Falls ${FQDN} öffentlich auflösbar ist:"
echo " apt-get install -y certbot python3-certbot-nginx && certbot --nginx -d ${FQDN}"
fi
# ── 8. logrotate ──────────────────────────────────────────────────────────
cat > /etc/logrotate.d/archivdms << LOGROTATE
${LOG_DIR}/*.log {
daily
rotate 365
compress
delaycompress
missingok
notifempty
create 640 ${DMS_USER} ${DMS_USER}
}
LOGROTATE
log "logrotate konfiguriert"
# ── 9. Cron-Job für Wiedervorlage-Benachrichtigung ─────────────────────────
# deploy/cron.d/archivdms-reminders liegt bereits fertig im Projekt (siehe
# README "Struktur") — hier nur nach /etc/cron.d/ kopieren.
info "Installiere Cron-Job für Wiedervorlage-Benachrichtigung..."
if [[ -f "$ARCHIVDMS_SRC/deploy/cron.d/archivdms-reminders" ]]; then
cp "$ARCHIVDMS_SRC/deploy/cron.d/archivdms-reminders" /etc/cron.d/archivdms-reminders
chmod 644 /etc/cron.d/archivdms-reminders
systemctl enable --now cron --quiet 2>/dev/null || true
log "Cron-Job installiert: /etc/cron.d/archivdms-reminders"
else
warn "deploy/cron.d/archivdms-reminders nicht gefunden — Wiedervorlage-Mails laufen nicht automatisch"
fi
# ── 9b. Cron-Job für Klassifizierer-Retraining ─────────────────────────────
info "Installiere Cron-Job für Klassifizierer-Retraining..."
if [[ -f "$ARCHIVDMS_SRC/deploy/cron.d/archivdms-classify-retrain" ]]; then
cp "$ARCHIVDMS_SRC/deploy/cron.d/archivdms-classify-retrain" /etc/cron.d/archivdms-classify-retrain
chmod 644 /etc/cron.d/archivdms-classify-retrain
systemctl enable --now cron --quiet 2>/dev/null || true
log "Cron-Job installiert: /etc/cron.d/archivdms-classify-retrain"
else
warn "deploy/cron.d/archivdms-classify-retrain nicht gefunden — Klassifizierer-Retraining läuft nicht automatisch"
fi
# ── 10. systemd Unit für 'archivdms serve' ──────────────────────────────────
# Der SFTP-Server läuft im selben Prozess mit (kein eigener systemd-Dienst) —
# er startet nur, wenn cfg.SFTP.Enabled in config.yml gesetzt ist
# (siehe cmd/archivdms/main.go).
info "Erstelle systemd Unit..."
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 erstellt und aktiviert"
# ── 11. Erstes Build + Deployment via update.sh ─────────────────────────────
info "Führe update.sh für den ersten Build/Deploy aus..."
if [[ -f "$SCRIPT_DIR/update.sh" ]]; then
chmod +x "$SCRIPT_DIR/update.sh"
ARCHIVDMS_SRC="$ARCHIVDMS_SRC" bash "$SCRIPT_DIR/update.sh"
else
warn "update.sh nicht gefunden neben install.sh — Build/Start manuell nötig: bash update.sh"
fi
# ── Superadmin-Passwort aus dem Log ziehen (siehe main.go seedDefaultUsers) ─
PW_SUPERADMIN="(nicht gefunden — journalctl -u archivdms | grep superadmin)"
for _i in $(seq 1 15); do
_pw=$(journalctl -u archivdms --no-pager -n 200 2>/dev/null | grep -A1 'superadmin :' | grep -oP 'superadmin\s*:\s*\K\S+' | tail -1)
if [[ -n "$_pw" ]]; then
PW_SUPERADMIN="$_pw"
break
fi
sleep 1
done
# ── Zusammenfassung ───────────────────────────────────────────────────────
SUMMARY_FILE="$CONFIG_DIR/install-summary.txt"
cat > "$SUMMARY_FILE" << SUMMARY
archivdms Native-Installation — $(date '+%d.%m.%Y %H:%M:%S')
Server: $FQDN
=== ZUGANGSDATEN ===
Datenbank: archivdms / $DB_PASSWORD
API-Secret: $API_SECRET
Web-Login (UNBEDINGT ÄNDERN!):
superadmin / $PW_SUPERADMIN
=== DIENSTE ===
Web (HTTPS): https://$FQDN
API intern: 127.0.0.1:8080
Frontend intern: 127.0.0.1:3000
=== DATEIPFADE ===
Konfiguration: $CONFIG_DIR/config.yml
Storage: $STORE_BASE/{inbox,store,ocr-tmp}
TLS-Zertifikat: $SSL_DIR/archivdms.crt
Logs: $LOG_DIR/
Updater: $SCRIPT_DIR/update.sh
Quellcode: $ARCHIVDMS_SRC
SUMMARY
chmod 600 "$SUMMARY_FILE"
log "Zusammenfassung: $SUMMARY_FILE"
# ── Abschluss ────────────────────────────────────────────────────────────
echo ""
echo " ╔══════════════════════════════════════════════════════════╗"
echo " ║ Installation abgeschlossen! ║"
echo " ╚══════════════════════════════════════════════════════════╝"
echo ""
echo " Web (HTTPS): https://$FQDN"
echo ""
echo " ┌─────────────────────────────────────────────────────────┐"
printf " │ DB archivdms: %-40s │\n" "$DB_PASSWORD"
printf " │ superadmin / %-40s │\n" "$PW_SUPERADMIN"
echo " └─────────────────────────────────────────────────────────┘"
echo ""
warn "Zusammenfassung mit Passwörtern: $SUMMARY_FILE"
warn "Standardpasswort unbedingt nach dem ersten Login ändern!"
echo ""