All checks were successful
Sonarqube Scanner / Build and analyze (push) Successful in 1m6s
867 lines
27 KiB
Markdown
867 lines
27 KiB
Markdown
# 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).
|