From 74e2128afb824e9d21c10a1e9817a17de641fbff Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Wed, 12 Aug 2026 08:50:44 +0200 Subject: [PATCH] feat: die Sicherung liegt an drei Orten statt an zweien Der letzte offene Punkt aus docs/ideen.md. Ein ZWEITES LAUFWERK, einstellbar im Panel unter Einstellungen -> Sicherung; sichern.ps1 legt das Archiv dort ab, vergleicht die Pruefsumme und duennt auch dort aus. WARUM DAS NICHT DASSELBE IST WIE DIE NEXTCLOUD. Die hilft, wenn der ganze Rechner weg ist -- aber wer 335 MB, spaeter 100 GB, zurueckholen muss, laedt sie ueber die Leitung. Das zweite Laufwerk ist in Minuten zurueckgespielt. Zwei Fragen, zwei Antworten, deshalb zwei Ziele. UND ES WARNT, WENN ES DIESELBE PLATTE IST -- das ist der eigentliche Inhalt. Ein zweiter Ordner auf C: sieht im Panel genauso gruen aus wie eine echte zweite Platte und hilft gegen gar nichts. Verglichen wird die PHYSISCHE Platte und nicht der Laufwerksbuchstabe: zwei Partitionen derselben NVMe sterben zusammen. Nachgemessen in beide Richtungen: C:\backup gegen D:\backup -> Nr. 0 gegen Nr. 1 -> "andere Platte" C:\backup gegen C:\Users -> Nr. 0 gegen Nr. 0 -> Warnung Auf diesem Rechner sind das zwei getrennte NVMe zu je 954 GB. GEMESSEN AN EINEM ECHTEN LAUF, keinem Trockentest: 4630 Eintraege, 335,9 MB, auf beiden lokalen Zielen und in der Nextcloud dieselbe Pruefsumme FFCE4D0A7310... -- identisch. Geprueft wird mit der Pruefsumme und nicht mit der Dateigroesse: eine abgebrochene Kopie auf eine volle Platte hat oft genau die richtige Laenge und trotzdem Nullen am Ende. Schlaegt das Kopieren oder der Vergleich fehl, ist der ganze Lauf gescheitert (exit 1, ok:false) -- ein Ziel, das still ausfaellt, ist genau das, wogegen das hier gebaut ist. Der Bericht traegt jetzt zweit:true. Fehlt das Feld, ist KEIN zweites Ziel eingerichtet -- es steht absichtlich nicht als false da, denn das laese sich wie "hat nicht geklappt". Die Statusseite zaehlt die Orte mit: "sicherung ok an 3 Orten" statt nur "ok". Eine Sicherung, die es nur einmal gibt, ist gruen und trotzdem eine, die ein Plattenausfall mitnimmt -- das gehoert dorthin, wo jemand hinsieht. Der Pfad wird beim Speichern auf seine FORM geprueft (Laufwerksbuchstabe oder UNC-Freigabe): der Dienst laeuft im Container und kann ihn nicht nachschlagen, aber ein relativer Pfad ist mit Sicherheit ein Tippfehler -- und ein Tippfehler in einem Sicherungsziel faellt sonst erst auf, wenn man die Sicherung braucht. Nachgemessen: "backup/woauchimmer" -> 400 mit Text. Und zurueckspielen.ps1 sagt im Kopf, was zu tippen ist, wenn genau diese Platte das Problem ist. Das ist der Fall, fuer den das Ganze da ist, und niemand soll ihn um vier Uhr nachts erst herleiten muessen. Co-Authored-By: Claude Opus 5 --- ROADMAP.md | 39 ++++++++ docs/API.md | 4 + docs/ideen.md | 22 ++++- server/src/routes/dash.ts | 26 ++++++ server/src/routes/status.ts | 18 +++- server/src/settings.ts | 13 +++ server/ui/src/api.ts | 5 ++ server/ui/src/pages/Einstellungen.tsx | 41 ++++++++- tools/sichern.ps1 | 124 ++++++++++++++++++++++++-- tools/zurueckspielen.ps1 | 12 +++ 10 files changed, 289 insertions(+), 15 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 32bdf9f..2988ccb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1066,6 +1066,45 @@ Kreuzchen, das sich setzen lässt und beim nächsten Laden weg ist. Diesmal beide Stellen angefasst und nachgemessen: **vier geschickt, vier gespeichert.** In beiden steht jetzt ein Verweis auf die andere. +### ✅ Fertig — die Sicherung liegt an drei Orten statt an zweien + +Der letzte offene Punkt aus `docs/ideen.md`. Ein **zweites Laufwerk**, +einstellbar im Panel unter Einstellungen → Sicherung; `tools/sichern.ps1` legt +das Archiv dort ab, vergleicht die Prüfsumme und dünnt auch dort aus. + +**Warum das nicht dasselbe ist wie die Nextcloud.** Die hilft, wenn der ganze +Rechner weg ist — aber wer 335 MB, später 100 GB, zurückholen muss, lädt sie +über die Leitung. Das zweite Laufwerk ist in Minuten zurückgespielt. Zwei +Fragen, zwei Antworten. + +**Und es warnt, wenn es dieselbe Platte ist.** Das ist der eigentliche Inhalt: +ein zweiter Ordner auf `C:` sieht im Panel genauso grün aus wie eine echte +zweite Platte und hilft gegen gar nichts. Verglichen wird die *physische* +Platte und nicht der Laufwerksbuchstabe — zwei Partitionen derselben NVMe +sterben zusammen. Nachgemessen, dieser Rechner: + +``` +Platte 0 SAMSUNG MZVL21T0HDLU-00B07 954 GB -> C: (Daten + C:\backup) +Platte 1 SAMSUNG MZVL21T0HDLU-00B07 954 GB -> D: (das zweite Ziel) + +C:\backup gegen D:\backup -> Nr. 0 gegen Nr. 1 -> "andere Platte" +C:\backup gegen C:\Users -> Nr. 0 gegen Nr. 0 -> Warnung +``` + +Ein echter Lauf, kein Trockentest: 4630 Einträge, 335,9 MB, auf beiden lokalen +Zielen und in der Nextcloud dieselbe Prüfsumme `FFCE4D0A7310…`. Der Bericht in +`sicherung.letzte` trägt jetzt `zweit: true`; fehlt das Feld, ist **kein** +zweites Ziel eingerichtet — es steht absichtlich nicht als `false` da, denn das +läse sich wie „hat nicht geklappt". + +Die **Statusseite** zählt die Orte mit: `sicherung ok an 3 Orten` statt nur +`ok`. Eine Sicherung, die es nur einmal gibt, ist grün und trotzdem eine, die +ein Plattenausfall mitnimmt — das gehört dorthin, wo jemand hinsieht. + +Und `tools/zurueckspielen.ps1` sagt im Kopf, was zu tippen ist, wenn genau +diese Platte das Problem ist. Das ist der Fall, für den das Ganze da ist, und +niemand soll ihn um vier Uhr nachts erst herleiten müssen. + ### ⬜ 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 5e81a05..a0ec80d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -177,6 +177,10 @@ dieses Dienstes — fällt eines aus, ist er `gestoert` und die Antwort ist Wer auch das rot haben will, hängt `?streng=1` an — dann gibt alles außer `gesund` eine 503. +Die Prüfung `sicherung` sagt zusätzlich, an **wie vielen Orten** das letzte +Archiv liegt (`an 3 Orten`, oder `nur an einem Ort`). Eine Sicherung, die es +nur einmal gibt, ist grün und trotzdem eine, die ein Plattenausfall mitnimmt. + ### In Uptime Kuma ``` diff --git a/docs/ideen.md b/docs/ideen.md index f115386..5735a64 100644 --- a/docs/ideen.md +++ b/docs/ideen.md @@ -138,10 +138,26 @@ die Oberfläche sie nicht ein zweites Mal hinschreibt. Bei 910 GB frei ist das heute keine Grenze, sondern eine Zusicherung: der Papierkorb kann nicht mehr unbemerkt zur zweiten Ablage werden. -### Mehrere Sicherungsziele — offen +### ~~Mehrere Sicherungsziele~~ — gebaut am 12.08.2026 -Die Archive liegen auf derselben Platte wie die Daten, die Kopie in der -Nextcloud ist die einzige Trennung. +Ein **zweites Laufwerk**, einstellbar im Panel unter Einstellungen → Sicherung. +Damit liegt jedes Archiv an drei Orten: `C:ackup`, das zweite Laufwerk und +die Nextcloud. + +Die drei sind nicht dreimal dasselbe. Die Nextcloud hilft, wenn der ganze +Rechner weg ist — aber wer 335 MB (oder später 100 GB) zurückholen muss, lädt +sie über die Leitung. Das zweite Laufwerk ist in Minuten zurückgespielt. + +**Und es warnt, wenn es dieselbe Platte ist.** Das ist der eigentliche Inhalt: +ein zweiter Ordner auf C: sieht im Panel genauso grün aus wie eine echte zweite +Platte und hilft gegen gar nichts. Verglichen wird die *physische* Platte, nicht +der Laufwerksbuchstabe — zwei Partitionen derselben NVMe sterben zusammen. +Nachgemessen in beide Richtungen: `C:` gegen `D:` ergibt Nr. 0 gegen Nr. 1 und +ein „andere Platte"; `C:` gegen `C:` ergibt zweimal Nr. 0 und die Warnung. + +Ein echter Lauf: 4630 Einträge, 335,9 MB, auf beiden Zielen die Prüfsumme +`FFCE4D0A7310…` — identisch. Die Statusseite sagt seitdem +`sicherung ok an 3 Orten` statt nur `ok`. --- diff --git a/server/src/routes/dash.ts b/server/src/routes/dash.ts index e406f71..fb38f4d 100644 --- a/server/src/routes/dash.ts +++ b/server/src/routes/dash.ts @@ -1311,6 +1311,7 @@ dashRoutes.post('/settings', async (c) => { passwort?: string ordner?: string behalten?: number | string + zweitziel?: string } }>(c) if (!body) return c.json(KEIN_JSON, 400) @@ -1418,6 +1419,31 @@ dashRoutes.post('/settings', async (c) => { } setSetting(SICHERUNG_KEYS.behalten, String(Math.round(n))) } + if (sic.zweitziel !== undefined) { + // Der Pfad gilt auf dem HOST. Der Dienst sieht ihn nie und kann + // ihn deshalb nicht nachschlagen -- er nimmt ihn entgegen und + // tools/sichern.ps1 stolpert notfalls darueber. Was hier trotzdem + // geht, ist die Form: ein Pfad ohne Laufwerk oder Freigabe ist mit + // Sicherheit ein Tippfehler, und ein Tippfehler in einem + // Sicherungsziel faellt sonst erst auf, wenn man die Sicherung + // braucht. + const v = sic.zweitziel.trim().replace(/[\\/]+$/, '') + // Entweder ein Laufwerksbuchstabe (D:\...) oder eine UNC-Freigabe + // (\\nas\sicherungen). Alles andere ist ein relativer Pfad, und + // relativ WOZU waere hier gar nicht beantwortbar. + if (v && !/^([A-Za-z]:[\\/]|\\\\[^\\]+\\)/.test(v)) { + return c.json( + { + error: + 'Das zweite Ziel braucht ein Laufwerk oder eine Freigabe, ' + + 'z. B. D:\\backup\\d4rk_media oder \\\\nas\\sicherungen.', + }, + 400, + ) + } + setSetting(SICHERUNG_KEYS.zweitziel, v) + } + // Wie beim Discord-Geheimnis: leer heisst unveraendert, "-" entfernt. if (sic.passwort !== undefined) { const v = sic.passwort.trim() diff --git a/server/src/routes/status.ts b/server/src/routes/status.ts index ecf33fc..b55fde4 100644 --- a/server/src/routes/status.ts +++ b/server/src/routes/status.ts @@ -189,7 +189,12 @@ async function pruefen(): Promise { pruefungen.push({ was: 'sicherung', ok: false, hinweis: 'noch keine gemeldet' }) } else { try { - const l = JSON.parse(roh) as { at: number; ok: boolean } + const l = JSON.parse(roh) as { + at: number + ok: boolean + hoch?: boolean + zweit?: boolean + } const stunden = (now() - l.at) / 3_600_000 if (!l.ok) { pruefungen.push({ was: 'sicherung', ok: false, hinweis: 'letzte fehlgeschlagen' }) @@ -200,7 +205,16 @@ async function pruefen(): Promise { hinweis: `letzte vor ${Math.round(stunden)} Stunden`, }) } else { - pruefungen.push({ was: 'sicherung', ok: true }) + // WIEVIELE ORTE, nicht nur "gelaufen". Eine Sicherung, die es + // nur einmal gibt, ist gruen und trotzdem eine, die ein + // Plattenausfall mitnimmt -- das gehoert auf die Statusseite, + // wo jemand hinschaut, und nicht nur ins Panel. + const orte = 1 + (l.zweit ? 1 : 0) + (l.hoch ? 1 : 0) + pruefungen.push({ + was: 'sicherung', + ok: true, + hinweis: orte === 1 ? 'nur an einem Ort' : `an ${orte} Orten`, + }) } } catch { pruefungen.push({ was: 'sicherung', ok: false, hinweis: 'Eintrag unlesbar' }) diff --git a/server/src/settings.ts b/server/src/settings.ts index 5662221..6caabc2 100644 --- a/server/src/settings.ts +++ b/server/src/settings.ts @@ -109,6 +109,17 @@ export const SICHERUNG_KEYS = { passwort: 'sicherung.passwort', ordner: 'sicherung.ordner', behalten: 'sicherung.behalten', + /** Ein ZWEITER lokaler Ablageort, moeglichst auf einer anderen Platte. + * + * WOZU, wenn es doch schon die Nextcloud gibt: die ist weit weg. Wer + * 100 GB zurueckholen muss, laedt sie ueber die Leitung -- das ist die + * Sicherung fuer den Fall, dass das Haus abbrennt, nicht fuer den Fall, + * dass eine Platte stirbt. Fuer den zweiten will man die Kopie IM Rechner + * haben, nur eben nicht auf derselben Platte. + * + * Der Pfad gilt auf dem HOST, nicht im Container -- ihn prueft deshalb + * nicht der Dienst, sondern tools/sichern.ps1. */ + zweitziel: 'sicherung.zweitziel', /** Schreibt das Skript nach jedem Lauf. JSON, damit das Panel es anzeigen * kann, ohne dass hier eine weitere Tabelle noetig waere. */ letzte: 'sicherung.letzte', @@ -119,6 +130,7 @@ export type SicherungConfig = { benutzer: string ordner: string behalten: number + zweitziel: string hatPasswort: boolean bereit: boolean } @@ -135,6 +147,7 @@ export function sicherungConfig(): SicherungConfig { benutzer, ordner, behalten, + zweitziel: getSetting(SICHERUNG_KEYS.zweitziel), hatPasswort: Boolean(passwort), bereit: Boolean(basis && benutzer && passwort), } diff --git a/server/ui/src/api.ts b/server/ui/src/api.ts index d9547e1..33d83df 100644 --- a/server/ui/src/api.ts +++ b/server/ui/src/api.ts @@ -309,6 +309,10 @@ export type LetzteSicherung = { groesse?: number /** true = in der Nextcloud, false = nur lokal, undefined = unbekannt. */ hoch?: boolean + /** true = auch auf der zweiten Platte. undefined heisst „kein zweites Ziel + * eingerichtet" und nicht „hat nicht geklappt" -- schiefgegangen waere ein + * Grund fuer ok:false, nicht fuer ein stilles false. */ + zweit?: boolean fehler?: string } | null @@ -320,6 +324,7 @@ export type Einstellungen = { benutzer: string ordner: string behalten: number + zweitziel: string hatPasswort: boolean bereit: boolean letzte: LetzteSicherung diff --git a/server/ui/src/pages/Einstellungen.tsx b/server/ui/src/pages/Einstellungen.tsx index df3f7a0..be7e835 100644 --- a/server/ui/src/pages/Einstellungen.tsx +++ b/server/ui/src/pages/Einstellungen.tsx @@ -1,5 +1,13 @@ import { useEffect, useState } from 'react' -import { AlertTriangle, CheckCircle2, CloudUpload, Loader2, ShieldCheck, ShieldX } from 'lucide-react' +import { + AlertTriangle, + CheckCircle2, + CloudUpload, + HardDrive, + Loader2, + ShieldCheck, + ShieldX, +} from 'lucide-react' import { api, type Einstellungen as EinstellungenDaten } from '../api' import { run, useStore } from '../store' import { bytes, zeit } from '../format' @@ -55,6 +63,7 @@ function SicherungKarte() { const [passwort, setPasswort] = useState('') const [ordner, setOrdner] = useState('') const [behalten, setBehalten] = useState('14') + const [zweitziel, setZweitziel] = useState('') async function laden() { setLaedt(true) @@ -65,6 +74,7 @@ function SicherungKarte() { setBenutzer(e.sicherung.benutzer) setOrdner(e.sicherung.ordner) setBehalten(String(e.sicherung.behalten)) + setZweitziel(e.sicherung.zweitziel) } catch (err) { note('fehler', err instanceof Error ? err.message : 'Unbekannt') } finally { @@ -87,6 +97,7 @@ function SicherungKarte() { ...(passwort ? { passwort } : {}), ordner, behalten: Number(behalten), + zweitziel, }), (r) => r.bereit @@ -116,7 +127,7 @@ function SicherungKarte() { return ( @@ -158,6 +169,7 @@ function SicherungKarte() { {l.groesse != null && `, ${bytes(l.groesse)}`} {l.hoch === true && ', in der Nextcloud'} {l.hoch === false && ', nur lokal'} + {l.zweit === true && ', auf der zweiten Platte'} {l.fehler && ( {l.fehler} )} @@ -199,11 +211,34 @@ function SicherungKarte() { min={1} max={365} value={behalten} - hinweis="gilt hier wie in der Nextcloud" + hinweis="gilt überall gleich" onChange={(e) => setBehalten(e.target.value)} /> + setZweitziel(e.target.value)} + /> +

+ + + + Die zwei Ziele beantworten zwei verschiedene Fragen. + {' '} + Die Nextcloud hilft, wenn der ganze Rechner weg ist — aber wer 100 GB + zurückholen muss, lädt sie über die Leitung. Das zweite Ziel liegt im + Rechner und ist in Minuten zurückgespielt; es hilft nur, wenn es auf einer{' '} + anderen Platte liegt als die + Daten. Ein zweiter Ordner auf derselben Platte sieht aus wie eine + Sicherung und ist keine. + +

+

diff --git a/tools/sichern.ps1 b/tools/sichern.ps1 index 578ebb4..6623ba6 100644 --- a/tools/sichern.ps1 +++ b/tools/sichern.ps1 @@ -16,14 +16,26 @@ # 3. Das Archiv wird GEPRUEFT: ausgepackt, die Datenbank geoeffnet, die # Eintraege gezaehlt und mit dem laufenden Dienst verglichen. Eine # Sicherung, die niemand nachzaehlt, ist eine Vermutung. -# 4. Das Archiv geht in die NEXTCLOUD, wenn im Panel Zugangsdaten stehen +# 4. Das Archiv geht auf ein ZWEITES LAUFWERK, wenn im Panel eines steht +# (Einstellungen → Sicherung → "Zweites Ziel"). Auch hier wird die +# Pruefsumme verglichen — eine Kopie, die niemand nachzaehlt, ist eine +# Vermutung. +# +# WOZU, wo es doch die Nextcloud gibt: die beiden beantworten zwei +# verschiedene Fragen. Die Nextcloud hilft, wenn der ganze Rechner weg +# ist — aber wer 100 GB zurueckholen muss, laedt sie ueber die Leitung. +# Das zweite Laufwerk ist in Minuten zurueckgespielt. Es hilft nur, wenn +# es eine ANDERE PLATTE ist: ein zweiter Ordner auf derselben sieht aus +# wie eine Sicherung und ist keine. +# +# 4a. Das Archiv geht in die NEXTCLOUD, wenn im Panel Zugangsdaten stehen # (Einstellungen → Sicherung). Bis dahin lag die Sicherung auf derselben # Platte wie die Daten — gegen einen Plattenausfall half sie gar nicht. -# 4b. Und der Lauf MELDET SICH ZURUECK: das Ergebnis landet in der Datenbank +# 4c. Und der Lauf MELDET SICH ZURUECK: das Ergebnis landet in der Datenbank # und steht danach im Panel. Eine Sicherung, die still aufhoert zu laufen, # ist der eigentliche Schaden — nicht die eine Nacht, in der sie fehlt. -# 5. Alte Archive fliegen raus, hier wie dort, sobald mehr als -Behalten da -# sind. +# 5. Alte Archive fliegen raus, an ALLEN drei Orten, sobald mehr als +# -Behalten da sind. # # WARUM CURL UND NICHT RCLONE. Nextcloud spricht WebDAV, curl spricht WebDAV, # und curl ist ohnehin da. rclone waere ein weiteres Programm, das installiert, @@ -43,7 +55,10 @@ param( [string]$Container = 'd4rk-media', [string]$Volume = 'd4rk_media_media-data', [string]$Abbild = 'd4rk-media:latest', - [int]$Behalten = 14 + [int]$Behalten = 14, + # Leer heisst: aus den Einstellungen im Panel holen. Ein Wert hier hat + # Vorrang -- fuer den Fall, dass jemand einmalig woanders hinsichern will. + [string]$Zweitziel = '' ) $ErrorActionPreference = 'Stop' @@ -56,6 +71,7 @@ function Schlecht($text) { Write-Host " $text" -ForegroundColor Red } # steht. Schlaegt DAS fehl, ist es kein Grund, die Sicherung als gescheitert zu # behandeln — das Archiv liegt dann trotzdem richtig. $inDerWolke = $false +$aufZweiter = $null function Melde($ok, $dateien, $groesse, $hoch, $fehler) { $eintrag = @{ @@ -65,6 +81,10 @@ function Melde($ok, $dateien, $groesse, $hoch, $fehler) { groesse = $groesse hoch = [bool]$hoch } + # $null heisst "kein zweites Ziel eingerichtet" und steht dann GAR NICHT im + # Bericht -- ein Feld, das immer "false" sagt, liest sich wie "hat nicht + # geklappt", und das waere eine Falschmeldung. + if ($null -ne $script:aufZweiter) { $eintrag['zweit'] = [bool]$script:aufZweiter } if ($fehler) { $eintrag['fehler'] = "$fehler" } $json = ($eintrag | ConvertTo-Json -Compress) @@ -191,7 +211,12 @@ if ($dateien -ne $zahlen.media) { } Gut 'Datenbank in der Sicherung ist lesbar und vollstaendig.' -# -- 4. In die Nextcloud ------------------------------------------------------- +# -- Wohin ueberhaupt? Zugangsdaten und Ziele lesen ---------------------------- +# +# Beides kommt aus derselben Quelle und wird deshalb an EINER Stelle geholt: die +# Nextcloud-Zugangsdaten und der Pfad des zweiten Laufwerks. Was danach kommt, +# sind die drei Ziele in der Reihenfolge, in der sie im Ernstfall gebraucht +# werden -- erst die Platte nebenan, dann die Wolke. # # Die Zugangsdaten liegen NEBEN den Sicherungen und nicht im Repo: sie gehoeren # zu dieser Maschine, nicht zum Code. Fehlt die Datei, wird das einmal gesagt @@ -235,17 +260,102 @@ console.log(JSON.stringify({ passwort: hol('sicherung.passwort'), ordner: hol('sicherung.ordner'), behalten: hol('sicherung.behalten'), + zweitziel: hol('sicherung.zweitziel'), })); "@ if ($LASTEXITCODE -eq 0 -and $roh) { $j = $roh | ConvertFrom-Json - foreach ($k in 'basis', 'benutzer', 'passwort', 'ordner', 'behalten') { + foreach ($k in 'basis', 'benutzer', 'passwort', 'ordner', 'behalten', 'zweitziel') { if ($j.$k) { $z[$k] = $j.$k } } } } if ($z['behalten']) { $Behalten = [int]$z['behalten'] } +if (-not $Zweitziel -and $z['zweitziel']) { $Zweitziel = $z['zweitziel'] } + +# -- 4. Auf die zweite Platte -------------------------------------------------- +# +# Das billigste Ziel von allen -- ein Kopiervorgang im selben Rechner -- und das +# einzige, aus dem sich hundert Gigabyte in Minuten zurueckholen lassen. +# +# GEPRUEFT WIRD MIT DER PRUEFSUMME und nicht mit der Dateigroesse: eine +# abgebrochene Kopie auf eine volle Platte hat oft genau die richtige Laenge und +# trotzdem Nullen am Ende. +# +# UND ES WIRD GEWARNT, WENN ES DIESELBE PLATTE IST -- das ist der eigentliche +# Fehler, gegen den das hier gebaut ist. Ein zweiter Ordner auf C: sieht im +# Panel genauso gruen aus wie eine echte zweite Platte und hilft gegen gar +# nichts. Verglichen wird die PHYSISCHE Platte und nicht der Laufwerksbuchstabe: +# zwei Partitionen derselben NVMe sterben zusammen. + +function PlattenNummer($pfad) { + # $null heisst "kein Urteil moeglich" (Netzfreigabe, ungewoehnliche + # Einhaengung) und ist ausdruecklich nicht dasselbe wie "andere Platte". + try { + $wurzel = [System.IO.Path]::GetPathRoot((Resolve-Path $pfad).Path) + $buchstabe = $wurzel.TrimEnd('\', ':') + if ($buchstabe.Length -ne 1) { return $null } + return (Get-Partition -DriveLetter $buchstabe -ErrorAction Stop).DiskNumber + } catch { return $null } +} + +if (-not $Zweitziel) { + Write-Host ' Kein zweites Ziel eingerichtet (Panel -> Einstellungen -> Sicherung).' -ForegroundColor Yellow +} else { + Schritt "Auf die zweite Platte: $Zweitziel" + + try { + New-Item -ItemType Directory -Force -Path $Zweitziel -ErrorAction Stop | Out-Null + } catch { + Schlecht "Das zweite Ziel ist nicht erreichbar: $($_.Exception.Message)" + $aufZweiter = $false + Melde $false $zahlen.media $groesse $inDerWolke "Zweites Ziel: $($_.Exception.Message)" + exit 1 + } + + $hier = PlattenNummer $Ziel + $dort = PlattenNummer $Zweitziel + if ($null -ne $hier -and $hier -eq $dort) { + Write-Host " ACHTUNG: $Zweitziel liegt auf DERSELBEN Platte (Nr. $hier) wie $Ziel." -ForegroundColor Yellow + Write-Host ' Gegen einen Plattenausfall hilft das nicht -- ein anderes Laufwerk waehlen.' -ForegroundColor Yellow + } elseif ($null -ne $hier -and $null -ne $dort) { + Gut "andere Platte: Nr. $dort statt Nr. $hier" + } + + $zweitDatei = Join-Path $Zweitziel $name + try { + Copy-Item $archiv $zweitDatei -Force -ErrorAction Stop + } catch { + Schlecht "Kopieren fehlgeschlagen: $($_.Exception.Message)" + $aufZweiter = $false + Melde $false $zahlen.media $groesse $inDerWolke "Zweites Ziel: $($_.Exception.Message)" + exit 1 + } + + $hierHash = (Get-FileHash $archiv -Algorithm SHA256).Hash + $dortHash = (Get-FileHash $zweitDatei -Algorithm SHA256).Hash + if ($hierHash -ne $dortHash) { + Schlecht 'Die Kopie auf der zweiten Platte ist NICHT identisch.' + Remove-Item $zweitDatei -Force -ErrorAction SilentlyContinue + $aufZweiter = $false + Melde $false $zahlen.media $groesse $inDerWolke 'Pruefsumme der zweiten Kopie weicht ab' + exit 1 + } + Gut "kopiert und verglichen: identisch ($($hierHash.Substring(0,12))...)" + $aufZweiter = $true + + # Auch dort ausduennen, sonst laeuft die zweite Platte still voll. + $dortAlle = Get-ChildItem $Zweitziel -Filter 'd4rk_media-*.tar.gz' | Sort-Object Name -Descending + if ($dortAlle.Count -gt $Behalten) { + $dortAlle | Select-Object -Skip $Behalten | ForEach-Object { + Remove-Item $_.FullName -Force + Gut "auf der zweiten Platte entfernt: $($_.Name)" + } + } +} + +# -- 4a. In die Nextcloud ------------------------------------------------------ if (-not ($z['basis'] -and $z['benutzer'] -and $z['passwort'])) { Write-Host ' Keine Nextcloud eingerichtet (Panel → Einstellungen → Sicherung) — die Sicherung bleibt lokal.' -ForegroundColor Yellow diff --git a/tools/zurueckspielen.ps1 b/tools/zurueckspielen.ps1 index 1c15776..91ac195 100644 --- a/tools/zurueckspielen.ps1 +++ b/tools/zurueckspielen.ps1 @@ -21,10 +21,22 @@ # Bestand ausliefern und raeumt hinterher auf. Der echte Dienst wird dabei # nicht angefasst -- er laeuft die ganze Zeit weiter. +# WENN GENAU DIESE PLATTE DAS PROBLEM IST: die Sicherung liegt seit dem zweiten +# Ziel auch woanders. Das ist der Fall, fuer den sie da ist, und niemand soll +# ihn um vier Uhr nachts erst herleiten muessen -- +# +# pwsh -File tools\zurueckspielen.ps1 -Ordner D:\backup\d4rk_media -Echt +# +# und wenn auch die zweite Platte weg ist, holt man das Archiv aus der Nextcloud +# (Sicherungen/d4rk_media) und zeigt mit -Archiv direkt darauf. Welche Ziele +# eingerichtet sind, steht im Panel unter Einstellungen -> Sicherung. + [CmdletBinding()] param( # Ohne Angabe: das neueste Archiv im Sicherungsordner. [string]$Archiv = '', + # Die ZWEITE Platte steht in den Einstellungen; ohne Angabe wird hier + # gesucht, weil das der Ordner ist, in den sichern.ps1 zuerst schreibt. [string]$Ordner = 'C:\backup\d4rk_media', [string]$Container = 'd4rk-media', [string]$Volume = 'd4rk_media_media-data',