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:
2026-08-11 21:27:53 +02:00
parent 40ed80da71
commit 9a24ea29e1
274 changed files with 53708 additions and 0 deletions
+552
View File
@@ -0,0 +1,552 @@
#!/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 ""