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:
+552
@@ -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 ""
|
||||
Reference in New Issue
Block a user