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 <noreply@anthropic.com>
447 lines
20 KiB
PowerShell
447 lines
20 KiB
PowerShell
# Sicherung von d4rk_media: Datenbank und Bilder in EIN Archiv.
|
|
#
|
|
# Aufruf:
|
|
# pwsh -File tools\sichern.ps1
|
|
# pwsh -File tools\sichern.ps1 -Ziel D:\woanders -Behalten 30
|
|
#
|
|
# Was passiert, und warum in dieser Reihenfolge:
|
|
#
|
|
# 1. Die Datenbank wird IM laufenden Container ueber SQLites eigene
|
|
# Sicherungsschnittstelle als eine in sich stimmige Datei abgelegt
|
|
# (dist/backup.js). Ein blosses Kopieren waere wegen des WAL still
|
|
# unvollstaendig.
|
|
# 2. Ein zweiter, kurzlebiger Container packt Bilder und diese Aufnahme in
|
|
# ein tar.gz. Er haengt das Volume NUR LESEND ein — eine Sicherung, die
|
|
# schreiben kann, kann auch kaputtmachen.
|
|
# 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 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.
|
|
# 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, 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,
|
|
# konfiguriert und aktuell gehalten werden will — fuer PUT, PROPFIND und DELETE
|
|
# lohnt das nicht.
|
|
#
|
|
# NICHT MIT DRIN sind die Vorschaubilder unter /data/thumbs, und das ist
|
|
# Absicht: sie lassen sich aus den Bildern jederzeit neu rechnen (Dashboard →
|
|
# Speicher → "Fehlende Vorschaubilder erzeugen"). Sie mitzusichern hiesse, jede
|
|
# Nacht Abgeleitetes mitzuschleppen. Wer zurueckspielt, drueckt den Knopf.
|
|
#
|
|
# Der Dienst laeuft die ganze Zeit weiter. Nichts hier haelt ihn an.
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$Ziel = 'C:\backup\d4rk_media',
|
|
[string]$Container = 'd4rk-media',
|
|
[string]$Volume = 'd4rk_media_media-data',
|
|
[string]$Abbild = 'd4rk-media:latest',
|
|
[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'
|
|
|
|
function Schritt($text) { Write-Host "==> $text" -ForegroundColor Cyan }
|
|
function Gut($text) { Write-Host " $text" -ForegroundColor Green }
|
|
function Schlecht($text) { Write-Host " $text" -ForegroundColor Red }
|
|
|
|
# Das Ergebnis in die Datenbank des Dienstes schreiben, damit es im Panel
|
|
# 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 = @{
|
|
at = [long]([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
|
|
ok = [bool]$ok
|
|
dateien = $dateien
|
|
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)
|
|
|
|
# Ueber die Standardeingabe und nicht als Argument: der JSON-Text enthaelt
|
|
# Anfuehrungszeichen, und die ueberleben den Weg durch zwei Shells nicht.
|
|
$skript = @"
|
|
let roh = '';
|
|
process.stdin.on('data', (d) => (roh += d)).on('end', () => {
|
|
const db = require('better-sqlite3')('/data/media.db');
|
|
db.prepare('INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ' +
|
|
'ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at')
|
|
.run('sicherung.letzte', roh.trim(), Date.now());
|
|
});
|
|
"@ -replace "`r", ''
|
|
$json | docker exec -i $Container node -e $skript 2>&1 | Out-Null
|
|
}
|
|
|
|
# -- Vorbedingungen ------------------------------------------------------------
|
|
|
|
$laeuft = docker ps --filter "name=^/$Container$" --format '{{.Names}}'
|
|
if ($laeuft -ne $Container) {
|
|
Schlecht "Der Container '$Container' laeuft nicht. Ohne ihn gibt es keine stimmige Aufnahme der Datenbank."
|
|
exit 1
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $Ziel | Out-Null
|
|
$stempel = Get-Date -Format 'yyyy-MM-dd-HHmm'
|
|
$name = "d4rk_media-$stempel.tar.gz"
|
|
$archiv = Join-Path $Ziel $name
|
|
|
|
# -- 1. Datenbank stimmig herausholen ------------------------------------------
|
|
|
|
Schritt 'Datenbank sichern (SQLite-Sicherungsschnittstelle im Container)'
|
|
# Auch hier zusammenfuegen: die Ausgabe kaeme sonst als Array, und -match
|
|
# darauf filtert, statt zu pruefen (siehe uebernehmen.ps1). Solange es eine
|
|
# Zeile ist, faellt das nicht auf -- bei zweien schon.
|
|
$ausgabe = (docker exec $Container node dist/backup.js /data/.sicherung/media.db 2>&1) -join "`n"
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Schlecht "Fehlgeschlagen: $ausgabe"
|
|
exit 1
|
|
}
|
|
Gut $ausgabe
|
|
# Die erwartete Zeilenzahl steht in der Meldung -- sie ist der Massstab fuer
|
|
# Schritt 3.
|
|
$erwartet = if ($ausgabe -match '(\d+) Medieneintraege') { [int]$Matches[1] } else { -1 }
|
|
|
|
# -- 2. Alles in ein Archiv ----------------------------------------------------
|
|
|
|
Schritt 'Bilder und Datenbank packen'
|
|
# Drei -C: die Bilder kommen als files/... in das Archiv, die Aufnahme als
|
|
# media.db daneben, und der Papierkorb als papierkorb/. Bewusst das eigene
|
|
# Abbild statt alpine -- dort ist GNU tar drin, und das kann mehrere -C.
|
|
#
|
|
# DER PAPIERKORB MUSS MIT, und das ist nachgerechnet und nicht Vorsicht: seine
|
|
# EINTRAEGE stehen in media.db und waren damit laengst gesichert -- die DATEIEN
|
|
# lagen unter /data/papierkorb und waren es nicht. Nach einem Zurueckspielen
|
|
# haette der Papierkorb also Zeilen gezeigt, deren "zurueckholen" ins Leere
|
|
# greift. Ein Papierkorb, der ein Versprechen gibt, das er nach einer
|
|
# Wiederherstellung nicht halten kann, ist schlimmer als keiner.
|
|
#
|
|
# Er kostet fast nichts: was darin liegt, ist hoechstens 30 Tage alt, danach
|
|
# raeumt der Dienst selbst auf.
|
|
#
|
|
# Die Abfrage davor ist noetig: hat noch nie jemand etwas geloescht, gibt es den
|
|
# Ordner gar nicht, und tar braeche mit "Cannot stat" ab. Eine Sicherung, die
|
|
# daran scheitert, dass nichts im Papierkorb liegt, waere absurd. Das Volume
|
|
# haengt NUR LESEND -- ein `mkdir` als Ausweg ginge hier also ohnehin nicht.
|
|
$packen = "if [ -d /data/papierkorb ]; then tar czf '/out/$name' -C /data files -C /data/.sicherung media.db -C /data papierkorb; else tar czf '/out/$name' -C /data files -C /data/.sicherung media.db; fi"
|
|
docker run --rm `
|
|
-v "${Volume}:/data:ro" `
|
|
-v "${Ziel}:/out" `
|
|
--entrypoint sh $Abbild -c $packen
|
|
if ($LASTEXITCODE -ne 0) { Schlecht 'Packen fehlgeschlagen.'; exit 1 }
|
|
|
|
# Die Zwischenaufnahme im Volume wieder weg: sie gehoert nicht zum Bestand und
|
|
# wuerde bei der naechsten Sicherung mitgezaehlt.
|
|
docker exec $Container rm -rf /data/.sicherung | Out-Null
|
|
|
|
$groesse = (Get-Item $archiv).Length
|
|
Gut "$name — $([math]::Round($groesse/1MB, 1)) MB"
|
|
|
|
# -- 3. Nachzaehlen ------------------------------------------------------------
|
|
|
|
Schritt 'Sicherung pruefen (auspacken, Datenbank oeffnen, zaehlen)'
|
|
|
|
# Die Wagenruecklaeufe muessen raus. Diese Datei liegt unter Windows und wird
|
|
# je nach Werkzeug mit CRLF gespeichert; die Shell im Container liest dann
|
|
# "set -e\r" und antwortet mit "Illegal option -". Der Fehler sieht aus wie ein
|
|
# Tippfehler im Skript und ist keiner — genau einmal reingefallen.
|
|
$skript = (@"
|
|
set -e
|
|
mkdir -p /tmp/p && cd /tmp/p
|
|
tar xzf '/in/$name'
|
|
# Aus /app heraus starten und nicht aus /tmp/p: Node sucht node_modules vom
|
|
# Arbeitsverzeichnis aufwaerts, und ueber /tmp liegt kein better-sqlite3.
|
|
cd /app
|
|
node -e "
|
|
const db = require('better-sqlite3')('/tmp/p/media.db', { readonly: true, fileMustExist: true });
|
|
const m = db.prepare('SELECT COUNT(*) AS n FROM media').get().n;
|
|
const t = db.prepare('SELECT COUNT(*) AS n FROM tokens').get().n;
|
|
const u = db.prepare('SELECT COUNT(*) AS n FROM users').get().n;
|
|
console.log(JSON.stringify({ media: m, tokens: t, users: u }));
|
|
"
|
|
find /tmp/p/files -type f | wc -l
|
|
"@) -replace "`r", ''
|
|
|
|
$pruefung = docker run --rm -v "${Ziel}:/in:ro" --entrypoint sh $Abbild -c $skript
|
|
if ($LASTEXITCODE -ne 0) { Schlecht "Pruefung fehlgeschlagen: $pruefung"; exit 1 }
|
|
|
|
$zeilen = @($pruefung -split "`n" | Where-Object { $_.Trim() })
|
|
$zahlen = $zeilen[0] | ConvertFrom-Json
|
|
$dateien = [int]$zeilen[1].Trim()
|
|
|
|
Gut "in der Sicherung: $($zahlen.media) Medieneintraege, $dateien Dateien, $($zahlen.tokens) Token, $($zahlen.users) Benutzer"
|
|
|
|
if ($erwartet -ge 0 -and $zahlen.media -ne $erwartet) {
|
|
Schlecht "PASST NICHT: erwartet waren $erwartet Eintraege."
|
|
exit 1
|
|
}
|
|
# Eine Datei mehr oder weniger als Eintraege ist ein echter Befund und kein
|
|
# Rundungsfehler: dann liegt eine Datei ohne Datensatz herum oder umgekehrt.
|
|
if ($dateien -ne $zahlen.media) {
|
|
Write-Host " Hinweis: $dateien Dateien zu $($zahlen.media) Eintraegen — das sollte gleich sein." -ForegroundColor Yellow
|
|
}
|
|
Gut 'Datenbank in der Sicherung ist lesbar und vollstaendig.'
|
|
|
|
# -- 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
|
|
# und lokal weitergesichert — eine Sicherung, die wegen eines fehlenden
|
|
# Passworts GAR NICHT stattfindet, waere der schlechtere Tausch.
|
|
#
|
|
# Aufbau der Datei (eine Zuweisung je Zeile, # ist ein Kommentar):
|
|
#
|
|
# basis = https://cdn.d4rkst3r.de
|
|
# benutzer = darkster
|
|
# passwort = xxxxx-xxxxx-xxxxx-xxxxx-xxxxx <- APP-PASSWORT, nicht das echte
|
|
# ordner = Sicherungen/d4rk_media
|
|
#
|
|
# Das App-Passwort steht in Nextcloud unter Einstellungen → Sicherheit →
|
|
# "Neues App-Passwort erstellen". Es laesst sich einzeln zurueckziehen, ohne
|
|
# dass das eigene Passwort geaendert werden muss — genau dafuer gibt es sie.
|
|
|
|
# Die Werte kommen aus der Datenbank des Dienstes, eingetragen im Panel unter
|
|
# Einstellungen → Sicherung. Gelesen wird DIREKT ueber `docker exec` und nicht
|
|
# ueber die API: so verlaesst das Passwort den Rechner nie.
|
|
#
|
|
# nextcloud.txt neben den Sicherungen gilt weiterhin und hat VORRANG — wer sie
|
|
# angelegt hat, soll nicht suchen muessen, warum sie ploetzlich ignoriert wird.
|
|
|
|
$z = @{}
|
|
$zugangDatei = Join-Path $Ziel 'nextcloud.txt'
|
|
|
|
if (Test-Path $zugangDatei) {
|
|
foreach ($zeile in Get-Content $zugangDatei) {
|
|
if ($zeile -match '^\s*#') { continue }
|
|
if ($zeile -match '^\s*([a-z]+)\s*=\s*(.+?)\s*$') { $z[$Matches[1]] = $Matches[2] }
|
|
}
|
|
Gut "Zugang aus $zugangDatei"
|
|
} else {
|
|
$roh = docker exec $Container node -e @"
|
|
const db = require('better-sqlite3')('/data/media.db', { readonly: true });
|
|
const hol = (k) => { const r = db.prepare('SELECT value FROM settings WHERE key = ?').get(k); return r ? r.value : ''; };
|
|
console.log(JSON.stringify({
|
|
basis: hol('sicherung.basis'),
|
|
benutzer: hol('sicherung.benutzer'),
|
|
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', '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
|
|
} else {
|
|
if (-not $z['ordner']) { $z['ordner'] = 'Sicherungen/d4rk_media' }
|
|
|
|
$basis = $z['basis'].TrimEnd('/')
|
|
$wurzel = "$basis/remote.php/dav/files/$($z['benutzer'])"
|
|
$auth = "$($z['benutzer']):$($z['passwort'])"
|
|
|
|
Schritt "In die Nextcloud: $($z['ordner'])"
|
|
|
|
# Die Ordner Ebene fuer Ebene anlegen. MKCOL legt NUR EINE an; auf einen
|
|
# fehlenden Zwischenschritt antwortet es mit 409, und dann sucht man den
|
|
# Fehler beim Passwort.
|
|
$pfadBisher = ''
|
|
foreach ($teil in $z['ordner'].Split('/')) {
|
|
if (-not $teil) { continue }
|
|
$pfadBisher = if ($pfadBisher) { "$pfadBisher/$teil" } else { $teil }
|
|
# 201 = angelegt, 405 = gab es schon. Beides ist in Ordnung.
|
|
$code = curl.exe -s -o NUL -w '%{http_code}' -u $auth -X MKCOL "$wurzel/$pfadBisher"
|
|
if ($code -notin @('201', '405')) {
|
|
$fehlertext = "MKCOL $pfadBisher -> HTTP $code"
|
|
if ($code -eq '401') { $fehlertext += ' (Benutzer oder App-Passwort stimmt nicht)' }
|
|
Schlecht $fehlertext
|
|
Melde $false $zahlen.media $groesse $false $fehlertext
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
$fern = "$wurzel/$($z['ordner'])/$name"
|
|
$code = curl.exe -s -o NUL -w '%{http_code}' -u $auth -T $archiv $fern
|
|
if ($code -notin @('201', '204')) {
|
|
Schlecht "Hochladen fehlgeschlagen -> HTTP $code"
|
|
Melde $false $zahlen.media $groesse $false "Hochladen -> HTTP $code"
|
|
exit 1
|
|
}
|
|
Gut "hochgeladen ($code)"
|
|
|
|
# NACHZAEHLEN, auch hier: zurueckholen und die Pruefsumme vergleichen. Ein
|
|
# PUT, der 201 sagt, hat noch nichts bewiesen — abgeschnittene
|
|
# Uebertragungen sehen genauso aus. Bei ein paar Megabyte ist das billig.
|
|
$zurueck = Join-Path $env:TEMP "pruefe-$name"
|
|
curl.exe -s -u $auth -o $zurueck $fern
|
|
$hierHash = (Get-FileHash $archiv -Algorithm SHA256).Hash
|
|
$dortHash = if (Test-Path $zurueck) { (Get-FileHash $zurueck -Algorithm SHA256).Hash } else { '' }
|
|
Remove-Item $zurueck -Force -ErrorAction SilentlyContinue
|
|
|
|
if ($hierHash -ne $dortHash) {
|
|
Schlecht 'Die Kopie in der Nextcloud ist NICHT identisch.'
|
|
Melde $false $zahlen.media $groesse $false 'Pruefsumme der Kopie weicht ab'
|
|
exit 1
|
|
}
|
|
Gut "zurueckgeholt und verglichen: identisch ($($hierHash.Substring(0,12))…)"
|
|
$inDerWolke = $true
|
|
|
|
# Auch draussen ausduennen. Sonst waechst die Nextcloud still voll, und das
|
|
# faellt erst auf, wenn sie voll ist.
|
|
$liste = (curl.exe -s -u $auth -X PROPFIND -H 'Depth: 1' "$wurzel/$($z['ordner'])") -join "`n"
|
|
$fernListe = @([regex]::Matches($liste, 'd4rk_media-[\d-]+\.tar\.gz') |
|
|
ForEach-Object { $_.Value } | Sort-Object -Unique -Descending)
|
|
if ($fernListe.Count -gt $Behalten) {
|
|
foreach ($alt in $fernListe | Select-Object -Skip $Behalten) {
|
|
$c = curl.exe -s -o NUL -w '%{http_code}' -u $auth -X DELETE "$wurzel/$($z['ordner'])/$alt"
|
|
if ($c -in @('204', '200')) { Gut "in der Nextcloud entfernt: $alt" }
|
|
else { Write-Host " konnte $alt nicht entfernen (HTTP $c)" -ForegroundColor Yellow }
|
|
}
|
|
}
|
|
}
|
|
|
|
# -- 5. Alte Sicherungen ausduennen (lokal) ------------------------------------
|
|
|
|
$alle = Get-ChildItem $Ziel -Filter 'd4rk_media-*.tar.gz' | Sort-Object Name -Descending
|
|
if ($alle.Count -gt $Behalten) {
|
|
Schritt "Alte Sicherungen entfernen (mehr als $Behalten)"
|
|
$alle | Select-Object -Skip $Behalten | ForEach-Object {
|
|
Remove-Item $_.FullName -Force
|
|
Gut "entfernt: $($_.Name)"
|
|
}
|
|
}
|
|
|
|
Melde $true $zahlen.media $groesse $inDerWolke $null
|
|
|
|
Write-Host ''
|
|
Gut "Fertig: $archiv"
|
|
Write-Host " Zurueckspielen: tar xzf <archiv>, dann media.db, files/ und papierkorb/" -ForegroundColor DarkGray
|
|
Write-Host " ins Volume legen," -ForegroundColor DarkGray
|
|
Write-Host " waehrend der Container steht. Siehe ROADMAP." -ForegroundColor DarkGray
|