Files
DogFatherGitandClaude Opus 5 46563b02fc Waechter merkt jetzt, wenn die naechtliche Sicherung ausfaellt
Beim Nachweis des ersten automatischen Laufs gefunden: Der Cron-Eintrag
endet auf >/dev/null 2>&1 -- jede Fehlermeldung wird verworfen. Das
Sicherungsskript fuehrt zwar ein eigenes Protokoll, aber alles, was VOR
der ersten Protokollzeile schiefgeht (Skript geloescht, sqlite3 weg,
Platte voll, Cron gestoppt), passiert spurlos. Niemand haette es
gemerkt -- ausser in dem Moment, in dem man die Sicherung braucht.

Der Waechter prueft ab sofort die Datei selbst, nicht das Protokoll:
Ein Protokoll kann "erfolgreich" melden, waehrend die Datei fehlt.

In lib/ ausgelagert, weil waechter.mjs beim Import sofort seinen ganzen
Durchlauf startet -- testbar war die Funktion dort nicht.

Beim Testentwurf einen eigenen Fehler gefunden: readdirSync wirft
sowohl bei fehlendem Ordner (ENOENT) als auch bei fehlenden Rechten
(EACCES). Die erste Fassung behandelte beides als "kein Urteil" und
haette damit einen geloeschten Sicherungsordner verschwiegen. Jetzt
getrennt: ENOENT meldet, EACCES schweigt.

Die 26-Stunden-Grenze ist bewusst nicht enger: Kurz vor dem naechsten
Lauf ist die juengste Sicherung regulaer 24 Stunden alt. Genau dort
entstehen die Fehlalarme, nach denen man die Meldungen abschaltet --
und dann geht der eine echte mit unter. Als eigener Testfall abgesichert.

14 von 14 Pruefungen bestanden.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-27 03:02:55 +02:00

111 lines
4.4 KiB
JavaScript

/* =====================================================================
Ist die naechtliche Sicherung wirklich gelaufen?
Eigene Datei, damit sie sich testen laesst: waechter.mjs startet beim
Import sofort seinen kompletten Durchlauf (Dienste, Seiten, Push) --
ein Test koennte die Funktion dort nicht anfassen, ohne den ganzen
Waechter auszuloesen.
ANLASS (27.08.2026)
Der Cron-Eintrag endet auf >/dev/null 2>&1 -- jede Fehlermeldung wird
verworfen. Das Sicherungsskript fuehrt zwar ein eigenes Protokoll,
aber alles, was VOR der ersten Protokollzeile schiefgeht (Skript
geloescht, sqlite3 weg, Platte voll, Cron gestoppt), passiert
spurlos.
Eine Sicherung, die still ausfaellt, ist schlimmer als gar keine: Man
verlaesst sich auf etwas, das es nicht mehr gibt. Auffallen wuerde es
genau in dem Moment, in dem man sie braucht.
WAS GEPRUEFT WIRD
Die Datei, auf die es im Ernstfall ankommt -- nicht das Protokoll.
Ein Protokoll kann "erfolgreich" melden, waehrend die Datei fehlt.
Die Datei selbst kann das nicht.
===================================================================== */
import { existsSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
/* Die Sicherung laeuft um 03:15, die juengste Datei ist also zu jedem
Zeitpunkt hoechstens 24 Stunden alt. Der Aufschlag deckt die Laufzeit
und die Zeitumstellung ab (eine Stunde im Maerz und Oktober) und
meldet einen Ausfall trotzdem noch am selben Morgen. */
export const MAX_STUNDEN = 26;
/* Die Datenbank war am 26.08.2026 rund 778 KB gross. Die Grenze liegt
bewusst weit darunter: Sie soll den stillen Totalausfall fangen (eine
Datei mit 0 Byte, ein abgebrochener Lauf), nicht ueber die richtige
Groesse urteilen. Zu eng gesetzt haette sie an dem Tag angeschlagen,
an dem jemand alte Auftraege loescht. */
export const MIN_BYTE = 50 * 1024;
/**
* @returns {null | {heil: boolean, grund: string}}
* null bedeutet ausdruecklich "kein Urteil moeglich" und ist NICHT
* dasselbe wie "kaputt" -- siehe unten.
*/
export function sicherungZustand(basis = "/var/backups/dogfather", jetzt = Date.now()) {
if (!existsSync(basis)) {
return { heil: false, grund: "es gibt gar keinen Sicherungsordner" };
}
const taeglich = join(basis, "taeglich");
let ordner;
try {
ordner = readdirSync(taeglich)
.filter((n) => /^\d{4}-\d{2}-\d{2}$/.test(n))
.sort();
} catch (e) {
/* Hier MUSS unterschieden werden, und beim ersten Entwurf tat ich es
nicht: readdirSync wirft aus zwei ganz verschiedenen Gruenden.
ENOENT -- der Ordner ist nicht da. Das ist ein echter Befund: Es
gab nie einen Lauf, oder jemand hat ihn geloescht. Das gehoert
gemeldet.
EACCES/EPERM -- der Ordner ist da, wir duerfen nur nicht hinein.
Das sagt ueber die Sicherung nichts aus. Wer hier Alarm schlaegt,
erzeugt bei jedem Rechtewechsel einen Fehlalarm -- und ein
Waechter, der regelmaessig grundlos ruft, wird weggeklickt. Dann
geht der eine echte Alarm mit unter.
Beides in einen Topf zu werfen hiesse: entweder den geloeschten
Ordner verschweigen oder bei jedem Rechteproblem faelschlich
Alarm schlagen. Keins von beidem ist brauchbar. */
if (e && (e.code === "EACCES" || e.code === "EPERM")) return null;
return { heil: false, grund: "der Ordner fuer die taeglichen Laeufe fehlt" };
}
if (!ordner.length) {
return { heil: false, grund: "kein einziger Sicherungslauf vorhanden" };
}
/* Sortiert wird ueber den Namen, nicht ueber die Aenderungszeit: Die
Ordner heissen YYYY-MM-DD, da ist die alphabetische Reihenfolge
zugleich die zeitliche. Aenderungszeiten koennen beim Kopieren
oder Wiederherstellen verrutschen, der Name nicht. */
const neueste = ordner[ordner.length - 1];
const datei = join(taeglich, neueste, "dogfather-internal.db");
let s;
try {
s = statSync(datei);
} catch {
return { heil: false, grund: `Lauf vom ${neueste} hat keine Datenbankdatei` };
}
const stunden = (jetzt - s.mtimeMs) / 3600000;
if (stunden > MAX_STUNDEN) {
return { heil: false, grund: `juengste Sicherung ist ${Math.round(stunden)} Stunden alt (${neueste})` };
}
if (s.size < MIN_BYTE) {
return { heil: false, grund: `Sicherung vom ${neueste} ist nur ${Math.round(s.size / 1024)} KB gross` };
}
return {
heil: true,
grund: `Sicherung vom ${neueste}, ${Math.round(s.size / 1024)} KB, vor ${Math.round(stunden)} h`,
};
}