feat: fehlende Bilder finden, Massen-Umbenennen, Bilder verkleinern, Einstellungen mitnehmen
FEHLENDE BILDER. Die Frage, die bisher niemand beantwortet hat: das Fotostudio weiss, was es aufgenommen hat, der Dienst weiss, was liegt, und die Differenz rechnet keiner aus -- weshalb "das Handy zeigt bei manchen Autos kein Bild" erst im Spiel auffaellt. POST /api/dash/fehlende nimmt ein Muster mit genau EINEM Platzhalter und eine Liste (als Array oder als Textblock aus einem Textfeld) und gibt beide Richtungen zurueck: was fehlt, und was daliegt ohne in der Liste zu stehen. Beides bedeutet etwas anderes -- fehlende sind Arbeit fuers Fotostudio, ueberzaehlige sind Platz, den man zurueckbekommt. Gesucht wird KLEINGESCHRIEBEN, weil so abgelegt wird: wer "WEAPON_SMG" hereinreicht, meint weapon_smg.png und bekaeme sonst ein "fehlt", das gelogen waere. Gemessen an echten Namen: 6 geprueft, 4 da, 2 fehlen, 985 ueberzaehlig. MASSEN-UMBENENNEN mit Vorschau. Bei 3578 Items ist eine Umbenennung von Hand keine. DIE VORSCHAU IST DER EIGENTLICHE TEIL: was hier passiert, ist dieselbe Warnung wie beim Ordner-Umbenennen -- jede alte Adresse gibt danach 404 --, nur mal tausend. Geprueft wird VOR dem ersten Schritt: Kollisionen untereinander, belegte Ziele, unzulaessige Pfade. Und EIN Problem verhindert ALLES; eine halbe Umbenennung ist der Zustand, aus dem niemand mehr herausfindet, weil die Grenze nirgends steht. Gemessen: Vorschau ueber 134 Dateien fasst nichts an; eine erzwungene Kollision gibt 409 und laesst alle drei Dateien stehen; der saubere Lauf benennt zwei um, Vorschau wandert mit, ein Eintrag im Verlauf statt zwei. BILDER BEIM ABLEGEN VERKLEINERN, je Token einstellbar. NICHT als Schutz -- das waere gemessen falsch, ein 12000x12000-PNG kostet 374 ms und 47 MB, weil libvips kachelweise arbeitet. Es geht um Plattenplatz und Ladezeit: ein Bildschirmfoto ist 3840 breit und wird auf einem Handy auf 400 gezeigt. withoutEnlargement ist dabei entscheidend, sonst blaeht die Einstellung kleine Icons auf. DABEI WAERE MIR EIN ECHTER FEHLER UNTERLAUFEN: der sha256 wird weiter oben aus dem Original gerechnet (fuer den Ersatzpfad, wenn kein X-Path kam). Wird das Bild verkleinert, ist das eine ANDERE Datei -- ein Datensatz mit dem Hash des Originals waere schlicht falsch, und jede spaetere Gegenprobe verglichen gegen etwas, das nirgends liegt. Gemessen nach der Korrektur: 3000x2000 -> 512x341, 82 KB -> 3 KB, und Datenbank und Platte stimmen in Groesse und Hash ueberein. EINSTELLUNGEN MITNEHMEN. Export als JSON, Import zurueck. GEHEIMNISSE GEHEN STANDARDMAESSIG NICHT MIT: unter den Schluesseln liegen das Discord-Client-Geheimnis und das Nextcloud-Passwort, und eine Datei, die man "mal eben" exportiert und in einen Chat zieht, ist genau der Weg, auf dem so etwas abhandenkommt. Wer sie braucht, sagt ?geheim=1 -- und der Hinweis steht dann IN der Datei, nicht nur auf dem Bildschirm, von dem er beim Weiterreichen abfaellt. Der Export mit Geheimnissen steht im Verwaltungs-Verlauf. Beim Import ueberschreiben LEERE WERTE NICHTS: eine Datei ohne Geheimnisse -- der Normalfall -- traegt sie als leere Zeichenkette, und die darf ein vorhandenes Passwort nicht loeschen. Gemessen: nach einem Import mit leerem client_secret stehen die 32 Zeichen unveraendert da. Nicht mitgenommen wird sicherung.letzte -- das ist der Bericht des letzten Laufs, kein Einstellungswert. In eine frische Anlage eingespielt behauptete er dem Wachhund eine Sicherung, die es dort nie gab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29e516559a
commit
381feade4e
4 files changed
+509
-12
No files matched your search
@@ -31,6 +31,7 @@ import {
|
||||
extensionFor,
|
||||
fileExists,
|
||||
kannBewegtVorschau,
|
||||
vielleichtKleiner,
|
||||
kannVorschau,
|
||||
lohntWebp,
|
||||
mimeFor,
|
||||
@@ -266,8 +267,27 @@ uploadRoutes.post('/upload', async (c) => {
|
||||
}
|
||||
}
|
||||
|
||||
// VOR dem Schreiben verkleinern, wenn der Token es verlangt. Danach waere
|
||||
// es zu spaet: dann laege das grosse Bild schon auf der Platte, und der
|
||||
// Hash im Datensatz gehoerte zu einer anderen Datei als der abgelegten.
|
||||
let daten = data
|
||||
let verkleinert: false | { von: string; auf: string } = false
|
||||
if (token.max_kante) {
|
||||
const r = await vielleichtKleiner(data, mime, token.max_kante)
|
||||
daten = r.data
|
||||
verkleinert = r.verkleinert
|
||||
}
|
||||
|
||||
// DER HASH GEHOERT ZU DEM, WAS WIRKLICH LIEGT. Weiter oben wird er aus dem
|
||||
// Original gerechnet -- der braucht ihn fuer den Ersatzpfad, wenn kein
|
||||
// X-Path kam. Wird das Bild verkleinert, ist das eine ANDERE Datei, und ein
|
||||
// Datensatz mit dem Hash des Originals waere schlicht falsch: der
|
||||
// Verwaisten-Sucher und jede spaetere Gegenprobe verglichen dann gegen
|
||||
// etwas, das nirgends liegt.
|
||||
const digestAbgelegt = verkleinert ? sha256(daten) : digest
|
||||
|
||||
try {
|
||||
await writeFileAtomic(path, data)
|
||||
await writeFileAtomic(path, daten)
|
||||
} catch (err) {
|
||||
console.error('[upload] Schreiben fehlgeschlagen:', err)
|
||||
return c.json({ error: 'Schreiben fehlgeschlagen' }, 500)
|
||||
@@ -276,8 +296,8 @@ uploadRoutes.post('/upload', async (c) => {
|
||||
// Die Vorschau gleich mit. Bewusst NACH dem eigentlichen Schreiben und mit
|
||||
// eigenem Ergebnis: scheitert sie, ist das Bild trotzdem abgelegt und der
|
||||
// Upload ein Erfolg. Das Dashboard faellt dann auf das Vollbild zurueck.
|
||||
if (kannVorschau(mime)) await writeThumb(path, data)
|
||||
if (lohntWebp(mime)) await writeWebp(path, data)
|
||||
if (kannVorschau(mime)) await writeThumb(path, daten)
|
||||
if (lohntWebp(mime)) await writeWebp(path, daten)
|
||||
|
||||
// Video und Ton: das Standbild kommt aus der Datei AUF DER PLATTE und
|
||||
// deshalb erst hier -- ffmpeg muss darin springen koennen. Die Laenge faellt
|
||||
@@ -292,23 +312,26 @@ uploadRoutes.post('/upload', async (c) => {
|
||||
db.prepare(
|
||||
`UPDATE media SET size = ?, sha256 = ?, mime = ?, dauer = ?, token_id = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
).run(data.length, digest, mime, dauer, token.id, timestamp, existing.id)
|
||||
).run(daten.length, digestAbgelegt, mime, dauer, token.id, timestamp, existing.id)
|
||||
} else {
|
||||
db.prepare(
|
||||
`INSERT INTO media (path, size, sha256, mime, dauer, token_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(path, data.length, digest, mime, dauer, token.id, timestamp, timestamp)
|
||||
).run(path, daten.length, digestAbgelegt, mime, dauer, token.id, timestamp, timestamp)
|
||||
}
|
||||
|
||||
logEvent(existing ? 'replace' : 'upload', path, data.length, 'token', token.name)
|
||||
logEvent(existing ? 'replace' : 'upload', path, daten.length, 'token', token.name)
|
||||
|
||||
return c.json({
|
||||
url: publicUrlFor(path),
|
||||
path,
|
||||
size: data.length,
|
||||
sha256: digest,
|
||||
size: daten.length,
|
||||
sha256: digestAbgelegt,
|
||||
mime,
|
||||
replaced: Boolean(existing),
|
||||
// Nur dann, wenn wirklich gerechnet wurde -- ein Feld, das immer
|
||||
// dasteht, sagt nichts.
|
||||
...(verkleinert ? { verkleinert } : {}),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user