Dateiablage im Image anlegen, sonst schlaegt jeder Upload fehl
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s

Das Volume `files` wird auf /data/files gelegt. Diesen Pfad gab es im Image
nicht, also legte Docker ihn beim Einhaengen selbst an - als root mit 755.
Der Dienst laeuft aber als afis (1001) und bekam beim ersten Schreibversuch
EACCES. Betroffen war jeder Upload: Anhaenge, Lichtbilder, und auch die
fertigen PDF-Abzuege, die in derselben Ablage landen.

Nachgemessen statt vermutet: ein leeres Volume uebernimmt Verzeichnis und
Eigentuemer aus dem Image, ein bereits befuelltes behaelt seine alten
Rechte. Da bisher nie ein Upload durchkam, ist die Ablage leer - ein Neubau
genuegt, das Volume muss nicht weg.

Dazu die Ablage in die Statusanzeige. Sie war der zweite stille Ausfall
dieser Art nach clamd: ein Zustand, den man nur durch Ausprobieren erfaehrt,
gehoert dorthin, wo man ihn sieht. Geprueft wird das Schreibrecht, nicht
durch eine Probedatei - ein Healthcheck, der im Sekundentakt schreibt und
aufraeumt, waere selbst ein Risiko.

Die Pruefung dazu stellt den echten Fall nach, ein leeres gesperrtes
Verzeichnis. Mein erster Versuch sperrte nur das Elternverzeichnis, waehrend
quarantine/ und store/ darin schon standen und beschreibbar blieben - der
Dienst haette weitergearbeitet, und die Anzeige haette zu Recht nichts
gemeldet.
This commit is contained in:
Harlekin
2026-09-04 16:04:40 +02:00
parent 6b2b4c4c8c
commit ad3bf48d2c
4 changed files with 123 additions and 8 deletions
+45 -5
View File
@@ -1,13 +1,17 @@
import { constants } from 'node:fs'
import { access, mkdir } from 'node:fs/promises'
import path from 'node:path'
import { db } from '@/lib/db'
import { getEnv } from '@/lib/env'
import { pingClamav } from '@/lib/files/clamav'
import { QUARANTINE, STORE } from '@/lib/files/paths'
export type ComponentState = 'up' | 'degraded' | 'down'
export type HealthReport = {
status: ComponentState
checkedAt: string
components: Record<'database' | 'discordGateway' | 'clamav', {
components: Record<'database' | 'discordGateway' | 'clamav' | 'fileStore', {
status: ComponentState
detail?: string
}>
@@ -49,29 +53,65 @@ async function checkClamav(): Promise<{ status: ComponentState; detail?: string
}
/**
* Sammelt den Zustand der drei Abhaengigkeiten. Die Gesamtbewertung faellt
* Ist die Dateiablage beschreibbar?
*
* Steht hier, weil genau das einmal unbemerkt kaputt war: das Volume wurde auf
* einen Pfad gelegt, den es im Image nicht gab, Docker legte ihn als root an,
* und der Dienst laeuft als afis. Jeder Upload scheiterte, und zu sehen war
* davon nichts - bis jemand eine Datei anhaengen wollte. Ein Zustand, den man
* nur durch Ausprobieren erfaehrt, gehoert in die Statusanzeige.
*
* Geprueft wird das Recht, nicht durch eine Probedatei: ein Healthcheck, der
* im Sekundentakt schreibt und aufraeumt, ist selbst ein Risiko.
*/
async function checkFileStore(): Promise<{ status: ComponentState; detail?: string }> {
const wurzel = path.resolve(getEnv().FILES_DIR)
try {
for (const bereich of [QUARANTINE, STORE]) {
const verzeichnis = path.join(wurzel, bereich)
// Anlegen, falls es fehlt - das ist der Normalfall bei einer frischen
// Ablage und kein Fehler.
await mkdir(verzeichnis, { recursive: true })
await access(verzeichnis, constants.W_OK | constants.X_OK)
}
return { status: 'up' }
} catch (error) {
const grund = error instanceof Error && 'code' in error ? String(error.code) : 'unbekannt'
return {
status: 'down',
detail:
grund === 'EACCES'
? `${wurzel} ist fuer den Dienst nicht beschreibbar`
: `${wurzel}: ${grund}`,
}
}
}
/**
* Sammelt den Zustand der Abhaengigkeiten. Die Gesamtbewertung faellt
* bewusst nur auf `down`, wenn die Datenbank weg ist: Worker, clamav und pdf
* haben eigene Healthchecks im Stack, und ein ausgefallener Virenscanner soll
* nicht den ganzen Web-Container als tot markieren. Sichtbar bleibt der Ausfall
* trotzdem, in dieser Antwort und in Dockge am jeweiligen Dienst.
*/
export async function collectHealth(): Promise<HealthReport> {
const [database, discordGateway, clamav] = await Promise.all([
const [database, discordGateway, clamav, fileStore] = await Promise.all([
checkDatabase(),
checkDiscordGateway(),
checkClamav(),
checkFileStore(),
])
const status: ComponentState =
database.status === 'down'
? 'down'
: discordGateway.status === 'up' && clamav.status === 'up'
: discordGateway.status === 'up' && clamav.status === 'up' && fileStore.status === 'up'
? 'up'
: 'degraded'
return {
status,
checkedAt: new Date().toISOString(),
components: { database, discordGateway, clamav },
components: { database, discordGateway, clamav, fileStore },
}
}
+1
View File
@@ -109,6 +109,7 @@ export const healthText = {
database: 'Datenbank',
discordGateway: 'Discord-Gateway',
clamav: 'Virenprüfung',
fileStore: 'Dateiablage',
} as Record<string, string>,
} as const