included serverside mime detection
Sonarqube Scanner / Build and analyze (push) Successful in 1m6s

This commit is contained in:
Florian Zumpe
2026-06-17 11:42:43 +02:00
parent 3b3069ca8c
commit 0def9b23db
11 changed files with 574 additions and 126 deletions
+51 -23
View File
@@ -29,14 +29,15 @@ https://wegwichtel.example.org/api
- Listen- und Metadatenzugriffe liefern JSON.
- Die Einzelendpunkte für GPX, Bilder und Audio liefern die jeweilige Datei direkt aus. Mit `?metadata=true` liefern die Bild- und Audioendpunkte stattdessen JSON-Metadaten.
- Schreibzugriffe ohne Dateien akzeptieren `application/json`, `application/x-www-form-urlencoded` oder `multipart/form-data`.
- GPX-Dateien werden weiterhin ausschließlich als `multipart/form-data` übertragen.
- GPX-Dateien werden ausschließlich als `multipart/form-data` übertragen.
- Bild- und Audioendpunkte akzeptieren wahlweise `multipart/form-data` oder ein JSON-Objekt mit Base64-kodierten Dateidaten.
- Der Server ermittelt den tatsächlichen MIME-Typ aus dem Dateiinhalt. Dateiname, Dateiendung, Multipart-`Content-Type`, JSON-Felder und Data-URL-Präfixe werden nicht als Typnachweis verwendet.
- Für JSON wird `Content-Type: application/json` empfohlen. `Content-Type: text/json` wird aus Kompatibilitätsgründen ebenfalls akzeptiert.
- Ein leeres JSON-Objekt (`{}`) enthält keine Datei und kann deshalb keinen Bild- oder Audio-Upload ausführen.
- Pro Medien-Request wird genau eine Bild- beziehungsweise Audiodatei verarbeitet.
- Base64 vergrößert die Requestgröße um ungefähr ein Drittel und benötigt beim Verarbeiten zusätzlichen Arbeitsspeicher. Für große Dateien ist `multipart/form-data` vorzuziehen.
- Zeitstempel werden als SQLite- oder ISO-8601-Text ausgegeben.
- Die aktuelle API besitzt keine Authentifizierung. Schreibzugriffe dürfen nicht ungeschützt öffentlich erreichbar sein.
- Die Node.js-Anwendung besitzt keine eigene Authentifizierung. Ein vorgeschalteter Webserver kann Basic Auth erzwingen; die mitgelieferten Python-Werkzeuge erkennen `401 Unauthorized`, fragen Zugangsdaten ab und wiederholen den Request.
### Uploadgrenzen und Dateitypen
@@ -46,7 +47,7 @@ Unterstützte Dateitypen:
| Ressource | Dateiendungen beziehungsweise MIME-Typen |
|---|---|
| GPX | `.gpx`, `application/gpx+xml`, `application/xml`, `text/xml` |
| GPX | GPX-XML mit `<gpx>`-Wurzelelement |
| Bilder | JPEG, PNG, WebP |
| Audio | MP3, MP4/M4A, AAC, Ogg, WAV, WebM |
@@ -62,12 +63,20 @@ Bei einem JSON-Medienupload ist `picture` beziehungsweise `audio` ein Objekt:
| Eigenschaft | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `filename` | Text | ja | ursprünglicher Dateiname einschließlich passender Endung |
| `contentType` | Text | ja, sofern keine `dataUrl` den Typ enthält | MIME-Typ, beispielsweise `image/jpeg` oder `audio/mpeg` |
| `filename` | Text | nein | ursprünglicher Dateiname für Protokollierung; die Endung wird nicht zur Typbestimmung verwendet |
| `base64` | Text | ja, sofern `dataUrl` fehlt | reiner Base64-Inhalt ohne Präfix |
| `dataUrl` | Text | alternativ zu `base64` | vollständige Base64-Data-URL, beispielsweise `data:image/png;base64,...` |
| `dataUrl` | Text | alternativ zu `base64` | vollständige Base64-Data-URL; der dort genannte Medientyp wird ignoriert |
`base64` und `dataUrl` sind Alternativen. Wird `dataUrl` verwendet, kann `contentType` daraus übernommen werden. `filename` bleibt erforderlich, weil die Dateiendung für Ablage und Auslieferung benötigt wird.
`base64` und `dataUrl` sind Alternativen. Der Server erkennt Format und Speicherendung ausschließlich anhand der dekodierten Bytes. Ein vom Client mitgesendetes `contentType`- oder `mimeType`-Feld wird ignoriert und ist nicht erforderlich.
### Inhaltsbasierte Dateityperkennung
Binäre Bild- und Audiodateien werden mit `file-type` anhand ihrer Magic Bytes analysiert. Für Audio-/Video-Container wird zusätzlich `@file-type/av` verwendet, damit beispielsweise M4A und WebM möglichst zuverlässig als Audio oder Video unterschieden werden. GPX ist ein textbasiertes XML-Format und wird deshalb separat als UTF-8 gelesen und anhand des `<gpx>`-Wurzelelements validiert; anschließend übernimmt der vorhandene GPX-Parser die fachliche Prüfung.
Daraus folgen zwei wichtige Regeln:
- Eine als `image/png` deklarierte Textdatei wird mit `415 Unsupported Media Type` abgewiesen.
- Eine echte PNG-Datei wird auch dann akzeptiert, wenn sie `datei.bin` heißt oder im Multipart-Request kein Datei-`Content-Type` angegeben ist.
### Fehlerformat
@@ -85,6 +94,7 @@ Typische Statuscodes:
| `200` | Anfrage erfolgreich |
| `201` | Ressource wurde angelegt |
| `400` | Parameter oder Upload fehlt beziehungsweise ist ungültig |
| `401` | vorgeschalteter Webserver verlangt Authentifizierung |
| `404` | Route, POI oder Medienressource wurde nicht gefunden |
| `409` | Ressource existiert bereits oder Dateisystemzustand verhindert die Operation |
| `413` | Datei überschreitet `MAX_UPLOAD_MB` |
@@ -202,6 +212,7 @@ Pro POI kann höchstens eine Audiodatei existieren. Deshalb wird die Audioressou
| `GET` | `/api/routes/:routeId/pois/:poiId` | einzelnen POI der Route lesen |
| `POST` | `/api/routes/:id/pois` | POI-Metadaten anlegen |
| `PUT` | `/api/routes/:routeId/pois/:poiId` | POI-Metadaten aktualisieren |
| `DELETE` | `/api/routes/:routeId/pois/:poiId` | POI einschließlich Bildern und Audio löschen |
| `GET` | `/api/routes/:routeId/pois/:poiId/pictures` | Bilder eines POIs auflisten |
| `GET` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | Bilddatei ausliefern; optional Metadaten mit `?metadata=true` |
| `POST` | `/api/routes/:routeId/pois/:poiId/pictures` | einzelnes Bild für den POI hochladen |
@@ -308,7 +319,7 @@ Content-Type: `multipart/form-data`
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---|---|
| `gpx` | Datei | ja | – | GPX-Datei mit mindestens einem Trackpunkt |
| `gpx` | Datei | ja | – | GPX-Datei mit mindestens einem Trackpunkt; der Dateiname ist unerheblich |
| `name` | Text | bedingt | Name aus GPX | erforderlich, wenn GPX keinen Namen enthält |
| `slug` | Text | nein | aus `name` | interne URL-freundliche Kennung |
| `description` | Text | nein | leer | Routenbeschreibung |
@@ -320,7 +331,7 @@ curl -X POST http://127.0.0.1:47145/api/routes \
-F 'slug=schulwald-runde-klasse-7a' \
-F 'description=Naturkundlicher Rundweg der Klasse 7a' \
-F 'schoolName=Beispielschule' \
-F 'gpx=@examples/sample-route.gpx;type=application/gpx+xml'
-F 'gpx=@examples/sample-route.gpx'
```
Erfolg: `201 Created` und `Location: /api/routes/<id>`.
@@ -341,7 +352,7 @@ curl -X PUT http://127.0.0.1:47145/api/routes/1 \
-F 'name=Schulwald-Runde 2026' \
-F 'description=Überarbeitete Strecke' \
-F 'schoolName=Beispielschule' \
-F 'gpx=@route-neu.gpx;type=application/gpx+xml'
-F 'gpx=@route-neu.gpx'
```
### `POST /api/routes/:id/append`
@@ -354,7 +365,7 @@ Hängt alle Trackpunkte einer GPX-Datei an die Route an und berechnet Streckenwe
```bash
curl -X POST http://127.0.0.1:47145/api/routes/1/append \
-F 'gpx=@verlaengerung.gpx;type=application/gpx+xml'
-F 'gpx=@verlaengerung.gpx'
```
## 7. POIs lesen und bearbeiten
@@ -443,6 +454,22 @@ curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7 \
}'
```
### `DELETE /api/routes/:routeId/pois/:poiId`
Löscht den POI-Datensatz sowie alle zugehörigen Bilddatensätze, Bilddateien und die optionale Audiodatei. Dieser Vorgang ist im Gegensatz zum Soft Delete einer vollständigen Route nicht wiederherstellbar.
```bash
curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7
```
```json
{
"id": 7,
"routeId": 1,
"deleted": true
}
```
## 8. Bilder eines POIs einzeln verwalten
Die Route und der POI sind Bestandteil jedes Bildpfades. Dadurch ist die Zuordnung eindeutig und beim Upload muss keine zusätzliche `poiId` übergeben werden.
@@ -512,7 +539,7 @@ Lädt genau ein Bild für den im Pfad angegebenen POI hoch. Zulässig sind zwei
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---:|---|
| `picture` | Datei | ja | – | JPEG-, PNG- oder WebP-Datei |
| `picture` | Datei | ja | – | JPEG-, PNG- oder WebP-Datei; der Typ wird aus dem Inhalt erkannt |
| `caption` | Text | nein | leer | sichtbare Bildbeschreibung und Grundlage für den Alternativtext |
| `sequence` | nichtnegative Ganzzahl | nein | nächster freier Wert des POIs | Reihenfolge in der Diashow |
@@ -520,7 +547,7 @@ Lädt genau ein Bild für den im Pfad angegebenen POI hoch. Zulässig sind zwei
curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/pictures \
-F 'caption=Blick auf die Baumkrone' \
-F 'sequence=0' \
-F 'picture=@eiche.jpg;type=image/jpeg'
-F 'picture=@eiche.jpg'
```
#### Variante B: JSON mit Base64
@@ -533,7 +560,6 @@ Empfohlener Content-Type: `application/json`. Der Server akzeptiert zusätzlich
"sequence": 0,
"picture": {
"filename": "eiche.jpg",
"contentType": "image/jpeg",
"base64": "/9j/4AAQSkZJRgABAQ..."
}
}
@@ -553,7 +579,6 @@ jq -n \
sequence: $sequence,
picture: {
filename: "eiche.jpg",
contentType: "image/jpeg",
base64: $data
}
}' > picture.json
@@ -606,7 +631,7 @@ Datei und Metadaten per Multipart ersetzen:
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
-F 'caption=Neue Aufnahme der Eiche' \
-F 'sequence=2' \
-F 'picture=@eiche-neu.webp;type=image/webp'
-F 'picture=@eiche-neu.webp'
```
Datei und Metadaten per JSON ersetzen:
@@ -617,7 +642,6 @@ Datei und Metadaten per JSON ersetzen:
"sequence": 2,
"picture": {
"filename": "eiche-neu.webp",
"contentType": "image/webp",
"base64": "UklGRiQAAABXRUJQVlA4..."
}
}
@@ -691,11 +715,11 @@ Legt die Audiodatei des im Pfad angegebenen POIs an. Zulässig sind `multipart/f
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `audio` | Datei | ja | MP3-, MP4/M4A-, AAC-, Ogg-, WAV- oder WebM-Datei |
| `audio` | Datei | ja | MP3-, MP4/M4A-, AAC-, Ogg-, WAV- oder WebM-Datei; der Typ wird aus dem Inhalt erkannt |
```bash
curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \
-F 'audio=@ansage.mp3;type=audio/mpeg'
-F 'audio=@ansage.mp3'
```
#### Variante B: JSON mit Base64
@@ -704,7 +728,6 @@ curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \
{
"audio": {
"filename": "ansage.mp3",
"contentType": "audio/mpeg",
"base64": "SUQzBAAAAAAAI1RTU0UAAA..."
}
}
@@ -717,7 +740,6 @@ jq -n --rawfile data ansage.mp3.b64 \
'{
audio: {
filename: "ansage.mp3",
contentType: "audio/mpeg",
base64: $data
}
}' > audio.json
@@ -745,7 +767,7 @@ Multipart-Beispiel:
```bash
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/audio \
-F 'audio=@ansage-neu.ogg;type=audio/ogg'
-F 'audio=@ansage-neu.ogg'
```
JSON-Beispiel:
@@ -754,7 +776,6 @@ JSON-Beispiel:
{
"audio": {
"filename": "ansage-neu.ogg",
"contentType": "audio/ogg",
"base64": "T2dnUwACAAAAAAAAAAB..."
}
}
@@ -836,3 +857,10 @@ Die Zuordnung lautet:
| `audio.url` | `/api/routes/:routeId/pois/:poiId/audio` |
Die Dateinamen und relativen Pfade unter `storage/` bleiben ausschließlich interne Implementierungsdetails. Dateien gelöschter Routen liegen im Papierkorb und sind über keinen Datei-Endpunkt erreichbar.
## 12. Python-Werkzeuge
Unter `tools/python/` liegen interaktive Skripte für Anlegen, Ändern, Erweitern, Wiederherstellen und Löschen von Routen, POIs, Bildern und Audio. Sie verwenden ausschließlich die Python-Standardbibliothek. Fehlende Parameter werden abgefragt. Antwortet ein vorgeschalteter Webserver mit HTTP 401, fragt die gemeinsame Request-Schicht Benutzername und Passwort ab und wiederholt den ursprünglichen Request.
Details und Aufrufbeispiele stehen in [`tools/python/README.md`](../tools/python/README.md).