/** * Bestehende Dateien nachtraeglich in den Umschlag stecken. * * npm run speicher:verschluesseln # nur schauen * npm run speicher:verschluesseln -- --anwenden * * Warum es diesen Lauf gibt: Der Objektspeicher-Treiber hat bis v1.10.2 * unverschluesselt abgelegt - es stand nur ServerSideEncryption: 'AES256' * darauf, also Verschluesselung mit dem Schluessel des Anbieters. Seit * v1.11.0 liegt derselbe Umschlag darum wie beim Dateitreiber. Neue Dateien * bekommen ihn beim Ablegen; die alten liegen weiter im Klartext, bis sie * hier einmal umgeschrieben werden. * * Lesen funktioniert waehrenddessen durchgehend: Der Treiber erkennt am * Kopf, ob ein Umschlag da ist, und reicht alles andere unveraendert durch. * Der Lauf laesst sich also jederzeit starten, abbrechen und wiederholen. * * Er ist absichtlich langsam und einfach: ein Objekt nach dem anderen, * lesen, pruefen, zurueckschreiben. Bei ein paar hundert Dokumenten dauert * das Sekunden, und ein Fehler betrifft immer nur eine Datei. * * OHNE --anwenden wird nichts geschrieben. */ import { createRequire } from 'node:module' import { readFile, writeFile } from 'node:fs/promises' /** * "server-only" fuer diesen Lauf neutralisieren. * * Das Paket wirft beim Import ausserhalb einer Server-Umgebung - genau * dafuer ist es da. Hier laufen wir aber bewusst ausserhalb von Next, und * die Alternative waere, den Umschlag im Skript noch einmal zu schreiben. * Zwei Fassungen derselben Krypto sind die schlechtere Wahl: Wenn eine * davon abweicht, merkt es niemand, bis eine Datei nicht mehr aufgeht. * * Deshalb wird der Modullader vorher umgebogen und danach dynamisch * importiert - statische Importe wuerden vorgezogen und liefen in den * Fehler. */ const verlangen = createRequire(import.meta.url) const modulLader = verlangen('node:module') as { _load: (anfrage: string, ...rest: unknown[]) => unknown } const echterLader = modulLader._load modulLader._load = function (anfrage: string, ...rest: unknown[]) { if (anfrage === 'server-only') return {} return echterLader.call(this, anfrage, ...rest) } const { env } = await import('../src/lib/env') const { istVerschluesselt, verschluesselnBinaer } = await import('../src/lib/speicher/umschlag') const anwenden = process.argv.includes('--anwenden') type Bilanz = { geprueft: number; umgeschrieben: number; schonGut: number; fehler: number } function zeile(text = ''): void { console.log(text) } /** Objektspeicher: auflisten, lesen, bei Bedarf zurueckschreiben. */ async function s3Durchgehen(): Promise { const s3 = await import('../src/lib/speicher/s3') const bilanz: Bilanz = { geprueft: 0, umgeschrieben: 0, schonGut: 0, fehler: 0 } const schluessel = await s3.objekteAuflisten() zeile(` ${schluessel.length} Objekte im Eimer "${env.s3.bucket}".`) zeile() for (const eintrag of schluessel) { bilanz.geprueft += 1 try { const roh = await s3.rohLesen(eintrag) if (istVerschluesselt(roh)) { bilanz.schonGut += 1 continue } if (anwenden) { // schreiben() legt den Umschlag selbst an - der Inhalt geht also // als Klartext hinein und kommt verschluesselt im Eimer an. await s3.schreiben(eintrag, roh, 'application/octet-stream') } bilanz.umgeschrieben += 1 zeile(` ${anwenden ? 'umgeschrieben' : 'waere umzuschreiben'}: ${eintrag}`) } catch (fehler) { bilanz.fehler += 1 zeile(` FEHLER bei ${eintrag}: ${fehler instanceof Error ? fehler.message : fehler}`) } } return bilanz } /** Dateitreiber: dasselbe fuer das Ablageverzeichnis. */ async function dateienDurchgehen(): Promise { const { readdir, stat } = await import('node:fs/promises') const { join, relative, sep } = await import('node:path') const treiber = await import('../src/lib/speicher/datei') const bilanz: Bilanz = { geprueft: 0, umgeschrieben: 0, schonGut: 0, fehler: 0 } const wurzel = treiber.ablageverzeichnis() zeile(` Verzeichnis ${wurzel}`) zeile() async function absteigen(verzeichnis: string): Promise { let eintraege: string[] try { eintraege = await readdir(verzeichnis) } catch { return // gibt es noch nicht } for (const name of eintraege) { const pfad = join(verzeichnis, name) if ((await stat(pfad)).isDirectory()) { await absteigen(pfad) continue } bilanz.geprueft += 1 try { const roh = await readFile(pfad) if (istVerschluesselt(roh)) { bilanz.schonGut += 1 continue } // Der Ablageschluessel ist der Pfad unterhalb der Wurzel, mit // Schraegstrichen - genau das, was der Treiber beim Schreiben // benutzt. Er gehoert in die Zusatzdaten, sonst waere die Datei // hinterher zwar verschluesselt, aber nicht an ihren Platz // gebunden, und liesse sich unter fremdem Namen wieder einspielen. const ablageschluessel = relative(wurzel, pfad).split(sep).join('/') if (anwenden) { await writeFile(pfad, verschluesselnBinaer(roh, ablageschluessel), { mode: 0o600 }) } bilanz.umgeschrieben += 1 zeile(` ${anwenden ? 'umgeschrieben' : 'waere umzuschreiben'}: ${pfad}`) } catch (fehler) { bilanz.fehler += 1 zeile(` FEHLER bei ${pfad}: ${fehler instanceof Error ? fehler.message : fehler}`) } } } await absteigen(wurzel) return bilanz } async function main(): Promise { zeile() zeile(' Bestehende Dateien in den Umschlag stecken') zeile(' ' + '-'.repeat(50)) zeile() if (!anwenden) { zeile(' TROCKENLAUF - es wird nichts geschrieben.') zeile(' Zum Anwenden: npm run speicher:verschluesseln -- --anwenden') zeile() } const treiber = env.speicherTreiber zeile(` Treiber: ${treiber}`) zeile() const bilanz = treiber === 's3' ? await s3Durchgehen() : await dateienDurchgehen() zeile() zeile(` Geprueft: ${bilanz.geprueft}`) zeile(` Schon im Umschlag: ${bilanz.schonGut}`) zeile(` ${anwenden ? 'Umgeschrieben' : 'Umzuschreiben'}: ${bilanz.umgeschrieben}`) if (bilanz.fehler > 0) zeile(` Fehler: ${bilanz.fehler}`) zeile() if (bilanz.fehler > 0) process.exitCode = 1 } main().catch((fehler) => { console.error(fehler) process.exitCode = 1 })