Wegwichtel/docs/REST-API.md

839 lines
25 KiB
Markdown
Raw 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 weiterhin ausschließlich als `multipart/form-data` übertragen.
- Bild- und Audioendpunkte akzeptieren wahlweise `multipart/form-data` oder ein JSON-Objekt mit Base64-kodierten Dateidaten.
- 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.
### 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`, `application/gpx+xml`, `application/xml`, `text/xml` |
| 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 | 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` |
| `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,...` |
`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.
### 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 |
| `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 |
| `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 |
| `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;type=application/gpx+xml'
```
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;type=application/gpx+xml'
```
### `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;type=application/gpx+xml'
```
## 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
}'
```
## 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 |
| `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;type=image/jpeg'
```
#### 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",
"contentType": "image/jpeg",
"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",
contentType: "image/jpeg",
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;type=image/webp'
```
Datei und Metadaten per JSON ersetzen:
```json
{
"caption": "Neue Aufnahme der Eiche",
"sequence": 2,
"picture": {
"filename": "eiche-neu.webp",
"contentType": "image/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 |
```bash
curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \
-F 'audio=@ansage.mp3;type=audio/mpeg'
```
#### Variante B: JSON mit Base64
```json
{
"audio": {
"filename": "ansage.mp3",
"contentType": "audio/mpeg",
"base64": "SUQzBAAAAAAAI1RTU0UAAA..."
}
}
```
```bash
base64 < ansage.mp3 | tr -d '\n' > ansage.mp3.b64
jq -n --rawfile data ansage.mp3.b64 \
'{
audio: {
filename: "ansage.mp3",
contentType: "audio/mpeg",
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;type=audio/ogg'
```
JSON-Beispiel:
```json
{
"audio": {
"filename": "ansage-neu.ogg",
"contentType": "audio/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.