From c8080fb2eb06105e9c988923d1de59ccd48b6b10 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Sat, 3 Oct 2026 10:06:54 +0200 Subject: [PATCH] Liste eines Ordners, mit Token -- fuer Aufrufer, die die Namen nicht kennen GET /api/liste?ordner=clothing nennt alle Pfade darin (mit Unterordnern). /api/fehlende braucht die Namen, und die kennt beim Fotostudio nur der Spiel-Client -- die Kleidungsnamen entstehen dort aus Natives. Statt 8.000 Namen ueber ein Netzereignis zum Server zu schicken, holt der Server die Liste, und das Studio prueft selbst. Schreibweise genau (substr statt LIKE, das in SQLite fuer ASCII schreibweisenblind ist). Ohne Ordnerfessel wie /exists. Gekappt bei 100.000 mit `gekappt: true` daneben. Geprueft gegen eine DB-Kopie im eigenen Container: 401 ohne Token, 400 ohne ordner und mit ../, clothing 2591 Pfade und deckungsgleich mit /api/fehlende, 'Clothing' (gross) leer, items enthaelt items/WEAPON_PISTOL.png. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/API.md | 18 ++++++++++++++ server/src/routes/upload.ts | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/docs/API.md b/docs/API.md index 71305cf..24f633f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -432,6 +432,24 @@ Dashboards ruft dieselbe. --- +## GET /api/liste?ordner=<ordner> + +Was liegt in einem Ordner, mit Unterordnern. Für Aufrufer, die die Namen +**nicht** vorher kennen — das Fotostudio erfährt die Kleidungsnamen erst im +Spiel-Client und prüft sie dort gegen diese Liste. + +```json +{ "ordner": "clothing", "gesamt": 2591, "gekappt": false, + "pfade": ["clothing/f_c_10_Female_Heist_0.png", "…"] } +``` + +- Mit Token, ohne Ordnerfessel (wie `/api/exists`). +- **Schreibweise genau**: `?ordner=Clothing` ist ein anderer Ordner. +- Höchstens 100 000 Pfade; darüber steht `gekappt: true`. Eine gekappte + Liste ist **keine** Auskunft über das, was dahinter liegt. + +--- + ## DELETE /api/media/<pfad> Braucht einen Token mit dem Recht **darf löschen**. diff --git a/server/src/routes/upload.ts b/server/src/routes/upload.ts index 1d97e5d..cb510ed 100644 --- a/server/src/routes/upload.ts +++ b/server/src/routes/upload.ts @@ -144,6 +144,7 @@ uploadRoutes.use('/media/*', requireToken) // soll. uploadRoutes.use('/zugang', requireToken) uploadRoutes.use('/fehlende', requireToken) +uploadRoutes.use('/liste', requireToken) uploadRoutes.use('/exists', requireToken) uploadRoutes.use('/exists/*', requireToken) @@ -489,6 +490,53 @@ uploadRoutes.get('/zugang', (c) => { * Kein .webp/.png am Ende der Adresse -- die Proxy-Falle von /exists/ * (siehe unten) kann hier nicht zuschnappen. */ +/** Was liegt in einem Ordner -- die Liste, nicht die Frage nach Namen. + * + * WOZU, ergaenzt am 03.10.2026. /api/fehlende braucht die NAMEN, und die + * kennt beim Fotostudio nur der Spiel-Client: die Kleidungsnamen entstehen + * dort aus Natives, je Textur eines. Sie zum Server zu schicken hiesse rund + * 8.000 Namen ueber ein Netzereignis. Umgekehrt ist es eine Anfrage: der + * Dienst sagt, was liegt, und das Studio prueft jeden Namen selbst. + * + * SCHREIBWEISE GENAU, wie ueberall: `substr(...) = ?` und nicht LIKE -- + * LIKE ist in SQLite fuer ASCII schreibweisenblind und naehme + * `Clothing/...` mit in `clothing/`. + * + * Mit Unterordnern (felgen// wird einer sein). Ohne + * Ordnerfessel, wie /exists: alles hier ist ueber /f/ ohnehin abrufbar. + * + * GEKAPPT bei LISTE_HOECHSTENS, und dann steht `gekappt: true` daneben -- + * eine abgeschnittene Liste darf nicht als vollstaendige durchgehen, sonst + * meldet der Aufrufer alles dahinter als fehlend. + */ +const LISTE_HOECHSTENS = 100_000 + +uploadRoutes.get('/liste', (c) => { + const roh = (c.req.query('ordner') ?? '').trim().replace(/^\/+|\/+$/g, '') + if (!roh) return c.json({ error: 'Abfrage "ordner" fehlt, z. B. ?ordner=clothing' }, 400) + let ordner: string + try { + ordner = checkPath(roh) + } catch (err) { + return c.json({ error: err instanceof PathError ? err.message : 'Ordner unbrauchbar' }, 400) + } + const praefix = ordner + '/' + const gesamt = ( + db + .prepare('SELECT count(*) AS n FROM media WHERE substr(path, 1, ?) = ?') + .get(praefix.length, praefix) as { n: number } + ).n + const pfade = ( + db + .prepare( + 'SELECT path FROM media WHERE substr(path, 1, ?) = ? ORDER BY path LIMIT ?', + ) + .all(praefix.length, praefix, LISTE_HOECHSTENS) as { path: string }[] + ).map((z) => z.path) + + return c.json({ ordner, gesamt, gekappt: pfade.length < gesamt, pfade }) +}) + uploadRoutes.post('/fehlende', async (c) => { let body: { muster?: unknown; namen?: unknown } | null = null try {