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
+60 -3
View File
@@ -1,4 +1,15 @@
import { describe, expect, it, vi } from 'vitest'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { chmod, mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
/**
* Die Statusanzeige ist die einzige Stelle, an der ein stiller Ausfall
* auffaellt, bevor jemand darueber stolpert. Beide Faelle, die es hier je
* wirklich gab, stehen deshalb als Pruefung da: ein Virenscanner, der nicht
* antwortet, und eine Dateiablage, in die der Dienst nicht schreiben darf.
*/
let ablage = ''
vi.mock('@/lib/db', () => ({
db: {
@@ -8,19 +19,65 @@ vi.mock('@/lib/db', () => ({
}))
vi.mock('@/lib/env', () => ({
getEnv: () => ({ CLAMAV_ADDRESS: 'clamav:3310' }),
getEnv: () => ({ CLAMAV_ADDRESS: 'clamav:3310', FILES_DIR: ablage }),
}))
vi.mock('@/lib/files/clamav', () => ({ pingClamav: vi.fn(async () => true) }))
describe('collectHealth', () => {
it('meldet betriebsbereit, wenn alle drei Abhaengigkeiten antworten', async () => {
beforeAll(async () => {
ablage = await mkdtemp(path.join(tmpdir(), 'afis-ablage-'))
})
afterAll(async () => {
await chmod(ablage, 0o755).catch(() => undefined)
await rm(ablage, { recursive: true, force: true })
})
it('meldet betriebsbereit, wenn alle Abhaengigkeiten antworten', async () => {
const { collectHealth } = await import('@/lib/health/check')
const report = await collectHealth()
expect(report.status).toBe('up')
expect(report.components.database.status).toBe('up')
expect(report.components.discordGateway.status).toBe('up')
expect(report.components.clamav.status).toBe('up')
expect(report.components.fileStore.status).toBe('up')
})
it('legt die beiden Verzeichnisse an, wenn sie fehlen', async () => {
// Eine frische Ablage ist leer. Das ist kein Fehler, sondern der
// Normalfall beim ersten Start.
const { collectHealth } = await import('@/lib/health/check')
const report = await collectHealth()
expect(report.components.fileStore.status).toBe('up')
})
it('meldet eine Ablage, in die der Dienst nicht schreiben darf', async () => {
// Genau der Fehler, der jeden Upload scheitern liess: das Volume gehoerte
// root, der Dienst laeuft als afis. Zu sehen war davon nichts.
//
// Nachgestellt wird ein **leeres**, gesperrtes Verzeichnis - so lag es
// auch im Container. Ein Verzeichnis, in dem quarantine/ und store/ schon
// stehen und beschreibbar sind, ist der Fall nicht: dann arbeitet der
// Dienst weiter, und die Anzeige soll ihn auch nicht anschwaerzen.
const vorher = ablage
ablage = await mkdtemp(path.join(tmpdir(), 'afis-gesperrt-'))
await chmod(ablage, 0o555)
try {
const { collectHealth } = await import('@/lib/health/check')
const report = await collectHealth()
expect(report.components.fileStore.status).toBe('down')
expect(report.components.fileStore.detail).toContain('nicht beschreibbar')
// Die Datenbank steht, also faellt nicht alles aus - aber betriebsbereit
// ist es auch nicht.
expect(report.status).toBe('degraded')
} finally {
await chmod(ablage, 0o755)
await rm(ablage, { recursive: true, force: true })
ablage = vorher
}
})
it('bleibt nur eingeschraenkt, wenn allein die Virenpruefung ausfaellt', async () => {