/health gab es und bleibt, wie es ist: app.get('/health', c => c.json({ok:true})).
Diese Zeile beweist genau eines -- der Prozess nimmt Anfragen an. Daran haengt
der HEALTHCHECK des Containers, und DORT ist billig richtig: eine schwere
Pruefung, die bei einer langsamen Platte einmal ausfaellt, liesse Docker den
Container neu starten, also genau dann, wenn er unter Last steht.
Fuer eine Statusseite ist das zu duenn -- sie stuende auf Gruen, waehrend die
Platte voll ist und kein Upload mehr angenommen wird.
/status sieht deshalb wirklich nach: Datenbank (eine echte Abfrage, nicht "die
Datei ist da"), Platte (schreiben UND wieder loeschen, der einzige Beweis),
Ausliefern (eine zufaellige Datei aus der Datenbank auf der Platte nachmessen),
Bestand, Platz und Sicherung. 200 wenn der Dienst sein Geschaeft tut, 503 wenn
nicht; ?streng=1 laesst auch eine Beeintraechtigung rot werden -- WELCHES von
beiden richtig ist, weiss nur, wer die Statusseite betreibt.
Oeffentlich, aber wortkarg: keine Dateizahlen, Groessen, Pfade, Tokennamen,
Benutzer. Und mit einer Zehn-Sekunden-Bremse -- ein oeffentlicher Endpunkt, der
auf die Platte schreibt, waere sonst ein Verstaerker.
UND DABEI FIEL EIN FEHLER IN MEINEM EIGENEN ZURUECKSPIEL-SKRIPT AUF.
Der harte Weg sollte an einem Wegwerf-Container geprueft werden. Der traf
zufaellig auf ein GEBRAUCHTES Volume, und die Zahlen waren eindeutig:
media.db aus dem Archiv, mit altem WAL daneben : 0 Zeilen
dieselbe Datei ohne die beiden Begleiter : 4452 Zeilen
Dateien auf der Platte : 4452
zurueckspielen.ps1 entfernte media.db, aber NICHT media.db-wal und
media.db-shm. Die liegen bei einem echten Zurueckspielen immer da -- der
laufende Dienst arbeitet im WAL-Modus. SQLite spielt das WAL der ALTEN
Datenbank ueber die NEUE, und heraus kommt der schlimmste denkbare Zustand: der
Dienst kommt hoch, /health ist gruen, die Mediathek ist leer, waehrend alle
Dateien danebenliegen.
Drei Konsequenzen:
1. Die Aufraeumzeile steht jetzt an EINER Stelle und nimmt media.db-wal,
media.db-shm und *.tmp mit. Uebung und Ernstfall fahren denselben Befehl --
zwei Fassungen waeren zwei, von denen die geuebte die harmlosere ist.
2. Die Uebung TAEUSCHT JETZT EINE BESTEHENDE INSTALLATION VOR, bevor sie
zurueckspielt: Container starten, warten bis media.db-wal daliegt, stoppen,
und erst dann einspielen. In ein leeres Volume zu spielen probt den Fall,
der nie eintritt.
3. /status erkennt den Zustand selbst -- "kein Eintrag in der Datenbank, aber
Dateien auf der Platte". Am kaputten Container gemessen:
/health sagt: HTTP 200
/status sagt: HTTP 503
Der geuebte Lauf danach, ueber eine vorgetaeuschte Installation:
im Volume liegt jetzt: files media.db media.db-shm media.db-wal
4452 Medieneintraege, 4452 Dateien -- gleich viele
ok items/shushi.png · items/weedbud_1.png · items/cc-castella.png
Die Uebung ist bestanden.
Gefunden beim Ueben und nicht im Ernstfall. Genau dafuer gibt es sie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
339 lines
16 KiB
PowerShell
339 lines
16 KiB
PowerShell
# Eine Sicherung zurueckspielen -- geuebt oder im Ernst.
|
|
#
|
|
# pwsh -File tools\zurueckspielen.ps1 <- UEBUNG (Standard)
|
|
# pwsh -File tools\zurueckspielen.ps1 -Echt <- der Ernstfall
|
|
#
|
|
# WOZU DAS UEBERHAUPT. sichern.ps1 prueft sein Archiv gruendlich: auspacken,
|
|
# Datenbank oeffnen, Zeilen zaehlen, hochladen, zurueckholen, Pruefsumme
|
|
# vergleichen. Das beantwortet die Frage "ist die Datei heil?".
|
|
#
|
|
# Es beantwortet NICHT die Frage, auf die es ankommt: "kommt daraus wieder ein
|
|
# laufender Dienst?" Dazwischen liegen Handgriffe, die man sonst zum ersten Mal
|
|
# an dem Abend macht, an dem man sie unter Druck macht -- wie das Volume
|
|
# heisst, dass der Container stehen muss, wem die Dateien gehoeren muessen,
|
|
# dass die Vorschauen abgeleitet sind und neu gerechnet werden wollen.
|
|
#
|
|
# EIN BACKUP, DAS MAN NIE ZURUECKGESPIELT HAT, IST KEIN BACKUP, SONDERN EINE
|
|
# DATEI.
|
|
#
|
|
# DIE UEBUNG ist deshalb der Standard und der eigentliche Gewinn: sie legt ein
|
|
# WEGWERF-VOLUME an, startet einen ZWEITEN Container davor, laesst ihn den
|
|
# Bestand ausliefern und raeumt hinterher auf. Der echte Dienst wird dabei
|
|
# nicht angefasst -- er laeuft die ganze Zeit weiter.
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
# Ohne Angabe: das neueste Archiv im Sicherungsordner.
|
|
[string]$Archiv = '',
|
|
[string]$Ordner = 'C:\backup\d4rk_media',
|
|
[string]$Container = 'd4rk-media',
|
|
[string]$Volume = 'd4rk_media_media-data',
|
|
[string]$Abbild = 'd4rk-media:latest',
|
|
# Der Ernstfall. Ohne diesen Schalter passiert dem laufenden Dienst nichts.
|
|
[switch]$Echt
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# WAS BEIM ZURUECKSPIELEN WEG MUSS -- an EINER Stelle, weil die Uebung genau
|
|
# denselben Befehl fahren soll wie der Ernstfall. Zwei Fassungen waeren zwei,
|
|
# von denen die geuebte die harmlosere ist.
|
|
#
|
|
# media.db-wal UND media.db-shm STEHEN HIER MIT ABSICHT, und das ist teuer
|
|
# gelernt: SQLite laeuft in diesem Dienst im WAL-Modus, neben der Datenbank
|
|
# liegen also immer zwei Begleitdateien. Die Sicherung enthaelt sie NICHT --
|
|
# sie braucht sie auch nicht, `db.backup()` schreibt eine fertig
|
|
# zusammengefuehrte Datei.
|
|
#
|
|
# Wer beim Zurueckspielen nur media.db ersetzt, laesst damit das WAL der ALTEN
|
|
# Datenbank neben der NEUEN liegen. SQLite spielt es beim Oeffnen ein, und das
|
|
# Ergebnis ist das Schlimmste, was hier passieren kann:
|
|
#
|
|
# media.db aus dem Archiv, mit altem WAL daneben : 0 Zeilen
|
|
# dieselbe Datei ohne die beiden Begleiter : 4452 Zeilen
|
|
# Dateien auf der Platte : 4452
|
|
#
|
|
# Der Dienst kaeme HOCH, /health waere gruen, und die Mediathek waere leer --
|
|
# waehrend alle Dateien danebenliegen. Nachgemessen am 12.08.2026, gefunden
|
|
# beim Ueben und nicht im Ernstfall. Genau dafuer gibt es die Uebung.
|
|
#
|
|
# .tmp ebenfalls: writeFileAtomic legt Zwischendateien an, und ein Absturz
|
|
# mitten im Schreiben laesst eine liegen.
|
|
$script:AUFRAEUMEN = 'rm -rf files media.db media.db-wal media.db-shm papierkorb thumbs webp *.tmp'
|
|
|
|
function Schritt($t) { Write-Host "==> $t" -ForegroundColor Cyan }
|
|
function Gut($t) { Write-Host " $t" -ForegroundColor Green }
|
|
function Schlecht($t) { Write-Host " $t" -ForegroundColor Red }
|
|
function Grau($t) { Write-Host " $t" -ForegroundColor DarkGray }
|
|
|
|
# --------------------------------------------------------------- Das Archiv
|
|
|
|
if (-not $Archiv) {
|
|
$neuestes = Get-ChildItem "$Ordner\*.tar.gz" -ErrorAction SilentlyContinue |
|
|
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
if (-not $neuestes) {
|
|
Schlecht "Kein Archiv in $Ordner. Erst tools\sichern.ps1 laufen lassen."
|
|
exit 1
|
|
}
|
|
$Archiv = $neuestes.FullName
|
|
}
|
|
if (-not (Test-Path $Archiv)) { Schlecht "Nicht gefunden: $Archiv"; exit 1 }
|
|
|
|
$archivDatei = Split-Path $Archiv -Leaf
|
|
$archivOrdner = Split-Path $Archiv -Parent
|
|
$mb = [math]::Round((Get-Item $Archiv).Length / 1MB, 1)
|
|
|
|
Write-Host ''
|
|
Schritt "Archiv: $archivDatei ($mb MB, vom $((Get-Item $Archiv).LastWriteTime))"
|
|
|
|
# Erst hineinsehen. Ein Archiv ohne media.db ist kein Archiv dieses Dienstes,
|
|
# und das soll auffallen, BEVOR irgendwo etwas geloescht wird.
|
|
$inhalt = docker run --rm -v "${archivOrdner}:/in:ro" --entrypoint sh $Abbild `
|
|
-c "tar tzf '/in/$archivDatei' | cut -d/ -f1 | sort | uniq -c"
|
|
if ($LASTEXITCODE -ne 0) { Schlecht 'Das Archiv liess sich nicht lesen.'; exit 1 }
|
|
$inhalt | ForEach-Object { Grau $_.Trim() }
|
|
|
|
if (($inhalt -join "`n") -notmatch 'media\.db') {
|
|
Schlecht 'In diesem Archiv steckt keine media.db. Falsche Datei?'
|
|
exit 1
|
|
}
|
|
|
|
# ------------------------------------------------------------------ Pruefen
|
|
#
|
|
# Egal ob Uebung oder Ernst: erst wird geprueft, was DRIN ist. Beides laeuft
|
|
# ueber denselben Weg -- ein zweiter waere einer, der beim naechsten Mal etwas
|
|
# anderes prueft als der erste.
|
|
|
|
$pruefen = @"
|
|
set -e
|
|
cd /app
|
|
node -e "
|
|
const db = require('better-sqlite3')('/data/media.db', { readonly: true, fileMustExist: true });
|
|
const n = db.prepare('SELECT COUNT(*) AS n FROM media').get().n;
|
|
const b = db.prepare('SELECT COALESCE(SUM(size),0) AS b FROM media').get().b;
|
|
const t = db.prepare('SELECT COUNT(*) AS n FROM tokens').get().n;
|
|
const u = db.prepare('SELECT COUNT(*) AS n FROM users').get().n;
|
|
const k = db.prepare('SELECT COUNT(*) AS n FROM papierkorb').get().n;
|
|
console.log(JSON.stringify({ medien: n, bytes: b, tokens: t, benutzer: u, papierkorb: k }));
|
|
"
|
|
"@ -replace "`r`n", "`n"
|
|
|
|
# ======================================================================= UEBUNG
|
|
if (-not $Echt) {
|
|
$pVolume = 'd4rk-media-probe'
|
|
$pContainer = 'd4rk-media-probe'
|
|
|
|
# Aufraeumen, falls ein frueherer Lauf abgebrochen ist. Sonst scheitert das
|
|
# Anlegen an einem Rest, den niemand mehr erwartet.
|
|
docker rm -f $pContainer 2>&1 | Out-Null
|
|
docker volume rm $pVolume 2>&1 | Out-Null
|
|
|
|
try {
|
|
Schritt 'Wegwerf-Volume anlegen'
|
|
docker volume create $pVolume | Out-Null
|
|
|
|
# ------------------------------------------------------------------
|
|
# ERST EINE BESTEHENDE INSTALLATION VORTAEUSCHEN, und das ist der
|
|
# Kern der Uebung: im Ernstfall wird ueber etwas zurueckgespielt, das
|
|
# schon da ist -- mit einer laufenden Datenbank, mit media.db-wal
|
|
# daneben, mit Vorschauen. In ein LEERES Volume zu spielen probt genau
|
|
# den Fall, der nie eintritt.
|
|
#
|
|
# Dass das ein Unterschied ist, ist nicht theoretisch: die erste
|
|
# Fassung dieses Skripts liess media.db-wal stehen, und der Dienst kam
|
|
# danach mit einer LEEREN Mediathek hoch, waehrend alle Dateien
|
|
# danebenlagen. Aufgefallen ist es nur, weil ein Uebungslauf zufaellig
|
|
# auf ein gebrauchtes Volume traf.
|
|
Schritt 'Eine bestehende Installation vortaeuschen (damit die Uebung eine ist)'
|
|
docker run -d --name "$pContainer-alt" `
|
|
-e PUBLIC_URL=http://probe.invalid -e ADMIN_PASSWORD=nur-fuer-die-uebung `
|
|
-v "${pVolume}:/data" $Abbild | Out-Null
|
|
Start-Sleep -Seconds 4
|
|
docker stop "$pContainer-alt" 2>&1 | Out-Null
|
|
docker rm "$pContainer-alt" 2>&1 | Out-Null
|
|
$begleiter = docker run --rm -v "${pVolume}:/data" --entrypoint sh $Abbild `
|
|
-c "ls /data | tr '\n' ' '"
|
|
Gut "im Volume liegt jetzt: $($begleiter.Trim())"
|
|
|
|
Schritt 'Zurueckspielen -- mit DEMSELBEN Befehl wie der Ernstfall'
|
|
docker run --rm -v "${pVolume}:/data" -v "${archivOrdner}:/in:ro" `
|
|
--entrypoint sh $Abbild -c "cd /data && $script:AUFRAEUMEN && tar xzf '/in/$archivDatei' && chown -R node:node /data"
|
|
if ($LASTEXITCODE -ne 0) { throw 'Auspacken fehlgeschlagen' }
|
|
Gut 'eingespielt'
|
|
|
|
Schritt 'Einen zweiten Dienst davorhaengen (ohne Port nach aussen)'
|
|
# KEIN veroeffentlichter Port: der Uebungslauf soll sich mit nichts
|
|
# anlegen, was schon laeuft. Geredet wird ueber docker exec.
|
|
# NODE_ENV NICHT auf production: sonst waere das Sitzungs-Cookie
|
|
# "Secure", und ueber http://127.0.0.1 im Container faende keine
|
|
# Anmeldung statt. Hier wird ohnehin nur ausgeliefert.
|
|
docker run -d --name $pContainer `
|
|
-e PUBLIC_URL=http://probe.invalid `
|
|
-e ADMIN_PASSWORD=nur-fuer-die-uebung `
|
|
-v "${pVolume}:/data" $Abbild | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw 'Der Probe-Container startete nicht' }
|
|
|
|
# Warten, bis er antwortet. Nicht blind schlafen -- fragen.
|
|
$lebt = $false
|
|
foreach ($i in 1..30) {
|
|
Start-Sleep -Milliseconds 700
|
|
$a = docker exec $pContainer node -e "fetch('http://127.0.0.1:8080/health').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))" 2>&1
|
|
if ($LASTEXITCODE -eq 0) { $lebt = $true; break }
|
|
}
|
|
if (-not $lebt) {
|
|
Schlecht 'Der Dienst kam aus dieser Sicherung NICHT hoch.'
|
|
Grau 'Protokoll:'
|
|
docker logs $pContainer --tail 25 2>&1 | ForEach-Object { Grau $_ }
|
|
throw 'Uebung fehlgeschlagen'
|
|
}
|
|
Gut 'Der Dienst ist hochgekommen und antwortet auf /health'
|
|
|
|
Schritt 'Nachzaehlen, was drin ist'
|
|
$roh = docker exec $pContainer sh -c $pruefen
|
|
if ($LASTEXITCODE -ne 0) { throw 'Die Datenbank liess sich nicht lesen' }
|
|
$z = $roh | ConvertFrom-Json
|
|
Gut "$($z.medien) Medieneintraege, $([math]::Round($z.bytes/1MB,1)) MB, $($z.tokens) Token, $($z.benutzer) Benutzer, $($z.papierkorb) im Papierkorb"
|
|
|
|
Schritt 'Zaehlen, was WIRKLICH auf der Platte liegt'
|
|
$aufPlatte = [int](docker exec $pContainer sh -c "find /data/files -type f | wc -l").Trim()
|
|
$passt = ($aufPlatte -eq $z.medien)
|
|
if ($passt) { Gut "$aufPlatte Dateien -- gleich viele wie Eintraege" }
|
|
else { Schlecht "$aufPlatte Dateien gegen $($z.medien) Eintraege -- das passt NICHT zusammen" }
|
|
|
|
# ---------------------------------------------------------------------
|
|
# DIE EIGENTLICHE PRUEFUNG: nicht "liegen Dateien da", sondern "liefert
|
|
# der Dienst die richtigen Bytes aus". Dazu wird eine zufaellige Datei
|
|
# durch den wiederhergestellten Dienst geholt und ihr SHA-256 gegen den
|
|
# verglichen, der in der Datenbank steht.
|
|
#
|
|
# Das ist der Unterschied zwischen "das Archiv ist heil" und "daraus
|
|
# wird wieder ein Dienst".
|
|
Schritt 'Drei zufaellige Dateien DURCH den Dienst holen und vergleichen'
|
|
$holen = @"
|
|
cd /app
|
|
node -e "
|
|
const db = require('better-sqlite3')('/data/media.db', { readonly: true });
|
|
const { createHash } = require('node:crypto');
|
|
const zeilen = db.prepare('SELECT path, sha256, size FROM media ORDER BY RANDOM() LIMIT 3').all();
|
|
(async () => {
|
|
let schlecht = 0;
|
|
for (const z of zeilen) {
|
|
const r = await fetch('http://127.0.0.1:8080/f/' + z.path.split('/').map(encodeURIComponent).join('/'));
|
|
if (!r.ok) { console.log('FEHLT ' + z.path + ' HTTP ' + r.status); schlecht++; continue; }
|
|
const buf = Buffer.from(await r.arrayBuffer());
|
|
const hash = createHash('sha256').update(buf).digest('hex');
|
|
if (hash === z.sha256) console.log('ok ' + z.path + ' ' + buf.length + ' B');
|
|
else { console.log('ANDERS ' + z.path + ' erwartet ' + z.sha256.slice(0,12) + ', bekommen ' + hash.slice(0,12)); schlecht++; }
|
|
}
|
|
process.exit(schlecht === 0 ? 0 : 1);
|
|
})();
|
|
"
|
|
"@ -replace "`r`n", "`n"
|
|
$bytesOk = $true
|
|
docker exec $pContainer sh -c $holen | ForEach-Object {
|
|
if ($_ -match '^ok') { Gut $_ } else { Schlecht $_; $script:bytesOk = $false }
|
|
}
|
|
if ($LASTEXITCODE -ne 0) { $bytesOk = $false }
|
|
|
|
Write-Host ''
|
|
if ($passt -and $bytesOk) {
|
|
Write-Host ' Die Uebung ist bestanden.' -ForegroundColor Green
|
|
Write-Host ' Aus dieser Sicherung wird wieder ein laufender Dienst,' -ForegroundColor Green
|
|
Write-Host ' und er liefert dieselben Bytes aus.' -ForegroundColor Green
|
|
Write-Host ''
|
|
Grau 'Der echte Dienst wurde dabei nicht angefasst.'
|
|
Grau 'Fuer den Ernstfall: pwsh -File tools\zurueckspielen.ps1 -Echt'
|
|
} else {
|
|
Write-Host ' DIE UEBUNG IST NICHT BESTANDEN.' -ForegroundColor Red
|
|
Write-Host ' Diese Sicherung wuerde im Ernstfall nicht reichen.' -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
} finally {
|
|
# Aufraeumen IMMER -- auch nach einem Abbruch. Ein liegengebliebenes
|
|
# Uebungs-Volume mit 300 MB findet sonst niemand wieder.
|
|
Schritt 'Aufraeumen'
|
|
docker rm -f "$pContainer-alt" 2>&1 | Out-Null
|
|
docker rm -f $pContainer 2>&1 | Out-Null
|
|
docker volume rm $pVolume 2>&1 | Out-Null
|
|
Gut 'Wegwerf-Volume und Probe-Container entfernt'
|
|
}
|
|
Write-Host ''
|
|
exit 0
|
|
}
|
|
|
|
# ======================================================================= ERNST
|
|
|
|
Write-Host ''
|
|
Write-Host ' ACHTUNG: das ist der Ernstfall.' -ForegroundColor Yellow
|
|
Write-Host ''
|
|
Write-Host " Der Container $Container wird ANGEHALTEN."
|
|
Write-Host " Im Volume $Volume werden media.db, files/ und papierkorb/"
|
|
Write-Host ' durch den Inhalt des Archivs ERSETZT. Alles, was seit'
|
|
Write-Host " $((Get-Item $Archiv).LastWriteTime) dazugekommen ist, ist danach weg."
|
|
Write-Host ''
|
|
Write-Host ' thumbs/ und webp/ werden geloescht. Beides ist abgeleitet und'
|
|
Write-Host ' wird mit zwei Knoepfen unter Speicher -> Wartung neu gerechnet --'
|
|
Write-Host ' stehenzulassen waere schlimmer: dann zeigte die Galerie die'
|
|
Write-Host ' Vorschau einer Datei, die dort nicht mehr liegt.'
|
|
Write-Host ''
|
|
Write-Host ' Hast du die Uebung laufen lassen? (ohne -Echt)' -ForegroundColor Yellow
|
|
Write-Host ''
|
|
$antwort = Read-Host ' Zum Fortfahren "ZURUECKSPIELEN" eintippen'
|
|
if ($antwort -ne 'ZURUECKSPIELEN') {
|
|
Write-Host ' Abgebrochen. Es wurde nichts angefasst.' -ForegroundColor Green
|
|
exit 0
|
|
}
|
|
|
|
Schritt "Container $Container anhalten"
|
|
docker stop $Container | Out-Null
|
|
Gut 'steht'
|
|
|
|
try {
|
|
Schritt 'Alten Bestand entfernen und Archiv einspielen'
|
|
# In EINEM Container-Aufruf: zwischen Loeschen und Einspielen darf nichts
|
|
# liegen, in dem jemand den Dienst wieder startet.
|
|
docker run --rm -v "${Volume}:/data" -v "${archivOrdner}:/in:ro" --entrypoint sh $Abbild `
|
|
-c "cd /data && $script:AUFRAEUMEN && tar xzf '/in/$archivDatei' && chown -R node:node /data"
|
|
if ($LASTEXITCODE -ne 0) { throw 'Einspielen fehlgeschlagen' }
|
|
Gut 'eingespielt'
|
|
} catch {
|
|
Schlecht $_.Exception.Message
|
|
Schlecht "Der Container ist ANGEHALTEN. Starte ihn mit: docker start $Container"
|
|
exit 1
|
|
}
|
|
|
|
Schritt "Container $Container starten"
|
|
docker start $Container | Out-Null
|
|
|
|
$lebt = $false
|
|
foreach ($i in 1..40) {
|
|
Start-Sleep -Milliseconds 700
|
|
$a = docker exec $Container node -e "fetch('http://127.0.0.1:8080/health').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))" 2>&1
|
|
if ($LASTEXITCODE -eq 0) { $lebt = $true; break }
|
|
}
|
|
if (-not $lebt) {
|
|
Schlecht 'Der Dienst kam nicht hoch. Protokoll:'
|
|
docker logs $Container --tail 30 2>&1 | ForEach-Object { Grau $_ }
|
|
exit 1
|
|
}
|
|
Gut 'laeuft und antwortet auf /health'
|
|
|
|
Schritt 'Nachzaehlen'
|
|
$roh = docker exec $Container sh -c $pruefen
|
|
$z = $roh | ConvertFrom-Json
|
|
$aufPlatte = [int](docker exec $Container sh -c "find /data/files -type f | wc -l").Trim()
|
|
Gut "$($z.medien) Medieneintraege, $aufPlatte Dateien, $($z.tokens) Token, $($z.benutzer) Benutzer"
|
|
if ($aufPlatte -ne $z.medien) {
|
|
Schlecht "Eintraege und Dateien passen nicht zusammen ($($z.medien) gegen $aufPlatte)."
|
|
Schlecht 'Dashboard -> Speicher -> "Verwaiste suchen" sagt, welche.'
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Host ' Zurueckgespielt.' -ForegroundColor Green
|
|
Write-Host ''
|
|
Write-Host ' NOCH ZU TUN -- Abgeleitetes ist nicht im Archiv:' -ForegroundColor Yellow
|
|
Write-Host ' Dashboard -> Speicher -> Wartung'
|
|
Write-Host ' "Fehlende Vorschaubilder erzeugen"'
|
|
Write-Host ' "Sparsame Fassungen erzeugen"'
|
|
Write-Host ''
|