Der naechtliche Lauf scheiterte mit HTTP 413. Nachgemessen mit drei Groessen,
statt geraten:
1 KB -> 201 | 60 MB -> 201 | 340 MB -> 413, "Server: cloudflare"
cdn.d4rkst3r.de laeuft ueber Cloudflare, und das deckelt Uploads im Gratistarif
bei 100 MB. Das Archiv ist 336 MB. Weder der Proxy (client_max_body_size 0) noch
die Nextcloud (16 GB) waren es.
Bestellt war ein eigener Name ohne Cloudflare. Vorher aber geprueft, ob es den
ueberhaupt braucht -- und er braucht es nicht: die Nextcloud laeuft auf
DERSELBEN Maschine. Ueber http://localhost:11000 verlaesst der Verkehr die
Rueckschleife nicht, und "localhost" steht schon in trusted_domains. Kein DNS,
kein Zertifikat, kein Proxy, und nebenbei fuenfzehnmal schneller:
ueber 192.168.100.1:11000 340 MB in 59 s
ueber localhost:11000 340 MB in 4 s
Kein TLS dabei, und das ist in Ordnung: die Verbindung geht nicht hinaus.
Ganzer Lauf zur Gegenprobe: 4630 Eintraege, 335,9 MB, zweite Platte identisch,
hochgeladen 201, zurueckgeholt und verglichen identisch. Statusseite sagt
wieder "sicherung ok an 3 Orten".
WICHTIG, DAMIT NIEMAND ES FALSCH LIEST -- und das steht auch im Skript: diese
Kopie ist KEINE Auswaertskopie. Die Nextcloud liegt auf derselben Platte wie
die Daten. Sie ist die dritte Kopie, nicht der Weg aus dem Haus. Der fehlt
weiterhin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
470 lines
21 KiB
PowerShell
470 lines
21 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 localhost UND NICHT DER OEFFENTLICHE NAME. Am 13.08.2026 scheiterte der
|
|
# naechtliche Upload mit HTTP 413. Nachgemessen mit drei Groessen:
|
|
#
|
|
# 1 KB -> 201 | 60 MB -> 201 | 340 MB -> 413, "Server: cloudflare"
|
|
#
|
|
# cdn.d4rkst3r.de laeuft ueber Cloudflare, und das deckelt Uploads im
|
|
# Gratistarif bei 100 MB. Das Archiv ist 336 MB.
|
|
#
|
|
# Der Umweg war ohnehin unnoetig: die Nextcloud laeuft auf DERSELBEN Maschine.
|
|
# Ueber http://localhost:11000 geht der Verkehr gar nicht erst hinaus -- kein
|
|
# Cloudflare, kein Proxy, kein Zertifikat, und nebenbei fuenfzehnmal schneller:
|
|
#
|
|
# ueber 192.168.100.1:11000 340 MB in 59 s
|
|
# ueber localhost:11000 340 MB in 4 s
|
|
#
|
|
# Dass dabei kein TLS im Spiel ist, ist in Ordnung: die Verbindung verlaesst die
|
|
# Rueckschleife nicht. "localhost" steht bereits in Nextclouds trusted_domains.
|
|
#
|
|
# WICHTIG, DAMIT NIEMAND ES FALSCH LIEST: diese Kopie ist KEINE Auswaertskopie.
|
|
# Die Nextcloud liegt im selben Docker-Volume-Bereich auf derselben Platte wie
|
|
# die Daten. Sie ist die dritte Kopie, nicht der Weg aus dem Haus -- der fehlt
|
|
# weiterhin.
|
|
#
|
|
# 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
|