Compare commits

...
3 Commits
Author SHA1 Message Date
D4rkst3randClaude Opus 5.5 c8080fb2eb 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) <noreply@anthropic.com>
2026-10-03 10:06:54 +02:00
D4rkst3randClaude Opus 5.5 6677b559f2 Fehlende mit Token, und die Rechnung vergleicht die Schreibweise genau
POST /api/fehlende stellt dieselbe Frage wie das Dashboard (welche Namen
haben kein Bild?), mit Token statt Sitzung. Das Fotostudio soll "fehlt" so
beantworten, wie der Laden es sieht -- fuer die Kleidung sind das rund 8.000
Namen, mit /exists ebenso viele Anfragen, hier zwei.

Die Rechnung steht jetzt EINMAL in server/src/fehlende.ts; die Dashboard-Route
ruft sie auf. Die Antwortform bleibt (da als Zahl), dazu kommt `schreibweise`.

DABEI GEFUNDEN: die alte Rechnung schrieb jeden Namen klein, "weil so
abgelegt wird". Seit a373b96 (22.08.) stimmt das nicht mehr. Gemessen am
03.10. ueber /f/: items/WEAPON_PISTOL.png 200, items/weapon_pistol.png 404 --
die Abfrage meldete also jede Waffe als fehlend, die da lag. Jetzt wird genau
verglichen; liegt etwas nur unter anderer Schreibweise, steht es zusaetzlich
in `schreibweise` (umbenennen statt neu aufnehmen). docs/API.md sagte
dasselbe Falsche und riet zu item:lower() -- berichtigt.

Geprueft in einem eigenen Container gegen eine Kopie der Datenbank (per
SQLite-Sicherung gezogen, danach geloescht), mit Wegwerf-Token: 13/13 --
401 ohne Token, 400 mit Text bei kaputtem JSON, fehlendem Platzhalter, ohne
Namen und bei 5001 Namen; WEAPON_PISTOL da, weapon_pistol fehlt und steht in
schreibweise, ein erfundener Name fehlt ohne schreibweise. Ernstfall: 7.928
lokale Kleidungsbilder des Fotostudios, 2.591 davon liegen im Dienst (33 %,
eine Stichprobe ueber /f/ hatte 11 von 40 ergeben).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 10:01:54 +02:00
D4rkst3randClaude Opus 5.5 73c9f310c0 Zugang: was darf dieser Token -- lag seit dem 26.08. nur in der Arbeitskopie
GET /api/zugang nennt die Ordner, in die ein Token schreiben darf (oder die
vorhandenen, wenn er ueberall darf). Das Fotostudio ruft die Route fuer seine
Ordnerauswahl auf.

Gemessen am 03.10.2026: der laufende Container ist am 26.08. um 15:05 aus
dieser Arbeitskopie gebaut worden, in derselben Minute wie die letzte
Aenderung an upload.ts, und /api/zugang antwortet ohne Token mit 401 (ein
erfundener Weg mit 404). Die Route laeuft also -- sie stand nur nicht im Git.
Dieser Commit holt nach, was im Betrieb ist; am Code aendert er nichts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 09:55:37 +02:00
4 changed files with 349 additions and 90 deletions

No files matched your search

+73 -8
View File
@@ -39,19 +39,28 @@ und niemand merkte es, bis das Bild fehlt.
### Groß- und Kleinschreibung
Was über das Dashboard oder ein Werkzeug hochgeladen wird, kommt **klein**
an: `WEAPON_SMG.png` liegt als `weapon_smg.png`. Der Grund ist, dass ein Name
zu einer Adresse wird und Adressen sich nicht auf die Schreibweise eines
Dateisystems verlassen sollen.
**Die Schreibweise bleibt, und sie zählt.** `WEAPON_SMG.png` liegt als
`WEAPON_SMG.png`, und `/f/items/weapon_smg.png` ist dann ein 404.
Für ein Skript heißt das: **die Vorlage arbeitet mit Kleinbuchstaben.**
> **Berichtigt am 03.10.2026.** Hier stand bis dahin, alles komme **klein**
> an und eine Vorlage solle `item:lower()` rufen. Das gilt seit `a373b96`
> (22.08.2026) nicht mehr: ox_inventory fragt unter dem exakten Item-Namen,
> und dessen Waffen heißen `WEAPON_*`. Gemessen am 03.10.2026:
> `items/WEAPON_PISTOL.png` 200, `items/weapon_pistol.png` 404. Wer der alten
> Zeile folgte, baute Adressen, die ins Leere zeigen — und die
> Dashboard-Abfrage „Fehlende“ tat genau das und meldete jede Waffe als
> fehlend.
Für ein Skript heißt das: **den Namen so einsetzen, wie er ist.** Klein
gemacht wird nur die Endung.
```lua
local url = ('https://media.d4rkst3r.de/f/items/%s.png'):format(item:lower())
local url = ('https://media.d4rkst3r.de/f/items/%s.png'):format(item)
```
Über `/api/upload` mit eigenem `X-Path` gilt das nicht — dort steht der Pfad
so, wie er geschickt wird.
Ersetzt werden beim Hochladen über das Fenster weiterhin Zeichen, die in einer
Adresse nichts verloren haben (Leerzeichen, Klammern, Umlaute). Über
`/api/upload` mit eigenem `X-Path` steht der Pfad so, wie er geschickt wird.
---
@@ -385,6 +394,62 @@ Upload.
---
## POST /api/fehlende
Dieselbe Frage wie im Dashboard unter **Fehlende**, aber mit Token: welche
dieser Namen haben **kein** Bild? Für „nur fehlende“ über einen ganzen
Katalog — die Kleidung des Fotostudios sind rund 8.000 Namen, das wären
8.000 Anfragen an `/api/exists` und hier zwei.
```json
{ "muster": "clothing/{model}.png", "namen": ["m_c_11_base_84", "…"] }
```
`namen` darf auch ein Textblock sein (Zeilen, Kommas oder Semikolons).
Höchstens **5000 Namen** je Anfrage, mehr gibt einen 400 mit Text.
```json
{ "muster": "clothing/{model}.png", "geprueft": 2, "da": 1,
"fehlt": [{ "name": "m_c_11_base_84", "pfad": "clothing/m_c_11_base_84.png" }],
"schreibweise": [],
"ueberzaehlig": ["…"], "ueberzaehligGesamt": 5337,
"unbrauchbar": [] }
```
- **Genau verglichen**, wie die öffentliche Adresse auch. Liegt etwas nur
unter einer anderen Schreibweise, steht es in `fehlt` **und** in
`schreibweise` (mit `liegtAls`) — es muss umbenannt werden, nicht neu
aufgenommen.
- `da` ist eine **Zahl**. Wer die einzelnen braucht: alle Namen ohne `fehlt`
und ohne `unbrauchbar`.
- **Keine Ordnerfessel**, wie bei `/api/exists`: die Antwort ist über `/f/`
ohnehin öffentlich.
- Endet nie auf eine Bildendung — die Proxy-Falle aus dem Abschnitt darüber
greift hier nicht.
Die Rechnung steht **einmal**, in `server/src/fehlende.ts`; die Route des
Dashboards ruft dieselbe.
---
## GET /api/liste?ordner=&lt;ordner&gt;
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/&lt;pfad&gt;
Braucht einen Token mit dem Recht **darf löschen**.
+142
View File
@@ -0,0 +1,142 @@
/** Wozu fehlt ein Bild? -- die EINE Rechnung dafuer.
*
* Zwei Aufrufer: das Dashboard (`POST /api/dash/fehlende`, mit Sitzung) und
* das Fotostudio (`POST /api/fehlende`, mit Token). Bis zum 03.10.2026 gab es
* nur den ersten, und die Rechnung stand in dash.ts. Eine zweite Fassung
* daneben waere dieselbe Frage zweimal beantwortet -- und die zweite Antwort
* veraltet als erste.
*
* DIE SCHREIBWEISE ZAEHLT. Hier stand bis zum 03.10.2026, gesucht werde
* kleingeschrieben, "weil so abgelegt wird". Das stimmt seit a373b96
* (22.08.2026, "Beim Hochladen bleibt die Schreibweise") nicht mehr. Gemessen
* am 03.10.2026 ueber /f/:
*
* items/WEAPON_PISTOL.png 200
* items/weapon_pistol.png 404
*
* Die Rechnung meldete also jede Waffe als fehlend, die da lag. Jetzt wird
* genau verglichen -- die oeffentliche Adresse unterscheidet ebenfalls, und
* was unter der erfragten Schreibweise nicht abrufbar ist, FEHLT fuer jeden,
* der genau diese Adresse baut.
*
* Liegt es unter einer ANDEREN Schreibweise, steht es zusaetzlich in
* `schreibweise` -- das ist kein Bild, das neu aufgenommen werden muss,
* sondern eines, das umbenannt werden will. Beides in einen Topf zu werfen
* hiesse, eine Viertelstunde Fotostudio fuer eine Sekunde Umbenennen.
*/
import { db } from './db.js'
import { PathError, checkPath } from './storage.js'
export const FEHLENDE_HOECHSTENS = 5000
export type FehlendeAntwort = {
muster: string
geprueft: number
/** Wie viele genau so daliegen. Eine ZAHL, weil das Dashboard sie so
* liest -- wer einzelne braucht, rechnet: geprueft minus fehlt minus
* unbrauchbar. */
da: number
fehlt: { name: string; pfad: string }[]
/** Fehlt unter der erfragten Schreibweise, liegt aber unter einer anderen. */
schreibweise: { name: string; pfad: string; liegtAls: string }[]
ueberzaehlig: string[]
ueberzaehligGesamt: number
unbrauchbar: { name: string; grund: string }[]
}
/** Wirft `FehlendeFehler` mit einem Satz fuer den Aufrufer -- beide Routen
* machen daraus einen 400 mit Text, keinen nackten 500. */
export class FehlendeFehler extends Error {}
export function fehlendeRechnen(musterRoh: unknown, namenRoh: unknown): FehlendeAntwort {
const muster = String(musterRoh ?? '').trim().replace(/^\/+/, '')
if (!muster) throw new FehlendeFehler('Kein Muster angegeben')
const platzhalter = muster.match(/\{[a-zA-Z]+\}/g) ?? []
if (platzhalter.length !== 1) {
throw new FehlendeFehler(
platzhalter.length === 0
? 'Im Muster fehlt der Platzhalter, z. B. vehicles/{model}.webp'
: `Genau ein Platzhalter, nicht ${platzhalter.length}. Sonst ist nicht klar, wofuer die Namen stehen.`,
)
}
// Die Namen kommen als Liste oder als Textblock aus einem Textfeld -- wer
// eine Spalte aus einer Tabelle kopiert, hat Zeilenumbrueche, wer aus einem
// Skript kommt, hat ein Array.
const roh = Array.isArray(namenRoh) ? namenRoh : String(namenRoh ?? '').split(/[\r\n,;]+/)
const namen = [...new Set(roh.map((n) => String(n).trim()).filter(Boolean))]
if (namen.length === 0) throw new FehlendeFehler('Keine Namen dabei')
if (namen.length > FEHLENDE_HOECHSTENS) {
throw new FehlendeFehler(`Hoechstens ${FEHLENDE_HOECHSTENS} Namen auf einmal`)
}
const wo = platzhalter[0] as string
// Der Ordner ist der Teil vor dem Platzhalter -- daran erkennen wir, was
// "ueberzaehlig" ueberhaupt heisst. Ohne ihn waere jede Datei im Dienst
// ueberzaehlig, und die Antwort waere unbrauchbar.
const bisPlatzhalter = muster.slice(0, muster.indexOf(wo))
const ordner = bisPlatzhalter.includes('/')
? bisPlatzhalter.slice(0, bisPlatzhalter.lastIndexOf('/'))
: ''
const genau = db.prepare('SELECT 1 FROM media WHERE path = ?')
const ohneSchreibweise = db.prepare(
'SELECT path FROM media WHERE path = ? COLLATE NOCASE LIMIT 1',
)
const da: string[] = []
const fehlt: FehlendeAntwort['fehlt'] = []
const schreibweise: FehlendeAntwort['schreibweise'] = []
const unbrauchbar: FehlendeAntwort['unbrauchbar'] = []
for (const name of namen) {
const pfad = muster.replace(wo, name)
let gueltig: string
try {
gueltig = checkPath(pfad)
} catch (err) {
unbrauchbar.push({
name,
grund: err instanceof PathError ? err.message : 'unbrauchbar',
})
continue
}
if (genau.get(gueltig)) {
da.push(gueltig)
continue
}
fehlt.push({ name, pfad: gueltig })
const anders = ohneSchreibweise.get(gueltig) as { path: string } | undefined
if (anders) schreibweise.push({ name, pfad: gueltig, liegtAls: anders.path })
}
// Die Gegenrichtung: was liegt in diesem Ordner, ohne in der Liste zu
// stehen? Nur Dateien, die auf das Muster passen -- eine README im selben
// Ordner ist nicht ueberzaehlig, sie gehoert nur nicht dazu.
const endung = muster.slice(muster.indexOf(wo) + wo.length)
const gesucht = new Set(da)
const maskiert = ordner.replace(/[\\%_]/g, (z) => `\\${z}`)
const imOrdner = ordner
? (db
.prepare(`SELECT path FROM media WHERE path LIKE ? ESCAPE '\\'`)
.all(`${maskiert}/%`) as { path: string }[])
: []
const ueberzaehlig = imOrdner
.map((r) => r.path)
.filter((p) => p.endsWith(endung) && !gesucht.has(p))
return {
muster,
geprueft: namen.length,
da: da.length,
fehlt,
schreibweise,
// Gekappt, damit die Antwort nicht zur Datenhalde wird -- die Zahl
// steht daneben, damit die Kappung nicht als Vollstaendigkeit
// durchgeht.
ueberzaehlig: ueberzaehlig.slice(0, 500),
ueberzaehligGesamt: ueberzaehlig.length,
unbrauchbar,
}
}
+6 -81
View File
@@ -10,6 +10,7 @@ import { unzipSync } from 'fflate'
import type { Context } from 'hono'
import { deleteCookie, getCookie, setCookie } from 'hono/cookie'
import { config } from '../config.js'
import { FehlendeFehler, fehlendeRechnen } from '../fehlende.js'
import {
avatarUrl,
db,
@@ -2292,90 +2293,14 @@ dashRoutes.post('/media/ersetzen', async (c) => {
*/
dashRoutes.post('/fehlende', async (c) => {
const body = await jsonBody<{ muster?: string; namen?: string[] | string }>(c)
if (!body?.muster?.trim()) return c.json({ error: 'Kein Muster angegeben' }, 400)
const muster = body.muster.trim().replace(/^\/+/, '')
const platzhalter = muster.match(/\{[a-zA-Z]+\}/g) ?? []
if (platzhalter.length !== 1) {
return c.json(
{
error:
platzhalter.length === 0
? 'Im Muster fehlt der Platzhalter, z. B. vehicles/{model}.webp'
: `Genau ein Platzhalter, nicht ${platzhalter.length}. Sonst ist nicht klar, wofuer die Namen stehen.`,
},
400,
)
}
// Die Namen kommen als Liste oder als Textblock aus einem Textfeld -- wer
// eine Spalte aus einer Tabelle kopiert, hat Zeilenumbrueche, wer aus einem
// Skript kommt, hat ein Array.
const roh = Array.isArray(body.namen)
? body.namen
: String(body.namen ?? '').split(/[\r\n,;]+/)
const namen = [...new Set(roh.map((n) => String(n).trim()).filter(Boolean))]
if (namen.length === 0) return c.json({ error: 'Keine Namen dabei' }, 400)
if (namen.length > 5000) return c.json({ error: 'Hoechstens 5000 Namen auf einmal' }, 400)
const wo = platzhalter[0] as string
// Der Ordner ist der Teil vor dem Platzhalter -- daran erkennen wir, was
// "ueberzaehlig" ueberhaupt heisst. Ohne ihn waere jede Datei im Dienst
// ueberzaehlig, und die Antwort waere unbrauchbar.
const bisPlatzhalter = muster.slice(0, muster.indexOf(wo))
const ordner = bisPlatzhalter.includes('/')
? bisPlatzhalter.slice(0, bisPlatzhalter.lastIndexOf('/'))
: ''
const vorhanden = db.prepare('SELECT 1 FROM media WHERE path = ?')
const fehlt: { name: string; pfad: string }[] = []
const da: string[] = []
const unbrauchbar: { name: string; grund: string }[] = []
for (const name of namen) {
// KLEINGESCHRIEBEN gesucht, weil so abgelegt wird. Wer "WEAPON_SMG"
// hereinreicht, meint weapon_smg.png -- und bekaeme sonst ein "fehlt",
// das gelogen waere.
const pfad = muster.replace(wo, name.toLowerCase())
// Die Rechnung steht seit dem 03.10.2026 in fehlende.ts -- das Fotostudio
// stellt dieselbe Frage mit Token unter /api/fehlende.
try {
const gueltig = checkPath(pfad)
if (vorhanden.get(gueltig)) da.push(gueltig)
else fehlt.push({ name, pfad: gueltig })
return c.json(fehlendeRechnen(body?.muster, body?.namen))
} catch (err) {
unbrauchbar.push({
name,
grund: err instanceof PathError ? err.message : 'unbrauchbar',
})
if (err instanceof FehlendeFehler) return c.json({ error: err.message }, 400)
throw err
}
}
// Die Gegenrichtung: was liegt in diesem Ordner, ohne in der Liste zu
// stehen? Nur Dateien, die auf das Muster passen -- eine README im selben
// Ordner ist nicht ueberzaehlig, sie gehoert nur nicht dazu.
const endung = muster.slice(muster.indexOf(wo) + wo.length)
const gesucht = new Set(da)
const maskiert = ordner.replace(/[\\%_]/g, (z) => `\\${z}`)
const imOrdner = ordner
? (db
.prepare(`SELECT path FROM media WHERE path LIKE ? ESCAPE '\\'`)
.all(`${maskiert}/%`) as { path: string }[])
: []
const ueberzaehlig = imOrdner
.map((r) => r.path)
.filter((pfad) => pfad.endsWith(endung) && !gesucht.has(pfad))
return c.json({
muster,
geprueft: namen.length,
da: da.length,
fehlt,
// Gekappt, damit die Antwort nicht zur Datenhalde wird -- die Zahl
// steht daneben, damit die Kappung nicht als Vollstaendigkeit
// durchgeht.
ueberzaehlig: ueberzaehlig.slice(0, 500),
ueberzaehligGesamt: ueberzaehlig.length,
unbrauchbar,
})
})
dashRoutes.get('/export', (c) => {
+128 -1
View File
@@ -22,9 +22,10 @@
import { Hono } from 'hono'
import type { Context, Next } from 'hono'
import { config } from '../config.js'
import { FehlendeFehler, fehlendeRechnen } from '../fehlende.js'
import { meldeUpload } from '../meldung.js'
import { db, logEvent, now, type Media, type Token } from '../db.js'
import { fesselText, tokenAllows, tokenFromHeader } from '../auth.js'
import { fesselText, tokenAllows, tokenFromHeader, tokenPrefixes } from '../auth.js'
import {
PathError,
checkPath,
@@ -141,6 +142,9 @@ uploadRoutes.use('/media/*', requireToken)
// '/exists/*' trifft sie NICHT, weil dort kein Schraegstrich mehr folgt -- und
// eine Auskunft ohne Token waere genau das Loch, das die Abfrageform schliessen
// soll.
uploadRoutes.use('/zugang', requireToken)
uploadRoutes.use('/fehlende', requireToken)
uploadRoutes.use('/liste', requireToken)
uploadRoutes.use('/exists', requireToken)
uploadRoutes.use('/exists/*', requireToken)
@@ -425,6 +429,129 @@ function existsAntwort(c: Context<{ Variables: Vars }>, roh: string) {
)
}
/** Was darf dieser Token eigentlich?
*
* WOZU. Das Fotostudio soll beim Einrichten eine AUSWAHL der Ordner zeigen
* statt eines Freitextfeldes -- und eine Auswahl, die etwas anbietet, das
* hinterher mit 403 abprallt, ist schlimmer als keine.
*
* Bisher erfuhr man die Fesseln nur, indem man dagegen lief: die
* 403-Meldung nennt sie. Das ist eine Auskunft ueber den eigenen Zugang, kein
* Geheimnis -- wer den Token hat, kennt seinen Umfang ohnehin, er muss ihn
* nur ausprobieren. Also lieber geradeheraus.
*
* Eine leere Liste heisst "ueberall", genau wie in der Datenbank. Das steht
* auch im Feld daneben, damit niemand "keine Ordner" liest.
*/
uploadRoutes.get('/zugang', (c) => {
const token = c.get('token') as Token
const fesseln = tokenPrefixes(token)
// WELCHE ORDNER ZUR AUSWAHL STEHEN.
//
// Ist der Token gefesselt, sind es genau seine -- eine Auswahl, die etwas
// anbietet, das hinterher mit 403 abprallt, waere schlimmer als keine.
//
// Ist er es NICHT, waere die Antwort "ueberall" fuer ein Auswahlfeld
// wertlos. Dann kommen die Ordner, die es wirklich gibt. Das ist keine
// Preisgabe: wer ueberall schreiben darf, kommt an jeden davon ohnehin --
// er muesste den Namen nur raten.
const vorhanden = (
db
.prepare(
`SELECT DISTINCT substr(path, 1, instr(path, '/') - 1) AS o
FROM media WHERE instr(path, '/') > 0 ORDER BY o`,
)
.all() as { o: string }[]
).map((z) => z.o)
return c.json({
// Woraus das Auswahlfeld waehlen darf.
ordner: fesseln.length > 0 ? fesseln : vorhanden,
// Und ob daneben noch etwas anderes ginge -- dann darf das Feld auch
// einen freien Namen zulassen.
ueberall: fesseln.length === 0,
darfLoeschen: token.can_delete === 1,
name: token.name,
})
})
/** Wozu fehlt ein Bild -- dieselbe Frage wie im Dashboard, mit Token.
*
* WOZU, ergaenzt am 03.10.2026. Das Fotostudio soll "fehlt" so beantworten,
* wie der Laden es sieht: liegt es HIER oder nicht. Mit /exists waeren das
* fuer die Kleidung rund 8.000 Anfragen; hier sind es zwei (hoechstens
* FEHLENDE_HOECHSTENS Namen je Anfrage).
*
* KEINE ORDNERFESSEL, genau wie bei /exists: was hier beantwortet wird, ist
* ueber /f/ ohnehin oeffentlich abrufbar. Der Token haelt nur Fremde davon ab,
* den Dienst mit Sammelabfragen zu beschaeftigen.
*
* Kein .webp/.png am Ende der Adresse -- die Proxy-Falle von /exists/<pfad>
* (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/<radtyp>/<nummer> 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 {
body = await c.req.json()
} catch {
return c.json({ error: 'Kein gueltiges JSON' }, 400)
}
try {
return c.json(fehlendeRechnen(body?.muster, body?.namen))
} catch (err) {
if (err instanceof FehlendeFehler) return c.json({ error: err.message }, 400)
throw err
}
})
uploadRoutes.get('/exists', (c) => {
const roh = c.req.query('pfad') ?? c.req.query('path') ?? ''
if (!roh) return c.json({ error: 'Abfrage "pfad" fehlt' }, 400)