diff --git a/tools/sicherung-holen.sh b/tools/sicherung-holen.sh index 3d6438eb..358f441e 100644 --- a/tools/sicherung-holen.sh +++ b/tools/sicherung-holen.sh @@ -41,12 +41,29 @@ scp -q dogfather-server:/home/dogiweb/workspace-daten/sicherungen/*.db "$ZIEL/" # # scp loescht nie etwas. Wer auf dem Server versehentlich ein PDF # entfernt, hat es hier weiterhin -- das ist Absicht, nicht Nachlaessigkeit. -echo "Hole hochgeladene Dateien und die Bibliothek ..." -mkdir -p "$ZIEL/dateien" "$ZIEL/wissen" -scp -qr dogfather-server:/home/dogiweb/workspace-daten/dateien/. "$ZIEL/dateien/" 2>/dev/null || true -scp -qr dogfather-server:/home/dogiweb/workspace-daten/wissen/. "$ZIEL/wissen/" 2>/dev/null || true +# PROFILBILDER GEHOEREN DAZU (ergaenzt 06.09.2026). +# +# Hier standen nur dateien/ und wissen/. Die Profilbilder liegen aber in +# einem dritten Ordner (workspace-steckbrief.js: profilbilder/) -- und +# der wurde nie mitgeholt. Nach einem Plattenausfall waeren alle Bilder +# weg gewesen, obwohl "die Sicherung" jeden Tag gruen gemeldet hat. +# +# Gefunden hat das nicht das Nachdenken, sondern die Probe: Beim ersten +# Lauf von tools/wiederherstellung-proben.mjs blieb genau eine von +# achtzehn Seiten rot -- der Steckbrief, mit einem 404 fuer ein Bild. +# Genau dafuer gibt es sie. +# +# Wer hier einen Ordner ergaenzt, traegt ihn auch in ORDNER unten ein; +# die Probe vergleicht die Liste gegen die Ordner, die der Server +# wirklich benutzt, und schlaegt bei einem vergessenen an. +echo "Hole hochgeladene Dateien, Bilder und die Bibliothek ..." +ORDNER="dateien wissen profilbilder" +for o in $ORDNER; do + mkdir -p "$ZIEL/$o" + scp -qr "dogfather-server:/home/dogiweb/workspace-daten/$o/." "$ZIEL/$o/" 2>/dev/null || true +done -DATEIEN=$(find "$ZIEL/dateien" "$ZIEL/wissen" -type f 2>/dev/null | wc -l | tr -d ' ') +DATEIEN=$(find $(for o in $ORDNER; do echo "$ZIEL/$o"; done) -type f 2>/dev/null | wc -l | tr -d ' ') echo " $DATEIEN Datei(en) aus Ablage und Bibliothek" # Geprueft wird mit Node, nicht mit dem sqlite3-Programm: Node bringt @@ -98,9 +115,19 @@ if [ -f "$SCHLUESSEL_DATEI" ]; then -H "Content-Type: application/json" \ -H "X-Kopie-Schluessel: $SCHLUESSEL" \ -d "{\"dateien\":$DATEIEN,\"sicherungen\":$ANZAHL,\"bytes\":$BYTES,\"rechner\":\"$(hostname)\"}" \ - https://dogfather-universe.com/workspace/api/zustand/kopie || echo "000") + https://workspace.dogfather-universe.com/workspace/api/zustand/kopie || echo "000") if [ "$ANTWORT" = "200" ]; then echo " Dem Server zurueckgemeldet." + elif [ "$ANTWORT" = "410" ]; then + # 410 heisst hier etwas Bestimmtes: Die ADRESSE stimmt nicht mehr. + # Genau das war am 06.09.2026 der Fall -- der Workspace ist auf + # workspace.dogfather-universe.com umgezogen, die alte Adresse + # antwortet seither mit 410 "Gone". Die Rueckmeldung lief seitdem + # ins Leere, und auf der Automationen-Seite waere die Kopie mit + # jedem Tag aelter erschienen, obwohl sie taeglich lief. + echo " FEHLER: Der Server sagt 410 -- die Adresse in diesem Skript ist veraltet." + echo " Aktuell: https://workspace.dogfather-universe.com" + notiere "Rueckmeldung 410: Adresse veraltet, Kopie selbst ist in Ordnung" else echo " Hinweis: Rueckmeldung an den Server ging nicht (HTTP $ANTWORT)." notiere "Rueckmeldung fehlgeschlagen (HTTP $ANTWORT), Kopie selbst ist in Ordnung" diff --git a/tools/wiederherstellung-proben.mjs b/tools/wiederherstellung-proben.mjs index af7b9258..c35309f4 100644 --- a/tools/wiederherstellung-proben.mjs +++ b/tools/wiederherstellung-proben.mjs @@ -56,7 +56,7 @@ AUFRUF: node tools/wiederherstellung-proben.mjs ===================================================================== */ -import { readdirSync, statSync, copyFileSync, mkdtempSync, rmSync, writeFileSync, existsSync } from "node:fs"; +import { readdirSync, statSync, copyFileSync, mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs"; import { tmpdir, homedir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -134,6 +134,89 @@ const vorherGroesse = statSync(kopie).size; ok(vorherGroesse === statSync(gewaehlt.pfad).size, `die Kopie ist vollstaendig (${vorherGroesse} Byte)`); +/* --------------------------------------------------------------------- + 2b. UND DIE DATEIEN. Eine Datenbank allein ist keine Wiederherstellung. + + Beim ersten Lauf dieser Probe (06.09.2026) blieb genau eine von + achtzehn Seiten rot: der Steckbrief, mit einem 404 in der Konsole. Das + war kein Fehler der Sicherung, sondern ihre wichtigste Aussage -- + das Profilbild lag NICHT in der Datenbank, sondern daneben im + Dateisystem. Wer nur die .db zurueckspielt, bekommt eine Anwendung, + die laeuft, und eine Bibliothek voller Verweise auf Dateien, die es + nicht mehr gibt. + + sicherung-holen.sh holt diese Ordner mit (dateien/, wissen/). Genau + deshalb gehoeren sie auch in die Probe: Sonst beweist sie die + Wiederherstellung nur zur Haelfte -- und ausgerechnet die Haelfte, + die man im Ernstfall nicht nachbauen kann. */ +function ordnerKopieren(von, nach) { + if (!existsSync(von)) return 0; + mkdirSync(nach, { recursive: true }); + let n = 0; + for (const e of readdirSync(von, { withFileTypes: true })) { + const q = join(von, e.name), z = join(nach, e.name); + if (e.isDirectory()) n += ordnerKopieren(q, z); + else { copyFileSync(q, z); n++; } + } + return n; +} +/* Kopiert wird ALLES, was in der Sicherung als Ordner liegt -- nicht + eine Liste von Hand. Kommt morgen ein vierter Datenordner dazu, ist + er hier von selbst dabei. */ +let dateienZahl = 0; +const gesichert = readdirSync(SICHERUNGEN, { withFileTypes: true }) + .filter((e) => e.isDirectory()).map((e) => e.name); +for (const o of gesichert) { + dateienZahl += ordnerKopieren(join(SICHERUNGEN, o), join(arbeit, o)); +} +ok(dateienZahl > 0, + dateienZahl ? `${dateienZahl} hochgeladene Dateien mit zurueckgespielt ` + + `(${gesichert.join(", ")})` + : "KEINE Dateien in der Sicherung -- Bilder und PDFs waeren nach einem " + + "Plattenausfall weg, auch wenn die Datenbank vollstaendig ist"); + +/* --------------------------------------------------------------------- + DIE ENTSCHEIDENDE FRAGE: Sichert das Skript ALLE Ordner, die der + Server ueberhaupt benutzt? + + Genau hier lag die Luecke. sicherung-holen.sh nannte dateien/ und + wissen/; die Profilbilder liegen in einem dritten Ordner und wurden + nie mitgeholt. Die Sicherung meldete trotzdem jeden Tag "in Ordnung" + -- sie hat ja nie behauptet, vollstaendig zu sein. + + Eine Liste von Hand kann das nicht verhindern; sie altert genau so + still wie die, die den Fehler verursacht hat. Deshalb wird hier + NACHGEZAEHLT: Welche Ordner legt der Server an (aus dem Quelltext), + und welche holt das Skript? Was fehlt, wird gemeldet. */ +{ + const server = join(WURZEL, "server"); + const benutzt = new Set(); + for (const datei of readdirSync(server).filter((n) => /^workspace.*\.js$/.test(n))) { + const text = (await import("node:fs")).readFileSync(join(server, datei), "utf8"); + for (const t of text.matchAll(/join\(DATEN_ORDNER,\s*"([^"]+)"\)/g)) benutzt.add(t[1]); + } + /* sicherungen/ ist die Sicherung selbst -- die holt das Skript als + .db-Dateien und nicht als Ordner. */ + benutzt.delete("sicherungen"); + + const skript = (await import("node:fs")).readFileSync(join(HIER, "sicherung-holen.sh"), "utf8"); + const geholt = (skript.match(/^ORDNER="([^"]+)"/m) || [])[1]?.split(/\s+/) || []; + + ok(benutzt.size > 0, `der Server benutzt ${benutzt.size} Datenordner: ${[...benutzt].join(", ")}`); + const fehlend = [...benutzt].filter((o) => !geholt.includes(o)); + ok(fehlend.length === 0, + fehlend.length + ? `NICHT GESICHERT: ${fehlend.join(", ")} -- diese Ordner legt der Server an, ` + + `tools/sicherung-holen.sh holt sie aber nicht. Nach einem Plattenausfall ` + + `waeren sie weg, ohne dass die taegliche Meldung je etwas gesagt haette.` + : `alle ${benutzt.size} holt tools/sicherung-holen.sh (${geholt.join(", ")})`); + + /* GEGENPROBE: Erkennt der Vergleich einen fehlenden Ordner ueberhaupt? + Ohne sie bewiese "nichts fehlt" nur, dass die Suche nichts findet. */ + ok([...benutzt, "erfundenerordner"].filter((o) => !geholt.includes(o)).length === 1, + "Gegenprobe: ein erfundener Ordner WIRD als fehlend erkannt"); +} + /* --------------------------------------------------------------------- 3. Zaehlen, was drin ist -- VOR der Wanderung --------------------------------------------------------------------- */