waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src main selbstsicherung.ts

4,9 KB Rohdatei
app/src/main/selbstsicherung.ts — 129 Zeilen
1 /**
2 * Die selbsttätige Sicherheitskopie des Lernstands.
3 *
4 * **Wozu.** Eine Sicherung entstand bis 0.22.0 ausschließlich, wenn jemand
5 * die Karte „Lernstand sichern und übertragen“ aufschlug und bediente. Die
6 * einzige selbsttätige Kopie entstand vor dem **Einspielen** einer fremden
7 * Sicherung – also genau dann, wenn ohnehin jemand mit Sicherungen hantiert.
8 * Wer die Karte nie öffnete, hatte nichts: Ein Datenträgerdefekt, ein
9 * versehentliches Löschen oder eine beschädigte Datei nahmen alles mit, was
10 * über Wochen gelernt worden war.
11 *
12 * Diese Kopie liegt auf demselben Datenträger und ersetzt deshalb **keine**
13 * richtige Sicherung – das sagt die Karte auch. Sie deckt die häufigen Fälle:
14 * die eine beschädigte Datei, den Fehlgriff, den missglückten Einspielvorgang.
15 * Gegen einen Plattendefekt hilft nur eine Kopie anderswo, und dazu rät die
16 * Anwendung weiterhin.
17 *
18 * **Warum beim Beenden.** Beim Start wäre die Kopie die des vorigen Standes
19 * und ließe die Arbeit des letzten Tages aus. Beim Beenden ist sie so frisch
20 * wie möglich. `VACUUM INTO` braucht dafür die offene Verbindung – der Aufruf
21 * gehört also **vor** `lernstandSchliessen()`.
22 *
23 * **Warum nicht bei jedem Beenden.** Eine Kopie je Programmstart füllte den
24 * Datenträger mit Fassungen, die sich um Minuten unterscheiden. Der Abstand
25 * von einer Woche ist der Kompromiss: alt genug, dass sich etwas geändert
26 * hat, jung genug, dass der Verlust überschaubar bleibt.
27 */
28
29 import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
30 import { join } from 'node:path';
31
32 import type BetterSqlite3 from 'better-sqlite3';
33
34 import { aufraeumen, sicherungSchreiben, type DatenbankKonstruktor } from './sicherung';
35
36 /** Unterordner im `userData`-Verzeichnis. */
37 export const SELBSTSICHERUNG_ORDNER = 'sicherungen';
38
39 /** Vorsatz im Dateinamen – hält die Kopien von allem anderen getrennt. */
40 export const SELBSTSICHERUNG_VORSATZ = 'Lernstand-selbsttaetig';
41
42 /** So lange gilt die letzte Kopie als frisch genug. */
43 export const ABSTAND_MS = 7 * 24 * 60 * 60 * 1000;
44
45 /** So viele Kopien bleiben liegen. */
46 export const KOPIEN_BEHALTEN = 3;
47
48 /** Voller Pfad des Kopienordners. */
49 export function selbstsicherungsordner(userData: string): string {
50 return join(userData, SELBSTSICHERUNG_ORDNER);
51 }
52
53 /** Alle vorhandenen Kopien, jüngste zuerst. */
54 function vorhandene(ordner: string): { name: string; zeit: number }[] {
55 if (!existsSync(ordner)) {
56 return [];
57 }
58 return readdirSync(ordner)
59 .filter(
60 (name) => name.startsWith(`${SELBSTSICHERUNG_VORSATZ}-`) && name.endsWith('.wsklernstand'),
61 )
62 .map((name) => ({ name, zeit: statSync(join(ordner, name)).mtimeMs }))
63 .sort((a, b) => b.zeit - a.zeit);
64 }
65
66 /**
67 * Ob eine neue Kopie fällig ist.
68 *
69 * Als eigene Funktion, weil daran die Entscheidung hängt und sie sich sonst
70 * nur über das Vorstellen der Systemuhr prüfen ließe.
71 */
72 export function istFaellig(letzteMs: number | null, jetztMs: number): boolean {
73 if (letzteMs === null) {
74 return true;
75 }
76 /* Eine Kopie mit einem Zeitstempel aus der Zukunft – Uhrsprung, kopierter
77 Ordner – gilt als überfällig, nicht als frisch. Sonst unterbliebe die
78 Sicherung bis zu dem Tag, den ihr Stempel behauptet. */
79 return jetztMs - letzteMs >= ABSTAND_MS || letzteMs > jetztMs;
80 }
81
82 /**
83 * Legt eine Kopie an, wenn eine fällig ist.
84 *
85 * @returns Pfad der geschriebenen Kopie, oder `null`, wenn keine fällig war.
86 */
87 export function selbstsicherungAnlegen(
88 datenbank: BetterSqlite3.Database,
89 userData: string,
90 Datenbank: DatenbankKonstruktor,
91 jetzt: Date = new Date(),
92 ): string | null {
93 const ordner = selbstsicherungsordner(userData);
94 const bisher = vorhandene(ordner);
95 const letzte = bisher[0]?.zeit ?? null;
96
97 if (!istFaellig(letzte, jetzt.getTime())) {
98 return null;
99 }
100
101 mkdirSync(ordner, { recursive: true });
102
103 /* Sekundengenauer Name: Zwei Kopien in derselben Sekunde kann es nicht
104 geben, weil eine je Woche entsteht – der Stempel ist hier nur der
105 Ordnung wegen so genau. */
106 const stempel = `${String(jetzt.getFullYear())}-${zwei(jetzt.getMonth() + 1)}-${zwei(jetzt.getDate())}-${zwei(jetzt.getHours())}${zwei(jetzt.getMinutes())}${zwei(jetzt.getSeconds())}`;
107 const ziel = join(ordner, `${SELBSTSICHERUNG_VORSATZ}-${stempel}.wsklernstand`);
108 aufraeumen(ziel);
109
110 sicherungSchreiben(datenbank, ziel, Datenbank);
111 aufraeumenAlte(ordner);
112 return ziel;
113 }
114
115 function zwei(wert: number): string {
116 return String(wert).padStart(2, '0');
117 }
118
119 /** Nur die jüngsten Kopien bleiben; ausschließlich dieses Muster. */
120 function aufraeumenAlte(ordner: string): void {
121 try {
122 for (const alt of vorhandene(ordner).slice(KOPIEN_BEHALTEN)) {
123 aufraeumen(join(ordner, alt.name));
124 }
125 } catch {
126 /* Aufräumen ist Komfort. Misslingt es, bleiben ein paar Dateien mehr
127 liegen – kein Grund, eine gelungene Sicherung zu verwerfen. */
128 }
129 }