Wegwichtel/docs/REST-API.md
Florian Zumpe 0def9b23db
All checks were successful
Sonarqube Scanner / Build and analyze (push) Successful in 1m6s
included serverside mime detection
2026-06-17 11:42:43 +02:00

867 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Wegwichtel REST-API
Diese Datei dokumentiert die vollständige HTTP-Schnittstelle des Wegwichtel-Servers. Sämtliche JSON-Ressourcen und aktiven GPX-, Bild- und Audiodateien werden unter `/api` bereitgestellt. Ein separates öffentliches `/media`-URL-Schema wird nicht verwendet.
## 1. Grundlagen
### Basisadressen
Lokaler Standard:
```text
http://127.0.0.1:47145
```
API-Basis:
```text
http://127.0.0.1:47145/api
```
Bei einer Nginx-Installation bleibt der Pfad gleich, beispielsweise:
```text
https://wegwichtel.example.org/api
```
### Formate
- 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 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 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
Das Dateilimit pro Datei wird durch `MAX_UPLOAD_MB` festgelegt und beträgt standardmäßig `50 MB`.
Unterstützte Dateitypen:
| Ressource | Dateiendungen beziehungsweise MIME-Typen |
|---|---|
| GPX | GPX-XML mit `<gpx>`-Wurzelelement |
| Bilder | JPEG, PNG, WebP |
| Audio | MP3, MP4/M4A, AAC, Ogg, WAV, WebM |
Die Uploadfelder heißen:
| Ressource | Multipart-Feld beziehungsweise JSON-Eigenschaft |
|---|---|
| GPX | `gpx` |
| einzelnes Bild | `picture` |
| einzelne Audiodatei | `audio` |
Bei einem JSON-Medienupload ist `picture` beziehungsweise `audio` ein Objekt:
| Eigenschaft | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `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; der dort genannte Medientyp wird ignoriert |
`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
```json
{
"error": "HttpError",
"message": "Strecke nicht gefunden."
}
```
Typische Statuscodes:
| Status | Bedeutung |
|---:|---|
| `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` |
| `415` | Dateityp wird nicht unterstützt |
| `500` | Interner Serverfehler |
### IDs
Alle IDs sind positive, von SQLite erzeugte Ganzzahlen.
- `:id` bezeichnet bei `/routes/:id/...` die Route.
- `:pictureId` bezeichnet einen Datensatz aus `poi_images`.
- `:poiId` bezeichnet den POI und zugleich seine höchstens eine Audioressource.
Beispiele verwenden überwiegend Route `1`, POI `7` und Bild `15`.
## 2. Datenmodelle
### Route
```json
{
"id": 1,
"slug": "schulwald-runde",
"name": "Schulwald-Runde",
"description": "Naturkundlicher Rundweg",
"schoolName": "Beispielschule",
"status": "active",
"start": { "lat": 52.5208, "lon": 13.407 },
"center": { "lat": 52.521, "lon": 13.408 },
"bounds": {
"minLat": 52.5208,
"minLon": 13.407,
"maxLat": 52.5212,
"maxLon": 13.409
},
"distanceM": 842.6,
"elevationGainM": 14.2,
"pointCount": 87,
"gpxUrl": "/api/routes/1/gpx",
"createdAt": "2026-06-16 12:00:00",
"updatedAt": "2026-06-16 12:00:00",
"deletedAt": null,
"proximityM": 324.8
}
```
### POI
```json
{
"id": 7,
"routeId": 1,
"title": "Die alte Eiche",
"description": "Hier wird das Alter der Eiche erklärt.",
"lat": 52.5208,
"lon": 13.407,
"triggerRadiusM": 60,
"sequence": 2,
"audioUrl": "/api/routes/1/pois/7/audio",
"images": [
{
"id": 15,
"caption": "Blick auf die Baumkrone",
"sequence": 0,
"url": "/api/routes/1/pois/7/pictures/15"
}
]
}
```
### Bildressource
```json
{
"id": 15,
"routeId": 1,
"poiId": 7,
"poiTitle": "Die alte Eiche",
"caption": "Blick auf die Baumkrone",
"sequence": 0,
"url": "/api/routes/1/pois/7/pictures/15",
"createdAt": "2026-06-16 12:30:00"
}
```
### Audioressource
Pro POI kann höchstens eine Audiodatei existieren. Deshalb wird die Audioressource über die `poiId` adressiert.
```json
{
"routeId": 1,
"poiId": 7,
"poiTitle": "Die alte Eiche",
"url": "/api/routes/1/pois/7/audio",
"updatedAt": "2026-06-16 12:35:00"
}
```
## 3. Endpunktübersicht
| Methode | Pfad | Zweck |
|---|---|---|
| `GET` | `/api/health` | Server- und SQLite-Zustand |
| `GET` | `/api/routes` | Routen auflisten |
| `GET` | `/api/routes/:id` | Route mit Punkten und POIs lesen |
| `GET` | `/api/routes/:id/gpx` | GPX-Datei der Route ausliefern |
| `POST` | `/api/routes` | Route anlegen |
| `PUT` | `/api/routes/:id` | Route aktualisieren |
| `POST` | `/api/routes/:id/append` | GPX-Punkte anhängen |
| `DELETE` | `/api/routes/:id` | Route weich löschen |
| `POST` | `/api/routes/:id/restore` | Route wiederherstellen |
| `GET` | `/api/routes/:id/pois` | POIs einer Route auflisten |
| `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 |
| `PUT` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | Bilddatei oder Metadaten aktualisieren |
| `DELETE` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | einzelnes Bild löschen |
| `GET` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei ausliefern; optional Metadaten mit `?metadata=true` |
| `POST` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei für den POI anlegen |
| `PUT` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei des POIs ersetzen |
| `DELETE` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei des POIs löschen |
## 4. Systemzustand
### `GET /api/health`
Parameter: keine.
```bash
curl http://127.0.0.1:47145/api/health
```
```json
{
"ok": true,
"service": "wegwichtel",
"socket": "127.0.0.1:47145",
"sqliteVersion": "3.46.1",
"timestamp": "2026-06-16T12:00:00.000Z"
}
```
## 5. Routen lesen
### `GET /api/routes`
Ohne Positionsparameter werden alle aktiven Routen alphabetisch geliefert. Mit `lat` und `lon` wird die Entfernung zum Routenstart berechnet, anhand `radiusKm` gefiltert und nach Entfernung sortiert.
#### Query-Parameter
| Parameter | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---:|---|
| `lat` | Dezimalzahl | nein | | Breitengrad; nur zusammen mit `lon` wirksam |
| `lon` | Dezimalzahl | nein | | Längengrad; nur zusammen mit `lat` wirksam |
| `radiusKm` | Dezimalzahl | nein | `DEFAULT_ROUTE_RADIUS_KM`, standardmäßig `25` | maximaler Abstand zum Routenstart |
| `includeDeleted` | Boolean-Text | nein | `false` | der exakte Wert `true` schließt gelöschte Routen ein |
Alle aktiven Routen:
```bash
curl http://127.0.0.1:47145/api/routes
```
Mit allen Query-Parametern:
```bash
curl --get http://127.0.0.1:47145/api/routes \
--data-urlencode 'lat=52.5208' \
--data-urlencode 'lon=13.4070' \
--data-urlencode 'radiusKm=12.5' \
--data-urlencode 'includeDeleted=true'
```
### `GET /api/routes/:id`
Liefert Route, GPX-Punkte und POIs einschließlich Medien-URLs.
#### Pfadparameter
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `id` | Ganzzahl | ja | Route |
#### Query-Parameter
| Parameter | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---:|---|
| `includeDeleted` | Boolean-Text | nein | `false` | mit `true` kann eine gelöschte Route gelesen werden |
```bash
curl http://127.0.0.1:47145/api/routes/1
```
```bash
curl 'http://127.0.0.1:47145/api/routes/1?includeDeleted=true'
```
### `GET /api/routes/:id/gpx`
Liefert die aktive GPX-Datei der Route direkt mit `Content-Type: application/gpx+xml` aus. Der in einer Routenressource enthaltene Wert `gpxUrl` verweist auf diesen Endpunkt.
```bash
curl http://127.0.0.1:47145/api/routes/1/gpx \
--output schulwald-runde.gpx
```
Parameter außer der Routen-ID sind nicht vorgesehen. Gelöschte oder unbekannte Routen antworten mit `404`.
## 6. Route anlegen und bearbeiten
### `POST /api/routes`
Legt eine Route aus einer GPX-Datei an.
Content-Type: `multipart/form-data`
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---|---|
| `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 |
| `schoolName` | Text | nein | leer | Schule oder Einrichtung |
```bash
curl -X POST http://127.0.0.1:47145/api/routes \
-F 'name=Schulwald-Runde' \
-F 'slug=schulwald-runde-klasse-7a' \
-F 'description=Naturkundlicher Rundweg der Klasse 7a' \
-F 'schoolName=Beispielschule' \
-F 'gpx=@examples/sample-route.gpx'
```
Erfolg: `201 Created` und `Location: /api/routes/<id>`.
### `PUT /api/routes/:id`
Aktualisiert Metadaten. Eine optionale GPX-Datei ersetzt alle bisherigen Trackpunkte.
| Feld | Typ | Pflicht | Verhalten ohne Feld |
|---|---|---:|---|
| `gpx` | Datei | nein | bisherige GPX-Punkte bleiben erhalten |
| `name` | Text | nein | bisheriger Wert bleibt |
| `description` | Text | nein | bisheriger Wert bleibt |
| `schoolName` | Text | nein | bisheriger Wert bleibt |
```bash
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'
```
### `POST /api/routes/:id/append`
Hängt alle Trackpunkte einer GPX-Datei an die Route an und berechnet Streckenwerte neu.
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `gpx` | Datei | ja | anzuhängende GPX-Datei |
```bash
curl -X POST http://127.0.0.1:47145/api/routes/1/append \
-F 'gpx=@verlaengerung.gpx'
```
## 7. POIs lesen und bearbeiten
### `GET /api/routes/:id/pois`
Liefert alle POIs einer aktiven Route nach `sequence` und `id`.
```bash
curl http://127.0.0.1:47145/api/routes/1/pois
```
### `GET /api/routes/:routeId/pois/:poiId`
Liefert einen einzelnen POI einschließlich seiner aktuellen Bild- und Audio-URLs. Route und POI werden gemeinsam geprüft; der POI muss zur angegebenen aktiven Route gehören.
| Pfadparameter | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `routeId` | Ganzzahl | ja | ID der aktiven Route |
| `poiId` | Ganzzahl | ja | ID des POIs innerhalb dieser Route |
```bash
curl http://127.0.0.1:47145/api/routes/1/pois/7
```
Eine unbekannte Route, ein unbekannter POI oder eine falsche Route-POI-Kombination liefert `404 Not Found`.
### `POST /api/routes/:id/pois`
Legt ausschließlich die POI-Metadaten an. Bilder und Audio werden anschließend über die gesonderten Medienendpunkte hochgeladen.
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---:|---|
| `title` | Text | nein | `Unbenannter POI` | Stationsname |
| `description` | Text | nein | leer | Beschreibung |
| `lat` | Dezimalzahl | ja | | Breitengrad |
| `lon` | Dezimalzahl | ja | | Längengrad |
| `triggerRadiusM` | Dezimalzahl | nein | `DEFAULT_POI_TRIGGER_METERS`, standardmäßig `80` | Aktivierungsradius in Metern |
| `sequence` | Ganzzahl | nein | `0` | Reihenfolge in der Stationsliste |
JSON-Beispiel mit allen Parametern:
```bash
curl -X POST http://127.0.0.1:47145/api/routes/1/pois \
-H 'Content-Type: application/json' \
-d '{
"title": "Die alte Eiche",
"description": "Hier wird das Alter der Eiche erklärt.",
"lat": 52.5208,
"lon": 13.4070,
"triggerRadiusM": 60,
"sequence": 2
}'
```
Erfolg: `201 Created` und `Location: /api/routes/<routeId>/pois/<poiId>`.
### `PUT /api/routes/:routeId/pois/:poiId`
Aktualisiert ausschließlich POI-Metadaten. Nicht übergebene Werte bleiben erhalten. Route und POI werden gemeinsam validiert; ein POI kann über diesen Endpunkt keiner anderen Route zugeordnet werden.
| Pfadparameter | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `routeId` | Ganzzahl | ja | ID der aktiven Route |
| `poiId` | Ganzzahl | ja | ID des POIs innerhalb dieser Route |
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `title` | Text | nein | neuer Stationsname |
| `description` | Text | nein | neue Beschreibung |
| `lat` | Dezimalzahl | nein | neuer Breitengrad |
| `lon` | Dezimalzahl | nein | neuer Längengrad |
| `triggerRadiusM` | Dezimalzahl | nein | neuer Aktivierungsradius |
| `sequence` | nichtnegative Ganzzahl | nein | neue Reihenfolge |
```bash
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7 \
-H 'Content-Type: application/json' \
-d '{
"title": "Die sehr alte Eiche",
"description": "Aktualisierte Beschreibung",
"lat": 52.5209,
"lon": 13.4071,
"triggerRadiusM": 45,
"sequence": 3
}'
```
### `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.
### Gemeinsame Pfadparameter
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `routeId` | positive Ganzzahl | ja | ID der aktiven Route |
| `poiId` | positive Ganzzahl | ja | ID eines POIs, der zu dieser Route gehört |
| `pictureId` | positive Ganzzahl | nur bei Einzelressourcen | ID des Bildes, das zu diesem POI gehört |
### `GET /api/routes/:routeId/pois/:poiId/pictures`
Liefert alle Bilder des angegebenen POIs in Diashow-Reihenfolge.
```bash
curl http://127.0.0.1:47145/api/routes/1/pois/7/pictures
```
```json
{
"pictures": [
{
"id": 15,
"routeId": 1,
"poiId": 7,
"poiTitle": "Die alte Eiche",
"caption": "Blick auf die Baumkrone",
"sequence": 0,
"url": "/api/routes/1/pois/7/pictures/15",
"createdAt": "2026-06-16 12:30:00"
}
]
}
```
### `GET /api/routes/:routeId/pois/:poiId/pictures/:pictureId`
Liefert standardmäßig die Bilddatei direkt aus. Genau dieser Pfad wird im Feld `url` der Bildressource und unter `pois[].images[].url` ausgegeben.
Bild speichern:
```bash
curl http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
--output bild-15.jpg
```
Metadaten statt Dateidaten abrufen:
```bash
curl --get http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
--data-urlencode 'metadata=true'
```
| Query-Parameter | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---:|---|
| `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Bilddatei liefern |
Die API antwortet mit `404`, wenn Route, POI oder Bild nicht zusammengehören.
### `POST /api/routes/:routeId/pois/:poiId/pictures`
Lädt genau ein Bild für den im Pfad angegebenen POI hoch. Zulässig sind zwei Übertragungsformen.
#### Variante A: `multipart/form-data`
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---:|---|
| `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 |
```bash
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'
```
#### Variante B: JSON mit Base64
Empfohlener Content-Type: `application/json`. Der Server akzeptiert zusätzlich `text/json`.
```json
{
"caption": "Blick auf die Baumkrone",
"sequence": 0,
"picture": {
"filename": "eiche.jpg",
"base64": "/9j/4AAQSkZJRgABAQ..."
}
}
```
Beispiel mit `text/json` und einer separat erzeugten Payload-Datei:
```bash
base64 < eiche.jpg | tr -d '\n' > eiche.jpg.b64
jq -n \
--arg caption 'Blick auf die Baumkrone' \
--argjson sequence 0 \
--rawfile data eiche.jpg.b64 \
'{
caption: $caption,
sequence: $sequence,
picture: {
filename: "eiche.jpg",
base64: $data
}
}' > picture.json
curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/pictures \
-H 'Content-Type: text/json' \
--data-binary @picture.json
```
Alternativ kann eine Data-URL übertragen werden:
```json
{
"caption": "Blick auf die Baumkrone",
"picture": {
"filename": "eiche.png",
"dataUrl": "data:image/png;base64,iVBORw0KGgoAAA..."
}
}
```
Ein Request mit `-d '{}'` schlägt mit `400 Bad Request` fehl, weil weder eine Multipart-Datei noch ein JSON-Dateiobjekt enthalten ist.
Erfolg: `201 Created` und `Location: /api/routes/1/pois/7/pictures/<pictureId>`.
### `PUT /api/routes/:routeId/pois/:poiId/pictures/:pictureId`
Aktualisiert Metadaten und kann optional die Datei ersetzen. Nicht übergebene Metadaten bleiben erhalten. Ein Bild kann über diesen Endpunkt nicht einem anderen POI zugeordnet werden; dafür muss es beim bisherigen POI gelöscht und beim Ziel-POI neu angelegt werden.
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `picture` | Multipart-Datei oder JSON-Dateiobjekt | nein | ersetzt die bisherige Bilddatei |
| `caption` | Text | nein | neue Bildbeschreibung; leerer Text entfernt die Beschreibung |
| `sequence` | nichtnegative Ganzzahl | nein | neue Position in der Diashow |
Nur Metadaten per JSON ändern:
```bash
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
-H 'Content-Type: application/json' \
-d '{
"caption": "Nahaufnahme der Eichenblätter",
"sequence": 1
}'
```
Datei und Metadaten per Multipart ersetzen:
```bash
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'
```
Datei und Metadaten per JSON ersetzen:
```json
{
"caption": "Neue Aufnahme der Eiche",
"sequence": 2,
"picture": {
"filename": "eiche-neu.webp",
"base64": "UklGRiQAAABXRUJQVlA4..."
}
}
```
```bash
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
-H 'Content-Type: application/json' \
--data-binary @picture-update.json
```
Wird eine Datei ersetzt, entfernt der Server die bisherige Datei nach erfolgreicher Datenbankaktualisierung.
### `DELETE /api/routes/:routeId/pois/:poiId/pictures/:pictureId`
Entfernt Bilddatensatz und Datei.
```bash
curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15
```
```json
{
"id": 15,
"routeId": 1,
"poiId": 7,
"deleted": true
}
```
## 9. Audiodatei eines POIs verwalten
Pro POI ist höchstens eine Audiodatei vorgesehen. Daher ist `/audio` selbst die Einzelressource; eine zusätzliche Audio-ID ist nicht erforderlich.
### Gemeinsame Pfadparameter
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `routeId` | positive Ganzzahl | ja | ID der aktiven Route |
| `poiId` | positive Ganzzahl | ja | ID eines POIs, der zu dieser Route gehört |
### `GET /api/routes/:routeId/pois/:poiId/audio`
Liefert standardmäßig die Audiodatei des POIs direkt aus. Genau dieser Pfad wird in `audioUrl` und im Feld `url` der Audioressource ausgegeben. Der Endpunkt unterstützt HTTP-Range-Anfragen, damit Browser innerhalb der Audiodatei springen können.
Audiodatei speichern:
```bash
curl http://127.0.0.1:47145/api/routes/1/pois/7/audio \
--output ansage-7.mp3
```
Metadaten statt Dateidaten abrufen:
```bash
curl --get http://127.0.0.1:47145/api/routes/1/pois/7/audio \
--data-urlencode 'metadata=true'
```
| Query-Parameter | Typ | Pflicht | Standard | Beschreibung |
|---|---|---:|---:|---|
| `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Audiodatei liefern |
Antwortet mit `404`, wenn Route und POI nicht zusammengehören oder der POI keine Audiodatei besitzt.
### `POST /api/routes/:routeId/pois/:poiId/audio`
Legt die Audiodatei des im Pfad angegebenen POIs an. Zulässig sind `multipart/form-data` und JSON mit Base64.
#### Variante A: `multipart/form-data`
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `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'
```
#### Variante B: JSON mit Base64
```json
{
"audio": {
"filename": "ansage.mp3",
"base64": "SUQzBAAAAAAAI1RTU0UAAA..."
}
}
```
```bash
base64 < ansage.mp3 | tr -d '\n' > ansage.mp3.b64
jq -n --rawfile data ansage.mp3.b64 \
'{
audio: {
filename: "ansage.mp3",
base64: $data
}
}' > audio.json
curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \
-H 'Content-Type: application/json' \
--data-binary @audio.json
```
Auch hier wird `Content-Type: text/json` akzeptiert. Ein leeres `{}` enthält keine Audiodatei und liefert `400 Bad Request`.
Erfolg: `201 Created` und `Location: /api/routes/1/pois/7/audio`.
Existiert bereits eine Audiodatei, antwortet der Server mit `409`. Zum Ersetzen ist `PUT` zu verwenden.
### `PUT /api/routes/:routeId/pois/:poiId/audio`
Ersetzt die vorhandene Audiodatei des POIs. Die Datei kann als Multipart-Upload oder als JSON-Dateiobjekt übertragen werden.
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---:|---|
| `audio` | Multipart-Datei oder JSON-Dateiobjekt | ja | neue Audiodatei |
Multipart-Beispiel:
```bash
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/audio \
-F 'audio=@ansage-neu.ogg'
```
JSON-Beispiel:
```json
{
"audio": {
"filename": "ansage-neu.ogg",
"base64": "T2dnUwACAAAAAAAAAAB..."
}
}
```
```bash
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/audio \
-H 'Content-Type: text/json' \
--data-binary @audio-update.json
```
Der Server entfernt die bisherige Datei nach erfolgreicher Aktualisierung. Besitzt der POI noch keine Audiodatei, antwortet der Server mit `404`; zum erstmaligen Anlegen ist `POST` zu verwenden.
### `DELETE /api/routes/:routeId/pois/:poiId/audio`
Entfernt die Audiodatei und setzt `audioUrl` des POIs auf `null`.
```bash
curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7/audio
```
```json
{
"routeId": 1,
"poiId": 7,
"deleted": true
}
```
## 10. Route weich löschen und wiederherstellen
### `DELETE /api/routes/:id`
Markiert die Route als gelöscht und verschiebt das vollständige Streckenverzeichnis mit GPX, Bildern und Audio nach `storage/trash/routes/`. Die Datenbankeinträge bleiben erhalten und ihre Pfade werden auf den Papierkorb umgeschrieben.
```bash
curl -X DELETE http://127.0.0.1:47145/api/routes/1
```
```json
{
"route": {
"id": 1,
"status": "deleted",
"gpxUrl": null
},
"softDeleted": true
}
```
### `POST /api/routes/:id/restore`
Verschiebt eine gelöschte Route zurück in den aktiven Speicher und schreibt alle GPX-, Bild- und Audiopfade zurück.
Parameter: keine.
```bash
curl -X POST http://127.0.0.1:47145/api/routes/1/restore
```
## 11. Dateien über REST ausliefern
Die API veröffentlicht keine internen Speicherpfade und keine `/media/...`-Adressen. Alle in JSON ausgegebenen Datei-URLs verweisen auf stabile REST-Endpunkte:
```text
GET /api/routes/1/gpx
GET /api/routes/1/pois/7/pictures/15
GET /api/routes/1/pois/7/audio
```
Die Zuordnung lautet:
| JSON-Feld | Datei-Endpunkt |
|---|---|
| `route.gpxUrl` | `/api/routes/:id/gpx` |
| `poi.audioUrl` | `/api/routes/:routeId/pois/:poiId/audio` |
| `poi.images[].url` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` |
| `picture.url` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` |
| `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).