// Alle Einstellungen an einer Stelle, alle aus der Umgebung. // // Was fehlt, faellt beim START auf und nicht beim ersten Upload. Das ist // Absicht: ein Dienst, der bereitwillig hochfaehrt und erst Stunden spaeter // merkt, dass ihm die halbe Konfiguration fehlt, kostet mehr Zeit als einer, // der sich sofort weigert. import { existsSync, mkdirSync } from 'node:fs' import { resolve } from 'node:path' function required(name: string): string { const value = process.env[name] if (!value) { console.error(`[config] ${name} fehlt. Ohne die Variable startet der Dienst nicht.`) process.exit(1) } return value } function optional(name: string, fallback: string): string { return process.env[name] || fallback } function number(name: string, fallback: number): number { const raw = process.env[name] if (!raw) return fallback const parsed = Number(raw) if (!Number.isFinite(parsed) || parsed <= 0) { console.error(`[config] ${name}="${raw}" ist keine sinnvolle Zahl.`) process.exit(1) } return parsed } /** Wohin die Dateien und die Datenbank kommen. EIN Ordner, damit eine * Sicherung aus genau einem `cp -a` besteht. */ const dataDir = resolve(optional('DATA_DIR', './data')) const filesDir = resolve(dataDir, 'files') for (const dir of [dataDir, filesDir]) { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) } /** Unter welcher Adresse der Dienst oeffentlich steht. Daraus wird die URL * gebaut, die nach dem Upload zurueckkommt — und die im Handy in der Vorlage * steht. Nur der Ursprung, ohne Pfad und ohne abschliessenden Schraegstrich: * https://media.d4rkst3r.de * * Das /f/ im Ein-Namen-Betrieb haengt der Dienst selbst an. Wer es hier * mitschriebe, bekaeme Adressen mit /f/f/ — und die faende niemand, bis das * erste Bild fehlt. */ const publicUrl = required('PUBLIC_URL').replace(/\/+$/, '') /** Ein Name oder zwei? * * Gesetzt → ZWEI Namen: unter diesem Host gibt es ausschliesslich Dateien, * kein Dashboard, keine API. Die oeffentliche Adresse hat damit * keine Angriffsflaeche, und die URLs kommen ohne Praefix aus. * Leer → EIN Name: Dashboard und API an der Wurzel, Dateien unter /f/. * * Frueher war der Standard der Host aus PUBLIC_URL. Das ist bei zwei Namen * richtig und bei einem toedlich: dann gilt JEDE Anfrage als Anfrage an den * Dateiwirt, /health antwortet 404 und die Anmeldung 405 "hier gibt es nur * Dateien". Nichts ist kaputt, und niemand kommt darauf. Deshalb ist der * Ein-Namen-Betrieb jetzt der Standard und der zweite Name die Ansage. */ const filesHost = (process.env.FILES_HOST ?? '').trim().toLowerCase() export const config = { port: number('PORT', 8080), dataDir, filesDir, dbPath: resolve(dataDir, 'media.db'), publicUrl, /** Der Hostname, unter dem NUR Dateien ausgeliefert werden — oder leer, * wenn alles unter einem Namen laeuft. Siehe oben. */ filesHost, /** Was zwischen Adresse und Pfad steht. Bei zwei Namen nichts, bei einem * "/f" — beides an genau EINER Stelle entschieden, damit die gebaute URL * und die Route, die sie ausliefert, nicht auseinanderlaufen koennen. */ filePrefix: filesHost ? '' : '/f', /** Wird beim Start angelegt, falls es noch keinen Benutzer gibt. */ adminPassword: required('ADMIN_PASSWORD'), adminUsername: optional('ADMIN_USERNAME', 'admin'), /** Groesste erlaubte Datei. Ein freigestelltes Fahrzeugbild wiegt rund * 200 KB; 64 MB lassen Luft fuer Videos, ohne dass ein Fehlgriff die * Platte fuellt. */ maxUploadBytes: number('MAX_UPLOAD_MB', 64) * 1024 * 1024, /** Wie lange eine Anmeldung im Dashboard haelt. */ sessionDays: number('SESSION_DAYS', 30), dev: process.env.NODE_ENV !== 'production', } as const export type Config = typeof config