feat: Sicherung, Anmeldebremse, Vorschaubilder -- und Schritt 5
SICHERUNG (tools/sichern.ps1, taeglich 04:30 als geplante Aufgabe). Die Datenbank wird nicht kopiert, sondern ueber SQLites eigene Sicherungsschnittstelle herausgeholt (server/src/backup.ts): im WAL-Modus liegt das Zuletzte noch nicht in media.db, und selbst alle drei Dateien zu kopieren ist nicht sicher, wenn waehrenddessen geschrieben wird. Die Bilder kommen aus einem NUR LESEND eingehaengten Volume dazu. Und sie prueft sich selbst: auspacken, Datenbank oeffnen, Medien/Token/ Benutzer zaehlen, mit dem laufenden Dienst vergleichen, sonst Fehler. Einmal wirklich zurueckgespielt -- leeres Volume, zweiter Dienst, Anmeldung mit dem echten Passwort, Bild abgerufen, Byte fuer Byte identisch. Eine Sicherung, die nie zurueckgespielt wurde, ist eine Hoffnung. ANMELDEBREMSE. Das Formular steht oeffentlich, und scrypt macht einen Versuch teuer -- aber teuer ist nicht selten. Fuenf freie Versuche je Adresse, dann Sperre ab 30 s mit Verdopplung bis 15 min, 429 samt Retry-After und einem Text, der sagt wie lange. Erfolg setzt zurueck. Nur im Speicher: wer sich aussperrt, startet den Container neu. Durchgemessen bis zur Erholung. VORSCHAUBILDER. sharp erzeugt beim Upload eine 320er WebP-Fassung unter /data/thumbs, ausgeliefert unter /t/<pfad>. Gemessen: 131502 -> 13078 Bytes, Faktor 10; eine Galerieseite faellt von 7,5 MB auf 766 KB. KEINE Spalte in der Datenbank -- ob es eine Vorschau gibt, sagt das Dateisystem, und die Galerie faellt bei 404 aufs Vollbild zurueck. Eine zweite Wahrheit, die auseinander- laufen kann, gibt es damit gar nicht erst. Ein Knopf zieht Fehlendes nach und nennt Zahlen statt "fertig". SCHRITT 5. Die Fivemanage-Dateien liegen in legacy/ mit Erklaerung; unsere docker-compose.media.yml heisst jetzt docker-compose.yml. Drei Volumes und drei Abbilder geloescht, rund 670 MB -- nachgezaehlt war vorher, dass kein Bild darin lag. Nebenbefund: `npm i sharp` scheitert auf diesem Rechner nicht an sharp, sondern an better-sqlite3 -- der Host laeuft auf Node 24 (ABI 137), node-gyp uebernimmt und findet keine Bauwerkzeuge. Genau die Falle aus dem Dockerfile. Eingetragen wurde mit --package-lock-only, gebaut wird im Abbild auf Node 22. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -120,8 +120,65 @@ export async function writeFileAtomic(path: string, data: Buffer): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- Vorschaubilder
|
||||
//
|
||||
// WARUM UEBERHAUPT. Ein freigestelltes Fahrzeugbild wiegt rund 124 KB. Eine
|
||||
// Galerieseite zeigt 60 davon — das sind 7,2 MB, nur damit jemand sieht, WELCHE
|
||||
// Bilder da sind. Bei 900 Fahrzeugen liegen 109 MB in der Ablage. Die Vorschau
|
||||
// wiegt ein Fuenfzigstel und reicht fuer eine Kachel voellig.
|
||||
//
|
||||
// WO SIE LIEGEN. Unter /data/thumbs, im selben Baum wie die Bilder, mit
|
||||
// ".webp" hinten dran. Also thumbs/vehicles/adder.webp.webp — haesslich, aber
|
||||
// eindeutig: haengte man die Endung nicht an, kollidierten adder.png und
|
||||
// adder.webp im Vorschauordner.
|
||||
//
|
||||
// KEINE SPALTE IN DER DATENBANK. Ob es eine Vorschau gibt, sagt das
|
||||
// Dateisystem. Eine Spalte waere eine zweite Wahrheit, die auseinanderlaufen
|
||||
// kann — und das Dashboard faellt ohnehin auf das Vollbild zurueck, wenn die
|
||||
// Vorschau fehlt.
|
||||
|
||||
export const THUMB_BREITE = 320
|
||||
|
||||
export const thumbPath = (path: string) => join(config.dataDir, 'thumbs', `${path}.webp`)
|
||||
|
||||
export const kannVorschau = (mime: string) =>
|
||||
/^image\/(png|jpeg|webp|avif|gif)$/.test(mime.split(';')[0]?.trim().toLowerCase() ?? '')
|
||||
|
||||
/** Eine Vorschau erzeugen. Schlaegt sie fehl, ist das KEIN Grund, den Upload
|
||||
* scheitern zu lassen — das Bild selbst liegt dann schon richtig. Der
|
||||
* Aufrufer bekommt false und schreibt eine Zeile ins Protokoll. */
|
||||
export async function writeThumb(path: string, data: Buffer): Promise<boolean> {
|
||||
const ziel = thumbPath(path)
|
||||
try {
|
||||
// Der Import steht hier drin und nicht oben: sharp zieht eine native
|
||||
// Bibliothek nach, und die soll nur geladen werden, wenn wirklich ein
|
||||
// Bild kommt.
|
||||
const { default: sharp } = await import('sharp')
|
||||
await mkdir(dirname(ziel), { recursive: true })
|
||||
|
||||
const bild = await sharp(data, { animated: false })
|
||||
// withoutEnlargement: ein 64 Pixel breites Bild wird nicht auf 320
|
||||
// aufgeblasen — das kostet Platz und sieht schlechter aus als das
|
||||
// Original.
|
||||
.resize({ width: THUMB_BREITE, withoutEnlargement: true })
|
||||
.webp({ quality: 72 })
|
||||
.toBuffer()
|
||||
|
||||
const temp = `${ziel}.${process.pid}.tmp`
|
||||
await writeFile(temp, bild)
|
||||
await rename(temp, ziel)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(`[thumbs] ${path}: ${err instanceof Error ? err.message : err}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFile(path: string): Promise<void> {
|
||||
await rm(absolutePath(path), { force: true })
|
||||
// Die Vorschau geht mit. Sonst bleibt sie liegen, und beim naechsten Bild
|
||||
// unter demselben Pfad sieht man das alte.
|
||||
await rm(thumbPath(path), { force: true })
|
||||
}
|
||||
|
||||
export async function fileExists(path: string): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user