DER FEHLER ZUERST, denn er ist meiner von gestern: die Liste der
Meldungsanlaesse stand ZWEIMAL da -- als Standard in meldung.ts und noch einmal
als Weissliste beim Speichern in dash.ts. Beim Einbau des Plattenwaechters habe
ich nur die erste angefasst.
Ergebnis: das Kaestchen "Der Platz auf der Platte wird knapp" liess sich
ankreuzen, der Server warf den Wert beim Speichern weg, und beim naechsten
Laden war es wieder aus. Ohne ein Wort dazu. Nachgemessen am laufenden Dienst:
geschickt ["sicherung","verwaiste","platte"]
gespeichert ["sicherung","verwaiste"]
Die Liste steht jetzt an EINER Stelle (ANLAESSE in meldung.ts) und wird von
beiden benutzt. Gegengeprueft: alle drei kommen an.
SITZUNGEN BEENDEN. Wer sich an einem fremden Rechner angemeldet hat und es
spaeter merkt, hatte keine Moeglichkeit das zurueckzunehmen -- ein geaendertes
Passwort half nicht, die Sitzungen haengen an einer eigenen Tabelle und
ueberleben es. Der Knopf steht im Kontomenue und nur dann, wenn es ueberhaupt
eine zweite Sitzung gibt. Gemessen: 87 offen, 86 beendet, meine lebt.
Dabei in die eigene Falle getreten: der Weg lag zuerst bei den anderen
/auth-Wegen -- und die stehen mit Absicht VOR der Wache, weil man sich anmelden
koennen muss, ohne angemeldet zu sein. Dort ist c.get('user') leer, und der Weg
antwortete mit "Cannot read properties of undefined". Hono setzt Middleware und
Handler in der Reihenfolge ihrer Anmeldung zusammen; der Pfad sagt darueber
nichts.
VERWALTUNGS-VERLAUF. Der Datei-Verlauf beantwortet "was ist mit den Dateien
passiert". Wer den Discord-Webhook geaendert, einen Token angelegt oder ein
Konto entfernt hat, stand nirgends. Bei zwei Konten verschmerzbar -- sobald ein
drittes ueber die Discord-Rolle von selbst entsteht, ist es die erste Frage.
Eigene Tabelle und nicht events: dort traegt jede Zeile path und size, und eine
Einstellungsaenderung muesste path mit etwas fuellen, das kein Pfad ist.
WERTE STEHEN DORT NIE, nur Schluesselnamen. Unter den Einstellungen liegen das
Discord-Geheimnis und das Nextcloud-Passwort; ein Verlauf, der sie mitschreibt,
macht aus einer Tabelle mit einem Passwort eine Tabelle mit allen, die es je
gab.
Beim Einbau derselbe Fehlertyp wie gestern beim SVG: im SQL-Kommentar standen
Backticks, und das Schema liegt in einem Template-Literal -- ein Backtick
beendet es. Der Uebersetzer hat es gefangen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
457 lines
19 KiB
TypeScript
457 lines
19 KiB
TypeScript
// Der Einstiegspunkt.
|
|
//
|
|
// EIN NAME ODER ZWEI, ein Prozess in beiden Faellen. Was gilt, entscheidet
|
|
// FILES_HOST, und der Start sagt es in einer Zeile.
|
|
//
|
|
// EIN NAME (Standard). Dashboard und API an der Wurzel, Dateien unter /f/:
|
|
// https://media.d4rkst3r.de/f/vehicles/adder.webp
|
|
// Ein DNS-Eintrag, ein Host im Proxy. Dafuer traegt die oeffentliche Adresse
|
|
// auch das Anmeldeformular.
|
|
//
|
|
// ZWEI NAMEN. Unter FILES_HOST gibt es dann ausschliesslich Dateien — kein
|
|
// Dashboard, keine API, nichts anzumelden:
|
|
// https://cdn.example.de/vehicles/adder.webp
|
|
// Das haelt den Namen, den jeder Spieler kennt, ohne Angriffsflaeche und
|
|
// spart das Praefix. Unter dem anderen Namen laeuft das Dashboard, und die
|
|
// Dateien sind dort zusaetzlich unter /f/ erreichbar.
|
|
//
|
|
// In beiden Faellen gilt: DER PFAD IST DER SCHLUESSEL. vehicles/adder.webp
|
|
// bleibt vehicles/adder.webp, damit die Vorlage im Handy {model}.webp lauten
|
|
// kann. Nur der Teil davor aendert sich — und der steht in PUBLIC_URL.
|
|
|
|
import { createReadStream } from 'node:fs'
|
|
import { stat } from 'node:fs/promises'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { join, resolve, sep } from 'node:path'
|
|
import { serve } from '@hono/node-server'
|
|
import { Hono } from 'hono'
|
|
import { logger } from 'hono/logger'
|
|
import { config } from './config.js'
|
|
import { ensureAdmin } from './auth.js'
|
|
import { pruneEvents, pruneSessions, pruneVerwaltung, schreibeAbrufe, zaehleAbruf } from './db.js'
|
|
import { pruefePlatte, pruefeSicherung } from './meldung.js'
|
|
import { absolutePath, checkPath, lohntWebp, mimeFor, thumbPath, webpPfad } from './storage.js'
|
|
import { uploadRoutes } from './routes/upload.js'
|
|
import { fivemanageRoutes } from './routes/fivemanage.js'
|
|
import { freigabeRoutes } from './routes/freigabe.js'
|
|
import { statusRoutes } from './routes/status.js'
|
|
import { dashRoutes, papierkorbAusleeren } from './routes/dash.js'
|
|
|
|
const app = new Hono()
|
|
|
|
if (config.dev) app.use('*', logger())
|
|
|
|
// Ein unerwarteter Fehler bekommt einen TEXT, keinen nackten 500.
|
|
//
|
|
// Genau das ist der Fehler, wegen dem dieses Projekt existiert: bei Fivemanage
|
|
// endete jeder Fehlschlag als "Internal Server Error" ohne ein Wort dazu. Wir
|
|
// haben das an den bekannten Stellen einzeln abgefangen -- und prompt hat ein
|
|
// Tippfehler in einer SQL-Zeile beim Export genau diese nackte Antwort
|
|
// erzeugt. Eine Auffanglinie an EINER Stelle schliesst die Luecke fuer alles,
|
|
// woran heute niemand denkt.
|
|
//
|
|
// Die Meldung geht mit hinaus. Das ist eine bewusste Abwaegung: dieses
|
|
// Dashboard hat einen Benutzerkreis von einer Person, und ein Fehler, den man
|
|
// nicht lesen kann, kostet mehr als er verbirgt. Die Spur bleibt zusaetzlich
|
|
// in der Konsole.
|
|
app.onError((err, c) => {
|
|
console.error('[fehler]', c.req.method, c.req.path, err)
|
|
const text = err instanceof Error ? err.message : String(err)
|
|
if (c.req.path.startsWith('/api/')) {
|
|
return c.json({ error: text }, 500)
|
|
}
|
|
return c.text(`Serverfehler: ${text}`, 500)
|
|
})
|
|
|
|
// Ein Fehler AUSSERHALB jedes try/catch darf den Dienst nicht umbringen.
|
|
//
|
|
// Genau das ist beim Uebernehmen von 3670 Dateien passiert: undici warf
|
|
// "ReadableStream is already closed" aus einem Microtask -- also an einer
|
|
// Stelle, an der kein try/catch der Welt steht. Node beendet den Prozess
|
|
// daraufhin, Docker startet ihn neu, die naechste Anfrage kippt ihn wieder:
|
|
// eine Neustartschleife, und nach aussen 502.
|
|
//
|
|
// DIE ABWAEGUNG, ausgesprochen: einen unerwarteten Fehler zu ueberleben ist
|
|
// nicht in jedem Fall richtig -- der Prozess KANN danach in einem kaputten
|
|
// Zustand sein. Hier ueberwiegt trotzdem das Weiterlaufen: dieser Dienst haelt
|
|
// keinen Zustand im Speicher, der verderben koennte (alles steht in SQLite und
|
|
// auf der Platte), und ein Dienst, der wegen EINER Anfrage fuer alle weg ist,
|
|
// ist der schlechtere Tausch.
|
|
//
|
|
// Laut wird es trotzdem: die Spur steht vollstaendig in der Konsole.
|
|
process.on('uncaughtException', (err) => {
|
|
console.error('[unerwartet] uncaughtException — der Dienst laeuft weiter:', err)
|
|
})
|
|
process.on('unhandledRejection', (grund) => {
|
|
console.error('[unerwartet] unhandledRejection — der Dienst laeuft weiter:', grund)
|
|
})
|
|
|
|
// -------------------------------------------------------- Dateien ausliefern
|
|
|
|
/** Liefert eine Datei aus, mit ETag und Bereichsanfragen.
|
|
*
|
|
* Bereichsanfragen sind kein Luxus: ohne sie kann ein Browser in einem Video
|
|
* nicht springen, und ein abgebrochener Download faengt von vorn an. */
|
|
async function serveFile(c: any, rawPath: string) {
|
|
let path: string
|
|
try {
|
|
path = checkPath(decodeURIComponent(rawPath))
|
|
} catch {
|
|
return c.text('nicht gefunden', 404)
|
|
}
|
|
|
|
let absolute = absolutePath(path)
|
|
let info
|
|
try {
|
|
info = await stat(absolute)
|
|
if (!info.isFile()) return c.text('nicht gefunden', 404)
|
|
} catch {
|
|
return c.text('nicht gefunden', 404)
|
|
}
|
|
|
|
// DIE SPARSAME FASSUNG, unter DERSELBEN Adresse.
|
|
//
|
|
// Der Browser sagt mit `Accept`, was er versteht. Versteht er WebP und
|
|
// liegt eine Fassung daneben, bekommt er sie. Die Adresse bleibt
|
|
// items/foo.png, und kein Skript merkt etwas davon. Lua, curl und alles
|
|
// ohne diese Ansage bekommen unveraendert das Original.
|
|
//
|
|
// Der Vergleich ist absichtlich stumpf: `Accept` traegt Gewichte, und ein
|
|
// "image/webp;q=0" hiesse eigentlich "bitte nicht". Das schickt in der
|
|
// Praxis niemand -- und faellt jemand doch darauf herein, bekommt er ein
|
|
// gueltiges Bild, nur in einem anderen Format. Der Preis eines Fehlers ist
|
|
// hier klein genug, dass die einfache Pruefung die richtige ist.
|
|
let typ = mimeFor(path)
|
|
let sparsam = false
|
|
if (lohntWebp(typ) && (c.req.header('accept') ?? '').includes('image/webp')) {
|
|
try {
|
|
const wInfo = await stat(webpPfad(path))
|
|
if (wInfo.isFile()) {
|
|
absolute = webpPfad(path)
|
|
info = wInfo
|
|
typ = 'image/webp'
|
|
sparsam = true
|
|
}
|
|
} catch {
|
|
/* keine sparsame Fassung -- dann eben das Original */
|
|
}
|
|
}
|
|
|
|
// Das Kennzeichen gehoert IN den ETag: sonst traegt das Original und die
|
|
// sparsame Fassung dieselbe Marke, sobald beide zufaellig gleich gross und
|
|
// gleich alt sind, und ein Aufrufer bekaeme auf seinen 304 hin das falsche
|
|
// Bild aus seinem eigenen Zwischenspeicher.
|
|
const etag = `"${info.size.toString(16)}-${info.mtimeMs.toString(16)}${sparsam ? '-w' : ''}"`
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': typ,
|
|
'Cache-Control': 'public, max-age=300',
|
|
// OHNE DIESE ZEILE legt ein Zwischenspeicher die WebP-Fassung fuer ALLE
|
|
// ab -- auch fuer die, die sie nicht lesen koennen.
|
|
Vary: 'Accept',
|
|
ETag: etag,
|
|
'Last-Modified': info.mtime.toUTCString(),
|
|
'Accept-Ranges': 'bytes',
|
|
// Kein Verlassen auf den Dateinamen: der Browser soll nicht raten,
|
|
// was er da vor sich hat.
|
|
'X-Content-Type-Options': 'nosniff',
|
|
}
|
|
|
|
// GEZAEHLT WIRD HIER, nach der Pruefung und vor der Auslieferung.
|
|
//
|
|
// Auch der 304 zaehlt: der Aufrufer WOLLTE die Datei, er hatte sie nur
|
|
// schon. Fuer die Frage "braucht das noch jemand" ist das dasselbe.
|
|
// HEAD zaehlt nicht -- das fragt nach der Datei, nicht nach dem Inhalt.
|
|
if (c.req.method !== 'HEAD') zaehleAbruf(path)
|
|
|
|
if (c.req.header('if-none-match') === etag) return c.body(null, 304, headers)
|
|
if (c.req.method === 'HEAD') {
|
|
return c.body(null, 200, { ...headers, 'Content-Length': String(info.size) })
|
|
}
|
|
|
|
const range = /^bytes=(\d*)-(\d*)$/.exec(c.req.header('range') ?? '')
|
|
if (range) {
|
|
const start = range[1] ? Number(range[1]) : 0
|
|
const end = range[2] ? Number(range[2]) : info.size - 1
|
|
if (start >= info.size || end >= info.size || start > end) {
|
|
return c.body(null, 416, { 'Content-Range': `bytes */${info.size}` })
|
|
}
|
|
return c.body(createReadStream(absolute, { start, end }) as any, 206, {
|
|
...headers,
|
|
'Content-Range': `bytes ${start}-${end}/${info.size}`,
|
|
'Content-Length': String(end - start + 1),
|
|
})
|
|
}
|
|
|
|
return c.body(createReadStream(absolute) as any, 200, {
|
|
...headers,
|
|
'Content-Length': String(info.size),
|
|
})
|
|
}
|
|
|
|
// ------------------------------------------------------------ Der Dateiwirt
|
|
|
|
app.use('*', async (c, next) => {
|
|
// Kein zweiter Name gesetzt: dann gibt es hier nichts zu entscheiden.
|
|
// Ohne diese Zeile wuerde im Ein-Namen-Betrieb jede Anfrage als Anfrage an
|
|
// den Dateiwirt gelten und die eigene API verschwinden.
|
|
if (!config.filesHost) return next()
|
|
|
|
const host = (c.req.header('host') ?? '').split(':')[0]?.toLowerCase()
|
|
if (host !== config.filesHost.split(':')[0]?.toLowerCase()) return next()
|
|
|
|
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
|
|
return c.text('hier gibt es nur Dateien', 405)
|
|
}
|
|
if (c.req.path === '/') return c.text('d4rk_media', 200)
|
|
|
|
return serveFile(c, c.req.path.slice(1))
|
|
})
|
|
|
|
// ------------------------------------------------------- Dashboard und API
|
|
|
|
// Der HEALTHCHECK des Containers haengt hier dran, und deshalb bleibt es
|
|
// billig: eine schwere Pruefung, die bei einer langsamen Platte einmal
|
|
// ausfaellt, liesse Docker den Container neu starten -- also genau dann, wenn
|
|
// er unter Last steht.
|
|
//
|
|
// Was diese Zeile beweist, ist genau eines: der Prozess nimmt Anfragen an.
|
|
// Fuer eine STATUSSEITE ist das zu duenn -- dafuer gibt es /status daneben,
|
|
// das wirklich nachsieht.
|
|
app.get('/health', (c) => c.json({ ok: true }))
|
|
|
|
// Fuer die Statusseite. Oeffentlich wie /health, aber es prueft Datenbank,
|
|
// Platte, Ausliefern und die Sicherung -- und antwortet mit 503, wenn der
|
|
// Dienst sein Geschaeft nicht mehr tun kann.
|
|
app.route('/status', statusRoutes)
|
|
|
|
app.route('/api', uploadRoutes)
|
|
// NACH uploadRoutes: unsere eigenen Wege haben Vorrang, falls je ein Name
|
|
// zusammenfaellt. Derzeit tut er das nicht (/upload, /exists, /media gegen
|
|
// /image, /video, /audio, /file, /v3/file), aber die Reihenfolge sagt, was
|
|
// gelten soll, wenn es einmal so kommt.
|
|
app.route('/api', fivemanageRoutes)
|
|
app.route('/api/dash', dashRoutes)
|
|
|
|
// Die Freigaben. OHNE Anmeldung und mit Absicht kurz: /s/<schluessel> laesst
|
|
// sich vorlesen und abtippen.
|
|
app.route('/api/s', freigabeRoutes)
|
|
|
|
app.get('/f/*', (c) => serveFile(c, c.req.path.slice(3)))
|
|
|
|
/** Die Vorschaubilder, unter /t/.
|
|
*
|
|
* Absichtlich NICHT unter dem Dateihost und absichtlich ohne Anmeldung: die
|
|
* Bilder selbst sind ohnehin oeffentlich, und das Dashboard laedt die
|
|
* Vorschauen mit gewoehnlichen <img>-Marken — mit Anmeldung braeuchte es
|
|
* dafuer einen Umweg ueber fetch und Blob-Adressen.
|
|
*
|
|
* Fehlt eine Vorschau, kommt 404 und das Dashboard nimmt das Vollbild. Kein
|
|
* Erzeugen auf Zuruf: eine Route, die bei jeder Anfrage rechnen kann, ist
|
|
* eine Route, die sich mit 900 gleichzeitigen Anfragen umbringen laesst. */
|
|
app.get('/t/*', async (c) => {
|
|
const roh = c.req.path.slice(3)
|
|
let path: string
|
|
try {
|
|
path = checkPath(decodeURIComponent(roh))
|
|
} catch {
|
|
return c.text('nicht gefunden', 404)
|
|
}
|
|
|
|
const absolute = thumbPath(path)
|
|
let info
|
|
try {
|
|
info = await stat(absolute)
|
|
if (!info.isFile()) return c.text('nicht gefunden', 404)
|
|
} catch {
|
|
return c.text('nicht gefunden', 404)
|
|
}
|
|
|
|
const etag = `"${info.size.toString(16)}-${info.mtimeMs.toString(16)}"`
|
|
if (c.req.header('if-none-match') === etag) return c.body(null, 304)
|
|
|
|
return c.body(createReadStream(absolute) as any, 200, {
|
|
'Content-Type': 'image/webp',
|
|
'Content-Length': String(info.size),
|
|
ETag: etag,
|
|
// Kurz: eine Vorschau aendert sich, wenn das Bild neu hochgeladen
|
|
// wird, und dann soll das Dashboard es sehen.
|
|
'Cache-Control': 'public, max-age=60',
|
|
'X-Content-Type-Options': 'nosniff',
|
|
})
|
|
})
|
|
|
|
// Die gebaute Oberflaeche. Alles, was keine API ist und keine Datei, bekommt
|
|
// index.html — sonst endet ein Neuladen auf /tokens im 404 statt im Router.
|
|
// fileURLToPath und nicht .pathname: unter Windows liefert letzteres
|
|
// "/C:/..." und jedes stat() darauf schlaegt fehl.
|
|
const webDir = resolve(fileURLToPath(new URL('../web/', import.meta.url)))
|
|
|
|
/** Der absolute Pfad zu einer Datei der Oberflaeche — oder null, wenn er aus
|
|
* dem Ordner herauszeigt.
|
|
*
|
|
* Dieselbe Guertel-und-Hosentraeger-Regel wie in storage.ts, und aus demselben
|
|
* Grund: der Pfad kommt vom Aufrufer. Ohne die Pruefung hat
|
|
* `GET /..%5Cpackage.json` unter Windows die Datei ausgeliefert — Hono reicht
|
|
* %5C unveraendert durch, und path.join behandelt den Backslash dort als
|
|
* Trenner. Unter Linux traegt derselbe Angriff nicht, aber das ist Glueck und
|
|
* keine Abwehr. */
|
|
function webFile(candidate: string): string | null {
|
|
const target = resolve(webDir, decodeSafely(candidate).replace(/^[/\\]+/, ''))
|
|
if (target !== webDir && !target.startsWith(webDir + sep)) return null
|
|
return target
|
|
}
|
|
|
|
/** %5C und Konsorten aufloesen, bevor geprueft wird — sonst prueft die
|
|
* Eindaemmung eine andere Zeichenkette als die, die spaeter im Dateisystem
|
|
* landet. Ein kaputtes Prozentzeichen ist kein Grund abzustuerzen. */
|
|
function decodeSafely(raw: string): string {
|
|
try {
|
|
return decodeURIComponent(raw)
|
|
} catch {
|
|
return raw
|
|
}
|
|
}
|
|
|
|
/** Die Dateitypen der Oberflaeche — bewusst eine EIGENE Tabelle und nicht die
|
|
* aus storage.ts.
|
|
*
|
|
* Dort fehlen html, css und js mit gutem Grund: eine hochgeladene .html wird
|
|
* unter dem Dateihost als application/octet-stream ausgeliefert und damit
|
|
* heruntergeladen statt ausgefuehrt. Stuende html in der geteilten Tabelle,
|
|
* koennte jeder mit einem Upload-Token eine Seite unter fivecdn.d4rkst3r.de
|
|
* veroeffentlichen. Die gebaute Oberflaeche liegt dagegen im Abbild und kommt
|
|
* von uns — hier ist der richtige Typ noetig, sonst laedt der Browser weder
|
|
* Stylesheet noch Modul. */
|
|
const WEB_MIME: Record<string, string> = {
|
|
html: 'text/html; charset=utf-8',
|
|
// webmanifest steht hier und nicht in der geteilten Tabelle: es ist eine
|
|
// Datei der Oberflaeche. Ohne den Eintrag kaeme sie als
|
|
// application/octet-stream, und der Browser wuerde sie verwerfen.
|
|
webmanifest: 'application/manifest+json; charset=utf-8',
|
|
css: 'text/css; charset=utf-8',
|
|
js: 'text/javascript; charset=utf-8',
|
|
mjs: 'text/javascript; charset=utf-8',
|
|
map: 'application/json; charset=utf-8',
|
|
ico: 'image/x-icon',
|
|
woff: 'font/woff',
|
|
woff2: 'font/woff2',
|
|
ttf: 'font/ttf',
|
|
}
|
|
|
|
const webMimeFor = (path: string) =>
|
|
WEB_MIME[path.split('.').pop()?.toLowerCase() ?? ''] ?? mimeFor(path)
|
|
|
|
app.get('*', async (c) => {
|
|
if (c.req.path.startsWith('/api/')) return c.json({ error: 'unbekannt' }, 404)
|
|
|
|
const candidate = c.req.path === '/' ? '/index.html' : c.req.path
|
|
const target = webFile(candidate)
|
|
if (target) {
|
|
try {
|
|
const info = await stat(target)
|
|
if (info.isFile()) {
|
|
return c.body(createReadStream(target) as any, 200, {
|
|
'Content-Type': webMimeFor(candidate),
|
|
'Cache-Control': candidate.includes('/assets/')
|
|
? 'public, max-age=31536000, immutable'
|
|
: 'no-cache',
|
|
})
|
|
}
|
|
} catch {
|
|
/* faellt unten auf index.html */
|
|
}
|
|
}
|
|
|
|
// Erst nachsehen, dann streamen. createReadStream meldet eine fehlende
|
|
// Datei ASYNCHRON ueber ein 'error'-Ereignis — ein try/catch darum faengt
|
|
// nichts. Vorher stand hier genau das, und das Ergebnis war bei ungebauter
|
|
// Oberflaeche ein leerer 200 samt ENOENT im Log statt der Auskunft unten.
|
|
const index = join(webDir, 'index.html')
|
|
try {
|
|
if ((await stat(index)).isFile()) {
|
|
return c.body(createReadStream(index) as any, 200, {
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
'Cache-Control': 'no-cache',
|
|
})
|
|
}
|
|
} catch {
|
|
/* faellt auf die Auskunft unten */
|
|
}
|
|
|
|
return c.text(
|
|
'Die Oberflaeche ist nicht gebaut. Der Dienst laeuft trotzdem — ' +
|
|
'die API steht unter /api.',
|
|
200,
|
|
)
|
|
})
|
|
|
|
// --------------------------------------------------------------------- Start
|
|
|
|
ensureAdmin()
|
|
pruneSessions()
|
|
setInterval(pruneSessions, 3600_000).unref()
|
|
|
|
// UND DIE EREIGNISSE, taeglich. Das stand bisher NUR an einem Wartungsweg
|
|
// (/maintenance/prune-sessions), den nicht einmal die Oberflaeche anbietet --
|
|
// die dokumentierten 180 Tage Aufbewahrung griffen damit nie, und die Tabelle
|
|
// wuchs fuer immer. Nach einem einzigen Tag mit Umzug und Serienlauf standen
|
|
// dort 5355 Zeilen.
|
|
//
|
|
// Nicht stuendlich wie die Sitzungen: ein DELETE ueber eine Zeitspanne von
|
|
// einem halben Jahr hat es nicht eilig, und 24 Stunden Verzoegerung beim
|
|
// Wegraeumen fallen niemandem auf.
|
|
setTimeout(() => {
|
|
pruneEvents()
|
|
pruneVerwaltung()
|
|
}, 120_000).unref()
|
|
setInterval(() => {
|
|
pruneEvents()
|
|
pruneVerwaltung()
|
|
}, 86_400_000).unref()
|
|
|
|
// Der Wachhund fuer die Sicherung. Beim Start mit Verzoegerung (der Dienst
|
|
// soll erst stehen) und danach stuendlich.
|
|
//
|
|
// DAS IST DER PUNKT DER GANZEN MELDUNGSFUNKTION: eine Sicherung, die aufhoert
|
|
// zu laufen, faellt sonst erst auf, wenn man sie braucht.
|
|
setTimeout(() => void pruefeSicherung(), 60_000).unref()
|
|
setInterval(() => void pruefeSicherung(), 3600_000).unref()
|
|
|
|
// Und der Platz auf der Platte. Derselbe Takt und derselbe Grund: eine volle
|
|
// Platte ist der eine Zustand, in dem gleichzeitig nichts mehr hereinkommt und
|
|
// nichts mehr gesichert wird.
|
|
setTimeout(() => void pruefePlatte(), 75_000).unref()
|
|
setInterval(() => void pruefePlatte(), 3600_000).unref()
|
|
|
|
// Die gezaehlten Abrufe wegschreiben. Alle 30 Sekunden reicht: wer wissen
|
|
// will, ob eine Datei benutzt wird, braucht keine Genauigkeit auf die Sekunde.
|
|
setInterval(schreibeAbrufe, 30_000).unref()
|
|
|
|
// Und beim Herunterfahren, damit die letzten nicht verlorengehen.
|
|
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
process.on(signal, () => {
|
|
schreibeAbrufe()
|
|
process.exit(0)
|
|
})
|
|
}
|
|
|
|
// Den Papierkorb ausleeren, was ueberfaellig ist. Beim Start mit Verzoegerung,
|
|
// danach stuendlich.
|
|
setTimeout(() => void papierkorbAusleeren(), 90_000).unref()
|
|
setInterval(() => void papierkorbAusleeren(), 3600_000).unref()
|
|
|
|
serve({ fetch: app.fetch, port: config.port }, (info) => {
|
|
console.log(`[d4rk_media] Port ${info.port}`)
|
|
console.log(
|
|
`[d4rk_media] Dateien oeffentlich unter ${config.publicUrl}${config.filePrefix}/`,
|
|
)
|
|
// Welche Betriebsart laeuft, steht beim Start da und nicht im Quelltext.
|
|
// Wer sich wundert, warum das Dashboard nicht aufgeht, liest hier zuerst.
|
|
console.log(
|
|
config.filesHost
|
|
? `[d4rk_media] Zwei Namen: Dateiwirt hoert auf "${config.filesHost}", Dashboard und API unter jedem anderen`
|
|
: '[d4rk_media] Ein Name: Dashboard und API an der Wurzel, Dateien unter /f/',
|
|
)
|
|
console.log(`[d4rk_media] Daten in ${config.dataDir}`)
|
|
})
|