diff --git a/README.md b/README.md index dced6ac..71318da 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,14 @@ sind ohnehin öffentlich, ein zurückgezogener Link nimmt nur die Übersicht weg Passwort. Das Passwort bleibt als Notausgang. **Sichern** — täglich, geprüft, in die Nextcloud. Und der Dienst meldet nach -Discord, wenn die Sicherung ausbleibt. +Discord, wenn die Sicherung ausbleibt. Zurückspielen lässt sich das Ganze mit +`tools\zurueckspielen.ps1` — und **üben** lässt es sich auch, in einem +Wegwerf-Volume neben dem laufenden Dienst. + +**Beobachten** — `/health` für den Container-Healthcheck (billig, beweist nur, +dass der Prozess antwortet) und `/status` für eine Statusseite: das prüft +Datenbank, Platte, Ausliefern, Bestand, Platz und Sicherung und antwortet mit +**503**, wenn der Dienst sein Geschäft nicht mehr tun kann. --- diff --git a/ROADMAP.md b/ROADMAP.md index 899eaab..614cf75 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -838,6 +838,82 @@ Container an, ersetzt `media.db`, `files/` und `papierkorb/`, **löscht Galerie zeigte die Vorschau einer Datei, die dort nicht mehr liegt), startet wieder und zählt selbst nach. +### ✅ Fertig — /status für die Statusseite, und ein Fehler, den es dabei fand + +**Es gab schon `/health`**, und es bleibt, wie es ist: + +```ts +app.get('/health', (c) => c.json({ ok: true })) +``` + +Diese Zeile beweist genau eines: der Prozess nimmt Anfragen an. Daran hängt der +`HEALTHCHECK` des Containers, und **dort** ist billig richtig — eine schwere +Prüfung, die bei einer langsamen Platte einmal ausfällt, ließe Docker den +Container neu starten, also genau dann, wenn er unter Last steht. + +Für eine Statusseite ist das zu dünn: sie stünde auf Grün, während die Platte +voll ist. `/status` sieht deshalb wirklich nach — Datenbank (echte Abfrage), +Platte (schreiben *und wieder löschen*), Ausliefern (zufällige Datei nachmessen), +Bestand, Platz, Sicherung. **200** wenn der Dienst sein Geschäft tut, **503** +wenn nicht; `?streng=1` lässt auch eine Beeinträchtigung rot werden. + +Öffentlich, aber wortkarg: keine Dateizahlen, Größen, Pfade, Tokennamen oder +Benutzer. Und mit einer 10-Sekunden-Bremse — ein öffentlicher Endpunkt, der auf +die Platte schreibt, wäre sonst ein Verstärker. + +**Beide Wege gemessen**, der weiche am laufenden Dienst (Sicherungszeitpunkt auf +40 Stunden gesetzt und wieder zurück): + +``` +stand: beeintraechtigt · /status -> 200 · /status?streng=1 -> 503 +``` + +#### Und dabei fiel ein Fehler in meinem eigenen Zurückspiel-Skript auf + +Der harte Weg sollte an einem Wegwerf-Container geprüft werden. Der traf +zufällig auf ein **gebrauchtes** Volume — und zeigte: + +| | Zeilen in `media.db` | +|---|---| +| mit den alten `media.db-wal` / `-shm` daneben | **0** | +| dieselbe Datei ohne sie | **4452** | +| Dateien auf der Platte | **4452** | + +`zurueckspielen.ps1` entfernte `media.db`, aber **nicht die beiden +WAL-Begleiter**. Die liegen bei einem echten Zurückspielen immer da — der +laufende Dienst arbeitet im WAL-Modus. SQLite spielt das WAL der *alten* +Datenbank über die *neue*, und heraus kommt der schlimmste denkbare Zustand: +**der Dienst kommt hoch, `/health` ist grün, die Mediathek ist leer** — während +alle Dateien danebenliegen. + +Zwei Konsequenzen: + +1. Die Aufräumzeile steht jetzt an **einer** Stelle und nimmt `media.db-wal`, + `media.db-shm` und `*.tmp` mit. Übung und Ernstfall fahren denselben Befehl — + zwei Fassungen wären zwei, von denen die geübte die harmlosere ist. +2. **Die Übung täuscht jetzt eine bestehende Installation vor**, bevor sie + zurückspielt. In ein leeres Volume zu spielen probt den Fall, der nie + eintritt. + +``` +==> Eine bestehende Installation vortaeuschen + im Volume liegt jetzt: files media.db media.db-shm media.db-wal +==> Zurueckspielen -- mit DEMSELBEN Befehl wie der Ernstfall + 4452 Medieneintraege · 4452 Dateien -- gleich viele + ok items/shushi.png · items/weedbud_1.png · items/cc-castella.png + Die Uebung ist bestanden. +``` + +3. Und `/status` erkennt diesen Zustand jetzt selbst. Am kaputten Container + gemessen: + +``` +/health sagt: HTTP 200 +/status sagt: HTTP 503 kein Eintrag in der Datenbank, aber Dateien auf der Platte +``` + +Genau dafür gibt es die Übung: gefunden beim Üben, nicht im Ernstfall. + ### ⬜ Als Nächstes **Der Vergleichsmaßstab ist weg.** Am 11.08.2026 sind die Lite-Container diff --git a/docs/API.md b/docs/API.md index fe8cb45..1cc94fe 100644 --- a/docs/API.md +++ b/docs/API.md @@ -137,6 +137,69 @@ direkt hierher. --- +## Für eine Statusseite + +Zwei Adressen, beide öffentlich, beide ohne Anmeldung — und sie beantworten +**verschiedene** Fragen. + +``` +GET /health -> 200 {"ok":true} +GET /status -> 200 | 503 mit Begründung +``` + +`/health` beweist genau eines: **der Prozess nimmt Anfragen an.** Daran hängt +auch der `HEALTHCHECK` des Containers, und dort ist eine billige Prüfung +richtig — eine schwere, die bei einer langsamen Platte einmal ausfällt, ließe +Docker den Container neu starten, also genau dann, wenn er unter Last steht. + +`/status` sieht wirklich nach: + +| Prüfung | wie | +|---|---| +| `datenbank` | eine echte Abfrage gegen eine echte Tabelle | +| `platte` | schreiben **und wieder löschen** — der einzige Beweis | +| `ausliefern` | eine zufällige Datei aus der Datenbank auf der Platte nachmessen | +| `bestand` | kein Eintrag, aber Dateien da? Dann stimmt etwas nicht | +| `platz` | ab 95 % belegt | +| `sicherung` | älter als 26 Stunden oder fehlgeschlagen | + +```json +{ "dienst": "d4rk_media", "ok": true, "stand": "gesund", "seit": 4271, + "pruefungen": [ { "was": "datenbank", "ok": true }, … ] } +``` + +**Zwei Stufen, und der Unterschied ist der zwischen „tut es nicht" und „braucht +Aufmerksamkeit".** Datenbank, Platte, Ausliefern und Bestand sind das Geschäft +dieses Dienstes — fällt eines aus, ist er `gestoert` und die Antwort ist +**503**. Eine alte Sicherung oder eine volle Platte machen ihn +`beeintraechtigt`: er liefert weiter aus, die Antwort bleibt **200**. + +Wer auch das rot haben will, hängt `?streng=1` an — dann gibt alles außer +`gesund` eine 503. + +### In Uptime Kuma + +``` +Dienst läuft https://fivemanage.d4rkst3r.de/status +Braucht Pflege https://fivemanage.d4rkst3r.de/status?streng=1 +``` + +> **Warum `bestand` dabei ist.** Nachgemessen an genau diesem Dienst: wird eine +> Sicherung zurückgespielt, ohne `media.db-wal` daneben zu entfernen, spielt +> SQLite das WAL der **alten** Datenbank über die neue. Ergebnis: 0 Einträge, +> während 4452 Dateien danebenliegen. Der Dienst kommt hoch, `/health` ist +> grün, die Mediathek ist leer. +> +> ``` +> /health sagt: HTTP 200 +> /status sagt: HTTP 503 kein Eintrag in der Datenbank, aber Dateien auf der Platte +> ``` + +Was **nicht** hinausgeht: Dateizahlen, Größen, Pfade, Tokennamen, Benutzer. Die +einzige Zahl ist die Laufzeit in Sekunden. + +--- + ## Anmeldung: Token Alle Skript-Wege brauchen einen Token im Kopf: diff --git a/server/src/index.ts b/server/src/index.ts index 5dc6aa5..f335481 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -34,6 +34,7 @@ import { absolutePath, checkPath, lohntWebp, mimeFor, thumbPath, webpPfad } from 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() @@ -208,8 +209,21 @@ app.use('*', async (c, next) => { // ------------------------------------------------------- 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 diff --git a/server/src/routes/status.ts b/server/src/routes/status.ts new file mode 100644 index 0000000..ecf33fc --- /dev/null +++ b/server/src/routes/status.ts @@ -0,0 +1,252 @@ +// Der Endpunkt fuer die Statusseite. +// +// WARUM NICHT /health. Das gibt es und bleibt, wie es ist: +// +// app.get('/health', (c) => c.json({ ok: true })) +// +// Diese Zeile beweist genau eines -- der Prozess nimmt HTTP-Anfragen an. Sie +// haengt am HEALTHCHECK im Dockerfile, und DORT ist eine billige Pruefung +// richtig: eine schwere, die bei einer langsamen Platte einmal ausfaellt, +// liesse Docker den Container neu starten. Der Dienst kippte dann genau dann, +// wenn er unter Last steht. +// +// Fuer eine Statusseite ist sie aber zu duenn. Sie stuende auf Gruen, waehrend +// die Platte voll ist und kein Upload mehr angenommen wird. +// +// HIER WIRD DESHALB WIRKLICH NACHGESEHEN, und zwar an den vier Stellen, an +// denen dieser Dienst scheitern kann: +// +// Datenbank eine echte Abfrage, nicht "die Datei ist da" +// Platte schreiben UND wieder loeschen -- der einzige Beweis +// Ausliefern eine Datei aus der Datenbank suchen und nachmessen +// Sicherung wie alt ist der letzte Lauf +// +// WAS NICHT HINAUSGEHT: Dateizahlen, Groessen, Pfade, Tokennamen, Benutzer. +// Ein oeffentlicher Endpunkt sagt "gesund" oder "krank" -- nicht, wie gross +// der Bestand ist. Die einzige Zahl, die herausgeht, ist die Laufzeit, und die +// steht in jedem Serverkopf ohnehin naeher dran, als es hier stuende. + +import { randomBytes } from 'node:crypto' +import { readdir, rm, stat, statfs, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { Hono } from 'hono' +import { config } from '../config.js' +import { db, now, type Media } from '../db.js' +import { getSetting } from '../settings.js' +import { absolutePath } from '../storage.js' + +export const statusRoutes = new Hono() + +type Pruefung = { was: string; ok: boolean; hinweis?: string } + +/** Wie alt eine Sicherung sein darf, bevor es auffaellt. 26 Stunden -- die + * gleiche Schwelle wie beim Wachhund in meldung.ts, damit nicht zwei Stellen + * verschiedener Meinung darueber sind, was "aktuell" heisst. */ +const SICHERUNG_STUNDEN = 26 + +// -- Nicht bei jeder Anfrage rechnen ----------------------------------------- +// +// Der Endpunkt ist OEFFENTLICH und schreibt auf die Platte. Ohne Bremse waere +// er ein Verstaerker: wer ihn im Sekundentakt abruft, erzeugt im Sekundentakt +// Schreibvorgaenge. Ein Ergebnis von vor zehn Sekunden ist fuer eine +// Statusseite so gut wie ein frisches -- Monitore fragen ohnehin im +// Minutentakt. +const FRISCHE_MS = 10_000 +let letzte: { zeit: number; antwort: Antwort } | null = null + +type Antwort = { + dienst: string + ok: boolean + stand: 'gesund' | 'beeintraechtigt' | 'gestoert' + seit: number + pruefungen: Pruefung[] +} + +async function pruefen(): Promise { + const pruefungen: Pruefung[] = [] + + // -- Datenbank ------------------------------------------------------------ + // + // Eine ECHTE Abfrage gegen eine echte Tabelle. `SELECT 1` bewiese nur, dass + // SQLite antwortet; eine beschaedigte Datei faellt erst auf, wenn jemand + // eine Seite davon liest. + let medien = 0 + try { + medien = (db.prepare('SELECT COUNT(*) AS n FROM media').get() as { n: number }).n + pruefungen.push({ was: 'datenbank', ok: true }) + } catch (err) { + pruefungen.push({ + was: 'datenbank', + ok: false, + hinweis: err instanceof Error ? err.message : 'nicht lesbar', + }) + } + + // -- Platte --------------------------------------------------------------- + // + // SCHREIBEN und wieder loeschen. Ein `stat` auf den Ordner bewiese nur, dass + // es ihn gibt -- eine volle oder schreibgeschuetzte Platte sieht dabei + // genauso aus wie eine gesunde. Die Probe liegt im Datenordner und NICHT + // unter files/: dort wuerde sie der Verwaisten-Sucher finden. + const probe = join(config.dataDir, `.status-${process.pid}-${randomBytes(4).toString('hex')}`) + try { + await writeFile(probe, 'probe') + await rm(probe, { force: true }) + pruefungen.push({ was: 'platte', ok: true }) + } catch (err) { + await rm(probe, { force: true }).catch(() => {}) + pruefungen.push({ + was: 'platte', + ok: false, + hinweis: err instanceof Error ? err.message : 'nicht beschreibbar', + }) + } + + // -- Ausliefern ----------------------------------------------------------- + // + // Die Probe aufs Exempel: eine Datei aus der Datenbank holen und nachsehen, + // ob sie mit der eingetragenen Groesse dort liegt. Damit haengen Datenbank + // UND Platte UND die Pfadaufloesung gemeinsam mit im Test -- das ist der + // Unterschied zwischen "beide Teile leben" und "sie passen zueinander". + // + // Ein leerer Bestand ist kein Fehler: ein frisch aufgesetzter Dienst ist + // gesund und hat nichts auszuliefern. + if (medien > 0) { + try { + const zeile = db + .prepare('SELECT path, size FROM media ORDER BY RANDOM() LIMIT 1') + .get() as Pick + const info = await stat(absolutePath(zeile.path)) + if (info.size === zeile.size) { + pruefungen.push({ was: 'ausliefern', ok: true }) + } else { + pruefungen.push({ + was: 'ausliefern', + ok: false, + hinweis: 'Groesse auf der Platte weicht vom Eintrag ab', + }) + } + } catch { + // Der Pfad steht ABSICHTLICH nicht in der Meldung: das hier ist + // oeffentlich. + pruefungen.push({ + was: 'ausliefern', + ok: false, + hinweis: 'eine eingetragene Datei liegt nicht auf der Platte', + }) + } + } else { + // KEIN EINTRAG, ABER DATEIEN AUF DER PLATTE -- der eine Zustand, in dem + // alles gesund AUSSIEHT und trotzdem alles weg ist. + // + // Nachgemessen, und zwar an genau diesem Dienst: wird eine Sicherung + // zurueckgespielt, ohne media.db-wal daneben zu entfernen, spielt SQLite + // das WAL der ALTEN Datenbank ueber die neue. Ergebnis: 0 Zeilen, + // waehrend 4452 Dateien danebenliegen. Der Dienst kommt hoch, /health + // ist gruen, die Mediathek ist leer. + // + // Ein frisch aufgesetzter Dienst hat weder Eintraege noch Dateien und + // ist voellig gesund. Erst der WIDERSPRUCH aus beidem ist das Zeichen. + try { + const drin = await readdir(config.filesDir) + if (drin.length > 0) { + pruefungen.push({ + was: 'bestand', + ok: false, + hinweis: 'kein Eintrag in der Datenbank, aber Dateien auf der Platte', + }) + } + } catch { + /* kein Dateiordner -- bei einem frischen Dienst normal */ + } + } + + // -- Platz ---------------------------------------------------------------- + // + // Nur als Beeintraechtigung, nicht als Stoerung: bei 95 % laeuft alles + // noch, aber nicht mehr lange. Prozent statt Gigabyte -- die absolute Zahl + // waere eine Auskunft ueber die Maschine. + try { + const s = await statfs(config.dataDir) + const belegt = 1 - (s.bavail * s.bsize) / (s.blocks * s.bsize) + if (belegt >= 0.95) { + pruefungen.push({ + was: 'platz', + ok: false, + hinweis: `${Math.round(belegt * 100)} % belegt`, + }) + } else { + pruefungen.push({ was: 'platz', ok: true }) + } + } catch { + // Auf manchen Dateisystemen gibt es statfs nicht. Kein Grund, den + // Dienst deswegen krank zu nennen. + } + + // -- Sicherung ------------------------------------------------------------ + const roh = getSetting('sicherung.letzte') + if (!roh) { + pruefungen.push({ was: 'sicherung', ok: false, hinweis: 'noch keine gemeldet' }) + } else { + try { + const l = JSON.parse(roh) as { at: number; ok: boolean } + const stunden = (now() - l.at) / 3_600_000 + if (!l.ok) { + pruefungen.push({ was: 'sicherung', ok: false, hinweis: 'letzte fehlgeschlagen' }) + } else if (stunden > SICHERUNG_STUNDEN) { + pruefungen.push({ + was: 'sicherung', + ok: false, + hinweis: `letzte vor ${Math.round(stunden)} Stunden`, + }) + } else { + pruefungen.push({ was: 'sicherung', ok: true }) + } + } catch { + pruefungen.push({ was: 'sicherung', ok: false, hinweis: 'Eintrag unlesbar' }) + } + } + + // -- Das Urteil ----------------------------------------------------------- + // + // ZWEI STUFEN, und der Unterschied ist der zwischen "tut es nicht" und + // "braucht Aufmerksamkeit". Datenbank, Platte und Ausliefern sind das + // Geschaeft dieses Dienstes -- faellt eines aus, ist er gestoert. Eine alte + // Sicherung oder eine volle Platte tun heute noch nicht weh und morgen + // sehr. + const HART = ['datenbank', 'platte', 'ausliefern', 'bestand'] + const gestoert = pruefungen.some((p) => HART.includes(p.was) && !p.ok) + const schwach = pruefungen.some((p) => !p.ok) + + return { + dienst: 'd4rk_media', + ok: !gestoert, + stand: gestoert ? 'gestoert' : schwach ? 'beeintraechtigt' : 'gesund', + // Auf Sekunden gerundet -- eine Statusseite zeigt "seit 3 Tagen" an, + // keine Millisekunden. + seit: Math.round(process.uptime()), + pruefungen, + } +} + +/** Der Endpunkt. + * + * `?streng=1` laesst auch eine Beeintraechtigung mit 503 antworten. Ohne den + * Schalter ist eine alte Sicherung kein "down" -- der Dienst liefert ja aus. + * WELCHES von beiden richtig ist, weiss nur, wer die Statusseite betreibt: + * deshalb ein Schalter und keine Entscheidung meinerseits. */ +statusRoutes.get('/', async (c) => { + if (!letzte || now() - letzte.zeit > FRISCHE_MS) { + letzte = { zeit: now(), antwort: await pruefen() } + } + const a = letzte.antwort + const streng = c.req.query('streng') === '1' + const schlecht = streng ? a.stand !== 'gesund' : !a.ok + + return c.json(a, schlecht ? 503 : 200, { + // Eine zwischengespeicherte Statusmeldung ist keine. Der Proxy greift + // hier zwar nicht zu (die Adresse endet auf keine Bildendung), aber das + // ist eine Einstellung und keine Zusicherung. + 'Cache-Control': 'no-store', + }) +}) diff --git a/tools/zurueckspielen.ps1 b/tools/zurueckspielen.ps1 index 6e0cd9c..1c15776 100644 --- a/tools/zurueckspielen.ps1 +++ b/tools/zurueckspielen.ps1 @@ -35,6 +35,32 @@ param( $ErrorActionPreference = 'Stop' +# WAS BEIM ZURUECKSPIELEN WEG MUSS -- an EINER Stelle, weil die Uebung genau +# denselben Befehl fahren soll wie der Ernstfall. Zwei Fassungen waeren zwei, +# von denen die geuebte die harmlosere ist. +# +# media.db-wal UND media.db-shm STEHEN HIER MIT ABSICHT, und das ist teuer +# gelernt: SQLite laeuft in diesem Dienst im WAL-Modus, neben der Datenbank +# liegen also immer zwei Begleitdateien. Die Sicherung enthaelt sie NICHT -- +# sie braucht sie auch nicht, `db.backup()` schreibt eine fertig +# zusammengefuehrte Datei. +# +# Wer beim Zurueckspielen nur media.db ersetzt, laesst damit das WAL der ALTEN +# Datenbank neben der NEUEN liegen. SQLite spielt es beim Oeffnen ein, und das +# Ergebnis ist das Schlimmste, was hier passieren kann: +# +# media.db aus dem Archiv, mit altem WAL daneben : 0 Zeilen +# dieselbe Datei ohne die beiden Begleiter : 4452 Zeilen +# Dateien auf der Platte : 4452 +# +# Der Dienst kaeme HOCH, /health waere gruen, und die Mediathek waere leer -- +# waehrend alle Dateien danebenliegen. Nachgemessen am 12.08.2026, gefunden +# beim Ueben und nicht im Ernstfall. Genau dafuer gibt es die Uebung. +# +# .tmp ebenfalls: writeFileAtomic legt Zwischendateien an, und ein Absturz +# mitten im Schreiben laesst eine liegen. +$script:AUFRAEUMEN = 'rm -rf files media.db media.db-wal media.db-shm papierkorb thumbs webp *.tmp' + function Schritt($t) { Write-Host "==> $t" -ForegroundColor Cyan } function Gut($t) { Write-Host " $t" -ForegroundColor Green } function Schlecht($t) { Write-Host " $t" -ForegroundColor Red } @@ -103,12 +129,37 @@ if (-not $Echt) { docker volume rm $pVolume 2>&1 | Out-Null try { - Schritt 'Wegwerf-Volume anlegen und Archiv hineinspielen' + Schritt 'Wegwerf-Volume anlegen' docker volume create $pVolume | Out-Null + + # ------------------------------------------------------------------ + # ERST EINE BESTEHENDE INSTALLATION VORTAEUSCHEN, und das ist der + # Kern der Uebung: im Ernstfall wird ueber etwas zurueckgespielt, das + # schon da ist -- mit einer laufenden Datenbank, mit media.db-wal + # daneben, mit Vorschauen. In ein LEERES Volume zu spielen probt genau + # den Fall, der nie eintritt. + # + # Dass das ein Unterschied ist, ist nicht theoretisch: die erste + # Fassung dieses Skripts liess media.db-wal stehen, und der Dienst kam + # danach mit einer LEEREN Mediathek hoch, waehrend alle Dateien + # danebenlagen. Aufgefallen ist es nur, weil ein Uebungslauf zufaellig + # auf ein gebrauchtes Volume traf. + Schritt 'Eine bestehende Installation vortaeuschen (damit die Uebung eine ist)' + docker run -d --name "$pContainer-alt" ` + -e PUBLIC_URL=http://probe.invalid -e ADMIN_PASSWORD=nur-fuer-die-uebung ` + -v "${pVolume}:/data" $Abbild | Out-Null + Start-Sleep -Seconds 4 + docker stop "$pContainer-alt" 2>&1 | Out-Null + docker rm "$pContainer-alt" 2>&1 | Out-Null + $begleiter = docker run --rm -v "${pVolume}:/data" --entrypoint sh $Abbild ` + -c "ls /data | tr '\n' ' '" + Gut "im Volume liegt jetzt: $($begleiter.Trim())" + + Schritt 'Zurueckspielen -- mit DEMSELBEN Befehl wie der Ernstfall' docker run --rm -v "${pVolume}:/data" -v "${archivOrdner}:/in:ro" ` - --entrypoint sh $Abbild -c "cd /data && tar xzf '/in/$archivDatei' && chown -R node:node /data" + --entrypoint sh $Abbild -c "cd /data && $script:AUFRAEUMEN && tar xzf '/in/$archivDatei' && chown -R node:node /data" if ($LASTEXITCODE -ne 0) { throw 'Auspacken fehlgeschlagen' } - Gut 'ausgepackt' + Gut 'eingespielt' Schritt 'Einen zweiten Dienst davorhaengen (ohne Port nach aussen)' # KEIN veroeffentlichter Port: der Uebungslauf soll sich mit nichts @@ -201,6 +252,7 @@ const zeilen = db.prepare('SELECT path, sha256, size FROM media ORDER BY RANDOM( # Aufraeumen IMMER -- auch nach einem Abbruch. Ein liegengebliebenes # Uebungs-Volume mit 300 MB findet sonst niemand wieder. Schritt 'Aufraeumen' + docker rm -f "$pContainer-alt" 2>&1 | Out-Null docker rm -f $pContainer 2>&1 | Out-Null docker volume rm $pVolume 2>&1 | Out-Null Gut 'Wegwerf-Volume und Probe-Container entfernt' @@ -241,7 +293,7 @@ try { # In EINEM Container-Aufruf: zwischen Loeschen und Einspielen darf nichts # liegen, in dem jemand den Dienst wieder startet. docker run --rm -v "${Volume}:/data" -v "${archivOrdner}:/in:ro" --entrypoint sh $Abbild ` - -c "cd /data && rm -rf files media.db papierkorb thumbs webp && tar xzf '/in/$archivDatei' && chown -R node:node /data" + -c "cd /data && $script:AUFRAEUMEN && tar xzf '/in/$archivDatei' && chown -R node:node /data" if ($LASTEXITCODE -ne 0) { throw 'Einspielen fehlgeschlagen' } Gut 'eingespielt' } catch {