Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8080fb2eb | ||
|
|
6677b559f2 | ||
|
|
73c9f310c0 |
No files matched your search
+73
-8
@@ -39,19 +39,28 @@ und niemand merkte es, bis das Bild fehlt.
|
|||||||
|
|
||||||
### Groß- und Kleinschreibung
|
### Groß- und Kleinschreibung
|
||||||
|
|
||||||
Was über das Dashboard oder ein Werkzeug hochgeladen wird, kommt **klein**
|
**Die Schreibweise bleibt, und sie zählt.** `WEAPON_SMG.png` liegt als
|
||||||
an: `WEAPON_SMG.png` liegt als `weapon_smg.png`. Der Grund ist, dass ein Name
|
`WEAPON_SMG.png`, und `/f/items/weapon_smg.png` ist dann ein 404.
|
||||||
zu einer Adresse wird und Adressen sich nicht auf die Schreibweise eines
|
|
||||||
Dateisystems verlassen sollen.
|
|
||||||
|
|
||||||
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
|
```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
|
Ersetzt werden beim Hochladen über das Fenster weiterhin Zeichen, die in einer
|
||||||
so, wie er geschickt wird.
|
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=<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>
|
## DELETE /api/media/<pfad>
|
||||||
|
|
||||||
Braucht einen Token mit dem Recht **darf löschen**.
|
Braucht einen Token mit dem Recht **darf löschen**.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { unzipSync } from 'fflate'
|
|||||||
import type { Context } from 'hono'
|
import type { Context } from 'hono'
|
||||||
import { deleteCookie, getCookie, setCookie } from 'hono/cookie'
|
import { deleteCookie, getCookie, setCookie } from 'hono/cookie'
|
||||||
import { config } from '../config.js'
|
import { config } from '../config.js'
|
||||||
|
import { FehlendeFehler, fehlendeRechnen } from '../fehlende.js'
|
||||||
import {
|
import {
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
db,
|
db,
|
||||||
@@ -2292,90 +2293,14 @@ dashRoutes.post('/media/ersetzen', async (c) => {
|
|||||||
*/
|
*/
|
||||||
dashRoutes.post('/fehlende', async (c) => {
|
dashRoutes.post('/fehlende', async (c) => {
|
||||||
const body = await jsonBody<{ muster?: string; namen?: string[] | string }>(c)
|
const body = await jsonBody<{ muster?: string; namen?: string[] | string }>(c)
|
||||||
if (!body?.muster?.trim()) return c.json({ error: 'Kein Muster angegeben' }, 400)
|
// Die Rechnung steht seit dem 03.10.2026 in fehlende.ts -- das Fotostudio
|
||||||
|
// stellt dieselbe Frage mit Token unter /api/fehlende.
|
||||||
const muster = body.muster.trim().replace(/^\/+/, '')
|
try {
|
||||||
const platzhalter = muster.match(/\{[a-zA-Z]+\}/g) ?? []
|
return c.json(fehlendeRechnen(body?.muster, body?.namen))
|
||||||
if (platzhalter.length !== 1) {
|
} catch (err) {
|
||||||
return c.json(
|
if (err instanceof FehlendeFehler) return c.json({ error: err.message }, 400)
|
||||||
{
|
throw err
|
||||||
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())
|
|
||||||
try {
|
|
||||||
const gueltig = checkPath(pfad)
|
|
||||||
if (vorhanden.get(gueltig)) da.push(gueltig)
|
|
||||||
else fehlt.push({ name, pfad: gueltig })
|
|
||||||
} catch (err) {
|
|
||||||
unbrauchbar.push({
|
|
||||||
name,
|
|
||||||
grund: err instanceof PathError ? err.message : 'unbrauchbar',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) => {
|
dashRoutes.get('/export', (c) => {
|
||||||
|
|||||||
+128
-1
@@ -22,9 +22,10 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import type { Context, Next } from 'hono'
|
import type { Context, Next } from 'hono'
|
||||||
import { config } from '../config.js'
|
import { config } from '../config.js'
|
||||||
|
import { FehlendeFehler, fehlendeRechnen } from '../fehlende.js'
|
||||||
import { meldeUpload } from '../meldung.js'
|
import { meldeUpload } from '../meldung.js'
|
||||||
import { db, logEvent, now, type Media, type Token } from '../db.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 {
|
import {
|
||||||
PathError,
|
PathError,
|
||||||
checkPath,
|
checkPath,
|
||||||
@@ -141,6 +142,9 @@ uploadRoutes.use('/media/*', requireToken)
|
|||||||
// '/exists/*' trifft sie NICHT, weil dort kein Schraegstrich mehr folgt -- und
|
// '/exists/*' trifft sie NICHT, weil dort kein Schraegstrich mehr folgt -- und
|
||||||
// eine Auskunft ohne Token waere genau das Loch, das die Abfrageform schliessen
|
// eine Auskunft ohne Token waere genau das Loch, das die Abfrageform schliessen
|
||||||
// soll.
|
// soll.
|
||||||
|
uploadRoutes.use('/zugang', requireToken)
|
||||||
|
uploadRoutes.use('/fehlende', requireToken)
|
||||||
|
uploadRoutes.use('/liste', requireToken)
|
||||||
uploadRoutes.use('/exists', requireToken)
|
uploadRoutes.use('/exists', 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) => {
|
uploadRoutes.get('/exists', (c) => {
|
||||||
const roh = c.req.query('pfad') ?? c.req.query('path') ?? ''
|
const roh = c.req.query('pfad') ?? c.req.query('path') ?? ''
|
||||||
if (!roh) return c.json({ error: 'Abfrage "pfad" fehlt' }, 400)
|
if (!roh) return c.json({ error: 'Abfrage "pfad" fehlt' }, 400)
|
||||||
|
|||||||
Reference in new issue
Block a user