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 {