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:
@@ -137,6 +137,21 @@ export type NewToken = {
|
||||
token: string
|
||||
}
|
||||
|
||||
export type ThumbLauf = {
|
||||
erzeugt: number
|
||||
vorhanden: number
|
||||
uebersprungen: number
|
||||
fehler: number
|
||||
gesamt: number
|
||||
}
|
||||
|
||||
/** Die Adresse der Vorschau zu einem Pfad.
|
||||
*
|
||||
* Ob es sie gibt, weiss nur das Dateisystem — deshalb faellt die Galerie bei
|
||||
* 404 auf das Vollbild zurueck, statt hier zu fragen. */
|
||||
export const vorschauUrl = (path: string) =>
|
||||
'/t/' + path.split('/').map(encodeURIComponent).join('/')
|
||||
|
||||
export const api = {
|
||||
login: (username: string, password: string) =>
|
||||
post<{ user: User }>('/api/dash/auth/login', { username, password }),
|
||||
@@ -154,6 +169,7 @@ export const api = {
|
||||
post<{ deleted: string[]; count: number }>('/api/dash/media/delete', { ids }),
|
||||
|
||||
stats: () => get<Stats>('/api/dash/stats'),
|
||||
thumbsNachziehen: () => post<ThumbLauf>('/api/dash/maintenance/thumbs'),
|
||||
|
||||
tokens: () => get<{ items: TokenRow[] }>('/api/dash/tokens'),
|
||||
createToken: (name: string, prefix: string, canDelete: boolean) =>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { api, type MediaItem, type MediaPage } from '../api'
|
||||
import { api, vorschauUrl, type MediaItem, type MediaPage } from '../api'
|
||||
import { run, useStore } from '../store'
|
||||
import { bytes, istBild, zeit } from '../format'
|
||||
import { Knopf, Leer, Nachfrage, useKopieren } from '../components/Bausteine'
|
||||
@@ -213,7 +213,20 @@ export function Galerie() {
|
||||
<div className="flex aspect-square items-center justify-center bg-grund">
|
||||
{istBild(item.mime) ? (
|
||||
<img
|
||||
src={item.url}
|
||||
// Erst die Vorschau: 60 Vollbilder
|
||||
// sind 7,2 MB, nur damit jemand
|
||||
// sieht, WELCHE Bilder da sind.
|
||||
// Fehlt sie (404), nimmt der
|
||||
// Browser das Vollbild — deshalb
|
||||
// braucht es dafuer weder eine
|
||||
// Spalte noch eine Nachfrage.
|
||||
src={vorschauUrl(item.path)}
|
||||
onError={(e) => {
|
||||
const bild = e.currentTarget
|
||||
if (bild.dataset.gefallen) return
|
||||
bild.dataset.gefallen = 'ja'
|
||||
bild.src = item.url
|
||||
}}
|
||||
alt={item.path}
|
||||
loading="lazy"
|
||||
className="h-full w-full object-contain"
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Check, Copy, Folder, HardDrive, KeyRound, Loader2 } from 'lucide-react'
|
||||
import { Check, Copy, Folder, HardDrive, Images, KeyRound, Loader2 } from 'lucide-react'
|
||||
import { api, type Stats } from '../api'
|
||||
import { useStore } from '../store'
|
||||
import { run, useStore } from '../store'
|
||||
import { bytes } from '../format'
|
||||
import { Karte, Leer, useKopieren } from '../components/Bausteine'
|
||||
import { Karte, Knopf, Leer, useKopieren } from '../components/Bausteine'
|
||||
|
||||
export function Speicher() {
|
||||
const note = useStore((s) => s.note)
|
||||
const [stats, setStats] = useState<Stats | null>(null)
|
||||
const [laedt, setLaedt] = useState(true)
|
||||
const [zieht, setZieht] = useState(false)
|
||||
const { kopiert, kopieren } = useKopieren()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -69,6 +70,37 @@ export function Speicher() {
|
||||
</p>
|
||||
</Karte>
|
||||
|
||||
<Karte titel="Wartung">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Knopf
|
||||
wartet={zieht}
|
||||
onClick={async () => {
|
||||
setZieht(true)
|
||||
await run(
|
||||
() => api.thumbsNachziehen(),
|
||||
(r) =>
|
||||
r.erzeugt === 0
|
||||
? `Nichts zu tun — ${r.vorhanden} Vorschauen sind schon da.`
|
||||
: `${r.erzeugt} Vorschauen erzeugt` +
|
||||
(r.fehler ? `, ${r.fehler} fehlgeschlagen` : '') +
|
||||
`. ${r.vorhanden} waren schon da.`,
|
||||
)
|
||||
setZieht(false)
|
||||
}}
|
||||
kind={
|
||||
<>
|
||||
<Images size={15} />
|
||||
Fehlende Vorschaubilder erzeugen
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-schrift-leise">
|
||||
Die Galerie lädt Vorschauen statt Vollbilder. Was vor dieser Funktion
|
||||
hochgeladen wurde, hat noch keine.
|
||||
</p>
|
||||
</div>
|
||||
</Karte>
|
||||
|
||||
<Karte titel="Größte Ordner">
|
||||
{stats.folders.length === 0 ? (
|
||||
<Leer text="Noch nichts abgelegt." />
|
||||
|
||||
Reference in New Issue
Block a user