fix: die Sicherung liess den Papierkorb aus -- ein Versprechen, das sie nicht halten konnte
GEFUNDEN BEIM NACHSEHEN, nicht beim Suchen: das Sicherungsskript packte
tar czf ... -C /data files -C /data/.sicherung media.db
also die Bilder und die Datenbank. Die EINTRAEGE des Papierkorbs 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: die Oberflaeche verspricht etwas, das die
Platte nicht hergibt. Genau die Sorte stiller Abweichung, wegen der dieses
Projekt Sicherungen ueberhaupt gegenprueft.
Er ist jetzt im Archiv, und er kostet fast nichts: was darin liegt, ist
hoechstens 30 Tage alt, danach raeumt der Dienst selbst auf. Die Abfrage
`if [ -d /data/papierkorb ]` davor ist noetig -- hat noch nie jemand etwas
geloescht, gibt es den Ordner gar nicht, und tar braeche mit "Cannot stat" ab.
Ein `mkdir` als Ausweg ginge nicht: das Volume haengt nur lesend.
Gemessen an einem echten Lauf, nicht angenommen:
d4rk_media-2026-08-11-2307.tar.gz -- 311,9 MB
in der Sicherung: 4452 Medieneintraege, 4452 Dateien, 2 Token, 2 Benutzer
hochgeladen (201), zurueckgeholt und verglichen: identisch
Inhalt: 4459 files/ 1 media.db 1 papierkorb/
UND DER PAPIERKORB SAGT ES JETZT SELBST. Die Liste prueft je Zeile, ob die
Datei wirklich noch daliegt (hoechstens 500 stat-Aufrufe), und zeigt fehlende
durchgestrichen samt Erklaerung. Die Sicherung nimmt ihn zwar inzwischen mit --
aber ein Papierkorb, der etwas verspricht, muss es halten koennen, und wenn
nicht, soll er es SAGEN statt es beim Druecken herauszufinden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -178,10 +178,16 @@ Wiki die Kopie. Wer dort tippt, verliert es beim nächsten Lauf.
|
||||
tar xzf d4rk_media-JJJJ-MM-TT-HHMM.tar.gz
|
||||
```
|
||||
|
||||
Ergibt `media.db` und `files/`. Beides ins Volume legen, während der Container
|
||||
steht. Vorschaubilder und WebP-Fassungen sind **nicht** im Archiv — beides ist
|
||||
abgeleitet und wird mit zwei Knöpfen unter *Speicher → Wartung* neu gerechnet.
|
||||
Der Papierkorb ist ebenfalls nicht drin: was dort liegt, ist gelöscht.
|
||||
Ergibt `media.db`, `files/` und `papierkorb/`. Alles drei ins Volume legen,
|
||||
während der Container steht. Vorschaubilder und WebP-Fassungen sind **nicht** im
|
||||
Archiv — beides ist abgeleitet und wird mit zwei Knöpfen unter
|
||||
*Speicher → Wartung* neu gerechnet.
|
||||
|
||||
> Der Papierkorb war anfangs **nicht** dabei, und das war ein Fehler: seine
|
||||
> Einträge stehen in `media.db` und waren damit gesichert, die Dateien lagen
|
||||
> daneben und waren es nicht. Nach einem Zurückspielen hätte der Papierkorb
|
||||
> Zeilen gezeigt, deren „zurückholen" ins Leere greift. Er kostet fast nichts —
|
||||
> was darin liegt, ist höchstens 30 Tage alt.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1746,14 +1746,41 @@ export async function papierkorbAusleeren(): Promise<number> {
|
||||
return alt.length
|
||||
}
|
||||
|
||||
dashRoutes.get('/papierkorb', (c) => {
|
||||
const items = db
|
||||
dashRoutes.get('/papierkorb', async (c) => {
|
||||
const roh = db
|
||||
.prepare('SELECT * FROM papierkorb ORDER BY geloescht DESC LIMIT 500')
|
||||
.all() as PapierkorbEintrag[]
|
||||
|
||||
// OB DIE DATEI WIRKLICH NOCH DA IST -- ein stat je Zeile, hoechstens 500.
|
||||
//
|
||||
// Der Eintrag steht in media.db, die Datei liegt daneben auf der Platte:
|
||||
// zwei Wahrheiten, die auseinanderlaufen koennen. Genau das ist passiert,
|
||||
// solange die Sicherung /data/papierkorb ausliess -- nach einem
|
||||
// Zurueckspielen zeigte der Papierkorb Zeilen, deren "zurueckholen" ins
|
||||
// Leere griff. Die Sicherung nimmt ihn inzwischen mit; DIESE Pruefung
|
||||
// bleibt trotzdem, weil ein Papierkorb, der etwas verspricht, es auch
|
||||
// halten koennen muss -- und wenn nicht, es sagen soll, statt es erst beim
|
||||
// Druecken herauszufinden.
|
||||
const items = await Promise.all(
|
||||
roh.map(async (e) => {
|
||||
try {
|
||||
await stat(papierkorbPfad(e.ablage))
|
||||
return { ...e, dateiDa: true }
|
||||
} catch {
|
||||
return { ...e, dateiDa: false }
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const summe = db
|
||||
.prepare('SELECT COUNT(*) AS n, COALESCE(SUM(size),0) AS bytes FROM papierkorb')
|
||||
.get() as { n: number; bytes: number }
|
||||
return c.json({ items, ...summe, haltbarTage: PAPIERKORB_TAGE })
|
||||
return c.json({
|
||||
items,
|
||||
...summe,
|
||||
haltbarTage: PAPIERKORB_TAGE,
|
||||
verwaist: items.filter((e) => !e.dateiDa).length,
|
||||
})
|
||||
})
|
||||
|
||||
/** Zurueck an ihren Platz.
|
||||
|
||||
@@ -140,6 +140,10 @@ export type PapierkorbEintrag = {
|
||||
mime: string
|
||||
geloescht: number
|
||||
von: string
|
||||
/** Liegt die Datei wirklich noch im Papierkorb? Der Eintrag steht in der
|
||||
* Datenbank, die Datei daneben auf der Platte — zwei Wahrheiten, die
|
||||
* auseinanderlaufen koennen. */
|
||||
dateiDa: boolean
|
||||
}
|
||||
|
||||
export type PapierkorbSeite = {
|
||||
@@ -147,6 +151,8 @@ export type PapierkorbSeite = {
|
||||
n: number
|
||||
bytes: number
|
||||
haltbarTage: number
|
||||
/** Eintraege ohne Datei. Sollte 0 sein. */
|
||||
verwaist: number
|
||||
}
|
||||
|
||||
export type Speicher = {
|
||||
|
||||
@@ -105,6 +105,20 @@ export function Papierkorb() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{daten.verwaist > 0 && (
|
||||
<p className="flex items-start gap-2 rounded-md border border-warn/40 bg-warn/10
|
||||
px-3 py-2 text-xs text-warn">
|
||||
<AlertTriangle size={14} className="mt-0.5 shrink-0" />
|
||||
<span>
|
||||
Bei <strong>{daten.verwaist}</strong> {daten.verwaist === 1 ? 'Eintrag' : 'Einträgen'}{' '}
|
||||
fehlt die Datei auf der Platte — zurückholen geht dort nicht. Das kommt
|
||||
vor, wenn eine Sicherung zurückgespielt wurde, die den Papierkorb nicht
|
||||
enthielt. Sie stehen unten durchgestrichen; „endgültig löschen" räumt sie
|
||||
auf.
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Karte>
|
||||
{items.length === 0 ? (
|
||||
<Leer
|
||||
@@ -131,7 +145,10 @@ export function Papierkorb() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((e) => (
|
||||
<tr key={e.id} className="border-t border-rand">
|
||||
<tr
|
||||
key={e.id}
|
||||
className={`border-t border-rand ${e.dateiDa ? '' : 'opacity-50'}`}
|
||||
>
|
||||
<td className="px-3 py-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -148,8 +165,14 @@ export function Papierkorb() {
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="max-w-0 truncate px-3 py-1.5 font-mono text-xs"
|
||||
title={e.path}
|
||||
className={`max-w-0 truncate px-3 py-1.5 font-mono text-xs ${
|
||||
e.dateiDa ? '' : 'line-through'
|
||||
}`}
|
||||
title={
|
||||
e.dateiDa
|
||||
? e.path
|
||||
: `${e.path} — die Datei fehlt auf der Platte`
|
||||
}
|
||||
>
|
||||
{e.path}
|
||||
</td>
|
||||
|
||||
+22
-5
@@ -114,13 +114,29 @@ $erwartet = if ($ausgabe -match '(\d+) Medieneintraege') { [int]$Matches[1] } el
|
||||
# -- 2. Alles in ein Archiv ----------------------------------------------------
|
||||
|
||||
Schritt 'Bilder und Datenbank packen'
|
||||
# Zwei -C: die Bilder kommen als files/... in das Archiv, die Aufnahme als
|
||||
# media.db daneben. Bewusst das eigene Abbild statt alpine -- dort ist GNU tar
|
||||
# drin, und das kann mehrere -C.
|
||||
# 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 "tar czf '/out/$name' -C /data files -C /data/.sicherung media.db"
|
||||
--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
|
||||
@@ -315,5 +331,6 @@ Melde $true $zahlen.media $groesse $inDerWolke $null
|
||||
|
||||
Write-Host ''
|
||||
Gut "Fertig: $archiv"
|
||||
Write-Host " Zurueckspielen: tar xzf <archiv>, dann media.db und files/ ins Volume legen," -ForegroundColor DarkGray
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user