feat: die Archive lassen sich aus dem Panel holen
Einstellungen -> "Sicherungen holen": die fertigen Archive mit Datum, Groesse
und einem Knopf. Der erste Schritt, der eine Kopie VON DIESEM RECHNER HERUNTER
bringt -- bisher haette man sich dafuer per Fernwartung auf den Server klicken
muessen, und genau daran hat es gelegen. Nicht Unwille, sondern
Umstaendlichkeit.
Der Ordner haengt NUR LESEND in den Container. Ein Dienst, der seine eigenen
Sicherungen loeschen kann, ist genau das Werkzeug, das man einem uebernommenen
Dienst nicht in die Hand geben will. Nachgemessen im Container:
touch /sicherungen/probe -> Read-only file system
Und es gibt NUR EINE FORM VON NAMEN -- nicht "alles ausser .. und /", sondern
ein Muster, das genau auf die eigenen Archive passt. Wer einen Pfad
hineinschmuggeln will, muesste ihn erst so benennen. Das ist der Unterschied
zwischen dicht gemacht und dicht gebaut:
../../etc/passwd -> 404
..%2f..%2fdata%2fmedia.db -> 400
media.db -> 400
<archiv>.tar.gz.evil -> 400
ohne Anmeldung -> 401
Der ganze Weg einmal wirklich gegangen, nicht nur die Kopfzeilen: 336 MB in
15,9 s durch den Proxy, sha256 auf der Platte gleich sha256 heruntergeladen,
tar tzf zeigt 4639 Eintraege. Gestroemt und nicht in den Speicher gelesen --
sonst haetten Dienst UND Proxy die 336 MB gleichzeitig im Arbeitsspeicher.
Jeder Griff steht im Verwaltungs-Verlauf. Geloggt wird der GRIFF und nicht der
Erfolg: ein abgebrochener Download taucht ebenfalls auf, und das ist gewollt --
wer spaeter fragt, wer sich den Bestand geholt hat, will die Anfrage sehen.
Die fuenf neuen Pruefungen sind in tools/rundgang.py mit drin; der steht jetzt
bei 29 in Ordnung, 0 nicht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ import {
|
||||
} from '../auth.js'
|
||||
import { mkdir, readdir, readFile, rm, rmdir, stat } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import {
|
||||
DISCORD_KEYS,
|
||||
MARKE_KEY,
|
||||
@@ -1938,6 +1939,79 @@ const GEHEIM = new Set<string>([DISCORD_KEYS.clientSecret, SICHERUNG_KEYS.passwo
|
||||
* behaupten, die es dort nie gab. */
|
||||
const NICHT_MITNEHMEN = [/^sicherung\.letzte$/, /^discord\.zuletzt/]
|
||||
|
||||
// ------------------------------------------------------- Sicherungen holen
|
||||
//
|
||||
// WOZU. Die Archive liegen auf dem WIRT, nicht im Container -- und wer sie
|
||||
// woanders haben will, musste sich bisher per Fernwartung auf den Server
|
||||
// klicken und sie von Hand ziehen. Das ist der Grund, aus dem es keine Kopie
|
||||
// ausserhalb dieser Maschine gibt: nicht Unwillen, sondern Umstaendlichkeit.
|
||||
//
|
||||
// DER ORDNER HAENGT NUR LESEND. Ein Dienst, der seine eigenen Sicherungen
|
||||
// loeschen kann, ist genau das Werkzeug, das man einem uebernommenen Dienst
|
||||
// nicht in die Hand geben will. Herausgeben reicht.
|
||||
//
|
||||
// UND ES GIBT NUR EINE FORM VON NAMEN. Nicht "alles ausser .. und /", sondern
|
||||
// ein Muster, das genau auf die eigenen Archive passt -- wer einen Pfad
|
||||
// hineinschmuggeln will, muesste ihn erst so benennen. Das ist der Unterschied
|
||||
// zwischen "dicht gemacht" und "dicht gebaut".
|
||||
const SICHERUNGEN_DIR = process.env.SICHERUNGEN_DIR || '/sicherungen'
|
||||
const ARCHIV_MUSTER = /^d4rk_media-\d{4}-\d{2}-\d{2}-\d{4}\.tar\.gz$/
|
||||
|
||||
dashRoutes.get('/sicherungen', async (c) => {
|
||||
let namen: string[]
|
||||
try {
|
||||
namen = await readdir(SICHERUNGEN_DIR)
|
||||
} catch {
|
||||
// Nicht eingehaengt ist kein Fehler, sondern ein Zustand -- die
|
||||
// Oberflaeche sagt dann, was zu tun ist, statt eine rote Meldung zu
|
||||
// zeigen, die nach Defekt aussieht.
|
||||
return c.json({ eingehaengt: false, items: [], ordner: SICHERUNGEN_DIR })
|
||||
}
|
||||
|
||||
const items = []
|
||||
for (const name of namen.filter((n) => ARCHIV_MUSTER.test(n))) {
|
||||
try {
|
||||
const s = await stat(join(SICHERUNGEN_DIR, name))
|
||||
items.push({ name, size: s.size, at: s.mtimeMs })
|
||||
} catch {
|
||||
/* zwischendurch weggeraeumt -- dann gehoert es nicht in die Liste */
|
||||
}
|
||||
}
|
||||
items.sort((a, b) => b.at - a.at)
|
||||
|
||||
return c.json({
|
||||
eingehaengt: true,
|
||||
ordner: SICHERUNGEN_DIR,
|
||||
items,
|
||||
bytes: items.reduce((n, i) => n + i.size, 0),
|
||||
})
|
||||
})
|
||||
|
||||
dashRoutes.get('/sicherungen/:name', async (c) => {
|
||||
const name = c.req.param('name')
|
||||
if (!ARCHIV_MUSTER.test(name)) {
|
||||
return c.json({ error: 'Kein Sicherungsarchiv.' }, 400)
|
||||
}
|
||||
|
||||
const pfad = join(SICHERUNGEN_DIR, name)
|
||||
let groesse: number
|
||||
try {
|
||||
groesse = (await stat(pfad)).size
|
||||
} catch {
|
||||
return c.json({ error: 'Gibt es nicht (mehr).' }, 404)
|
||||
}
|
||||
|
||||
// Gestroemt und nicht in den Speicher gelesen: das Archiv ist 336 MB, und
|
||||
// die haetten sonst der Dienst UND der Proxy gleichzeitig im Arbeitsspeicher.
|
||||
logVerwaltung(c.get('user').username, 'sicherung', 'geholt', name, `${groesse} Bytes`)
|
||||
return c.body(createReadStream(pfad) as any, 200, {
|
||||
'Content-Type': 'application/gzip',
|
||||
'Content-Length': String(groesse),
|
||||
'Content-Disposition': `attachment; filename="${name}"`,
|
||||
'Cache-Control': 'no-store',
|
||||
})
|
||||
})
|
||||
|
||||
dashRoutes.get('/einstellungen/export', (c) => {
|
||||
const mitGeheim = c.req.query('geheim') === '1'
|
||||
const alle = db.prepare('SELECT key, value FROM settings ORDER BY key').all() as {
|
||||
|
||||
Reference in New Issue
Block a user