# 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 , 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