/pictures und /audio endpoints moved under /poi
This commit is contained in:
parent
833b5ad3a4
commit
5c85a0194b
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,6 +7,3 @@ storage/trash/routes/*
|
|||||||
!storage/trash/routes/.gitkeep
|
!storage/trash/routes/.gitkeep
|
||||||
.env
|
.env
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
||||||
nginx/
|
|
||||||
systemd/
|
|
||||||
|
|||||||
165
docs/REST-API.md
165
docs/REST-API.md
@ -129,13 +129,13 @@ Beispiele verwenden überwiegend Route `1`, POI `7` und Bild `15`.
|
|||||||
"lon": 13.407,
|
"lon": 13.407,
|
||||||
"triggerRadiusM": 60,
|
"triggerRadiusM": 60,
|
||||||
"sequence": 2,
|
"sequence": 2,
|
||||||
"audioUrl": "/api/routes/1/audio/7",
|
"audioUrl": "/api/routes/1/pois/7/audio",
|
||||||
"images": [
|
"images": [
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 15,
|
||||||
"caption": "Blick auf die Baumkrone",
|
"caption": "Blick auf die Baumkrone",
|
||||||
"sequence": 0,
|
"sequence": 0,
|
||||||
"url": "/api/routes/1/pictures/15"
|
"url": "/api/routes/1/pois/7/pictures/15"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -151,7 +151,7 @@ Beispiele verwenden überwiegend Route `1`, POI `7` und Bild `15`.
|
|||||||
"poiTitle": "Die alte Eiche",
|
"poiTitle": "Die alte Eiche",
|
||||||
"caption": "Blick auf die Baumkrone",
|
"caption": "Blick auf die Baumkrone",
|
||||||
"sequence": 0,
|
"sequence": 0,
|
||||||
"url": "/api/routes/1/pictures/15",
|
"url": "/api/routes/1/pois/7/pictures/15",
|
||||||
"createdAt": "2026-06-16 12:30:00"
|
"createdAt": "2026-06-16 12:30:00"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -165,7 +165,7 @@ Pro POI kann höchstens eine Audiodatei existieren. Deshalb wird die Audioressou
|
|||||||
"routeId": 1,
|
"routeId": 1,
|
||||||
"poiId": 7,
|
"poiId": 7,
|
||||||
"poiTitle": "Die alte Eiche",
|
"poiTitle": "Die alte Eiche",
|
||||||
"url": "/api/routes/1/audio/7",
|
"url": "/api/routes/1/pois/7/audio",
|
||||||
"updatedAt": "2026-06-16 12:35:00"
|
"updatedAt": "2026-06-16 12:35:00"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -187,16 +187,15 @@ Pro POI kann höchstens eine Audiodatei existieren. Deshalb wird die Audioressou
|
|||||||
| `GET` | `/api/pois/:id` | einzelnen POI lesen |
|
| `GET` | `/api/pois/:id` | einzelnen POI lesen |
|
||||||
| `POST` | `/api/routes/:id/pois` | POI-Metadaten anlegen |
|
| `POST` | `/api/routes/:id/pois` | POI-Metadaten anlegen |
|
||||||
| `PUT` | `/api/pois/:id` | POI-Metadaten aktualisieren |
|
| `PUT` | `/api/pois/:id` | POI-Metadaten aktualisieren |
|
||||||
| `GET` | `/api/routes/:id/pictures` | Bilder einer Route auflisten |
|
| `GET` | `/api/routes/:routeId/pois/:poiId/pictures` | Bilder eines POIs auflisten |
|
||||||
| `GET` | `/api/routes/:id/pictures/:pictureId` | Bilddatei ausliefern; optional Metadaten mit `?metadata=true` |
|
| `GET` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | Bilddatei ausliefern; optional Metadaten mit `?metadata=true` |
|
||||||
| `POST` | `/api/routes/:id/pictures` | einzelnes Bild hochladen |
|
| `POST` | `/api/routes/:routeId/pois/:poiId/pictures` | einzelnes Bild für den POI hochladen |
|
||||||
| `PUT` | `/api/routes/:id/pictures/:pictureId` | Bilddatei oder Metadaten aktualisieren |
|
| `PUT` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | Bilddatei oder Metadaten aktualisieren |
|
||||||
| `DELETE` | `/api/routes/:id/pictures/:pictureId` | einzelnes Bild löschen |
|
| `DELETE` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | einzelnes Bild löschen |
|
||||||
| `GET` | `/api/routes/:id/audio` | Audiodateien einer Route auflisten |
|
| `GET` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei ausliefern; optional Metadaten mit `?metadata=true` |
|
||||||
| `GET` | `/api/routes/:id/audio/:poiId` | Audiodatei ausliefern; optional Metadaten mit `?metadata=true` |
|
| `POST` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei für den POI anlegen |
|
||||||
| `POST` | `/api/routes/:id/audio` | Audiodatei für einen POI anlegen |
|
| `PUT` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei des POIs ersetzen |
|
||||||
| `PUT` | `/api/routes/:id/audio/:poiId` | Audiodatei eines POIs ersetzen |
|
| `DELETE` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei des POIs löschen |
|
||||||
| `DELETE` | `/api/routes/:id/audio/:poiId` | Audiodatei eines POIs löschen |
|
|
||||||
|
|
||||||
## 4. Systemzustand
|
## 4. Systemzustand
|
||||||
|
|
||||||
@ -417,29 +416,24 @@ curl -X PUT http://127.0.0.1:47145/api/pois/7 \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8. Bilder einzeln verwalten
|
## 8. Bilder eines POIs einzeln verwalten
|
||||||
|
|
||||||
### `GET /api/routes/:id/pictures`
|
Die Route und der POI sind Bestandteil jedes Bildpfades. Dadurch ist die Zuordnung eindeutig und beim Upload muss keine zusätzliche `poiId` übergeben werden.
|
||||||
|
|
||||||
Liefert alle Bilder der Route. Optional kann auf einen POI eingeschränkt werden.
|
### Gemeinsame Pfadparameter
|
||||||
|
|
||||||
#### Query-Parameter
|
|
||||||
|
|
||||||
| Parameter | Typ | Pflicht | Beschreibung |
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|---|---|---:|---|
|
|---|---|---:|---|
|
||||||
| `poiId` | positive Ganzzahl | nein | liefert nur Bilder dieses POIs |
|
| `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 |
|
||||||
|
|
||||||
Alle Bilder der Route:
|
### `GET /api/routes/:routeId/pois/:poiId/pictures`
|
||||||
|
|
||||||
|
Liefert alle Bilder des angegebenen POIs in Diashow-Reihenfolge.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:47145/api/routes/1/pictures
|
curl http://127.0.0.1:47145/api/routes/1/pois/7/pictures
|
||||||
```
|
|
||||||
|
|
||||||
Nur Bilder des POIs `7`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl --get http://127.0.0.1:47145/api/routes/1/pictures \
|
|
||||||
--data-urlencode 'poiId=7'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -452,28 +446,28 @@ curl --get http://127.0.0.1:47145/api/routes/1/pictures \
|
|||||||
"poiTitle": "Die alte Eiche",
|
"poiTitle": "Die alte Eiche",
|
||||||
"caption": "Blick auf die Baumkrone",
|
"caption": "Blick auf die Baumkrone",
|
||||||
"sequence": 0,
|
"sequence": 0,
|
||||||
"url": "/api/routes/1/pictures/15",
|
"url": "/api/routes/1/pois/7/pictures/15",
|
||||||
"createdAt": "2026-06-16 12:30:00"
|
"createdAt": "2026-06-16 12:30:00"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `GET /api/routes/:id/pictures/:pictureId`
|
### `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.
|
Liefert standardmäßig die Bilddatei direkt aus. Genau dieser Pfad wird im Feld `url` der Bildressource und unter `pois[].images[].url` ausgegeben.
|
||||||
|
|
||||||
Bild speichern:
|
Bild speichern:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:47145/api/routes/1/pictures/15 \
|
curl http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
|
||||||
--output bild-15.jpg
|
--output bild-15.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Metadaten statt Dateidaten abrufen:
|
Metadaten statt Dateidaten abrufen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --get http://127.0.0.1:47145/api/routes/1/pictures/15 \
|
curl --get http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
|
||||||
--data-urlencode 'metadata=true'
|
--data-urlencode 'metadata=true'
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -481,57 +475,54 @@ curl --get http://127.0.0.1:47145/api/routes/1/pictures/15 \
|
|||||||
|---|---|---:|---:|---|
|
|---|---|---:|---:|---|
|
||||||
| `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Bilddatei liefern |
|
| `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Bilddatei liefern |
|
||||||
|
|
||||||
### `POST /api/routes/:id/pictures`
|
Die API antwortet mit `404`, wenn Route, POI oder Bild nicht zusammengehören.
|
||||||
|
|
||||||
Lädt genau ein Bild hoch und ordnet es einem POI derselben Route zu.
|
### `POST /api/routes/:routeId/pois/:poiId/pictures`
|
||||||
|
|
||||||
|
Lädt genau ein Bild für den im Pfad angegebenen POI hoch.
|
||||||
|
|
||||||
Content-Type: `multipart/form-data`
|
Content-Type: `multipart/form-data`
|
||||||
|
|
||||||
| Feld | Typ | Pflicht | Standard | Beschreibung |
|
| Feld | Typ | Pflicht | Standard | Beschreibung |
|
||||||
|---|---|---:|---:|---|
|
|---|---|---:|---:|---|
|
||||||
| `picture` | Datei | ja | – | JPEG-, PNG- oder WebP-Datei |
|
| `picture` | Datei | ja | – | JPEG-, PNG- oder WebP-Datei |
|
||||||
| `poiId` | positive Ganzzahl | ja | – | Ziel-POI derselben Route |
|
|
||||||
| `caption` | Text | nein | leer | sichtbare Bildbeschreibung und Grundlage für den Alternativtext |
|
| `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 |
|
| `sequence` | nichtnegative Ganzzahl | nein | nächster freier Wert des POIs | Reihenfolge in der Diashow |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://127.0.0.1:47145/api/routes/1/pictures \
|
curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/pictures \
|
||||||
-F 'poiId=7' \
|
|
||||||
-F 'caption=Blick auf die Baumkrone' \
|
-F 'caption=Blick auf die Baumkrone' \
|
||||||
-F 'sequence=0' \
|
-F 'sequence=0' \
|
||||||
-F 'picture=@eiche.jpg;type=image/jpeg'
|
-F 'picture=@eiche.jpg;type=image/jpeg'
|
||||||
```
|
```
|
||||||
|
|
||||||
Erfolg: `201 Created` und `Location: /api/routes/1/pictures/<pictureId>`.
|
Erfolg: `201 Created` und `Location: /api/routes/1/pois/7/pictures/<pictureId>`.
|
||||||
|
|
||||||
### `PUT /api/routes/:id/pictures/:pictureId`
|
### `PUT /api/routes/:routeId/pois/:poiId/pictures/:pictureId`
|
||||||
|
|
||||||
Aktualisiert Metadaten und kann optional die Datei ersetzen. Nicht übergebene Metadaten bleiben erhalten.
|
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 |
|
| Feld | Typ | Pflicht | Beschreibung |
|
||||||
|---|---|---:|---|
|
|---|---|---:|---|
|
||||||
| `picture` | Datei | nein | ersetzt die bisherige Bilddatei |
|
| `picture` | Datei | nein | ersetzt die bisherige Bilddatei |
|
||||||
| `poiId` | positive Ganzzahl | nein | ordnet das Bild einem anderen POI derselben Route zu |
|
|
||||||
| `caption` | Text | nein | neue Bildbeschreibung; leerer Text entfernt die Beschreibung |
|
| `caption` | Text | nein | neue Bildbeschreibung; leerer Text entfernt die Beschreibung |
|
||||||
| `sequence` | nichtnegative Ganzzahl | nein | neue Position in der Diashow |
|
| `sequence` | nichtnegative Ganzzahl | nein | neue Position in der Diashow |
|
||||||
|
|
||||||
Nur Metadaten per JSON ändern:
|
Nur Metadaten per JSON ändern:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X PUT http://127.0.0.1:47145/api/routes/1/pictures/15 \
|
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{
|
-d '{
|
||||||
"poiId": 7,
|
|
||||||
"caption": "Nahaufnahme der Eichenblätter",
|
"caption": "Nahaufnahme der Eichenblätter",
|
||||||
"sequence": 1
|
"sequence": 1
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Datei und alle Metadaten ersetzen:
|
Datei und Metadaten ersetzen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X PUT http://127.0.0.1:47145/api/routes/1/pictures/15 \
|
curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \
|
||||||
-F 'poiId=7' \
|
|
||||||
-F 'caption=Neue Aufnahme der Eiche' \
|
-F 'caption=Neue Aufnahme der Eiche' \
|
||||||
-F 'sequence=2' \
|
-F 'sequence=2' \
|
||||||
-F 'picture=@eiche-neu.webp;type=image/webp'
|
-F 'picture=@eiche-neu.webp;type=image/webp'
|
||||||
@ -539,12 +530,12 @@ curl -X PUT http://127.0.0.1:47145/api/routes/1/pictures/15 \
|
|||||||
|
|
||||||
Wird eine Datei ersetzt, entfernt der Server die bisherige Datei nach erfolgreicher Datenbankaktualisierung.
|
Wird eine Datei ersetzt, entfernt der Server die bisherige Datei nach erfolgreicher Datenbankaktualisierung.
|
||||||
|
|
||||||
### `DELETE /api/routes/:id/pictures/:pictureId`
|
### `DELETE /api/routes/:routeId/pois/:poiId/pictures/:pictureId`
|
||||||
|
|
||||||
Entfernt Bilddatensatz und Datei.
|
Entfernt Bilddatensatz und Datei.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X DELETE http://127.0.0.1:47145/api/routes/1/pictures/15
|
curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -556,58 +547,32 @@ curl -X DELETE http://127.0.0.1:47145/api/routes/1/pictures/15
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 9. Audiodateien einzeln verwalten
|
## 9. Audiodatei eines POIs verwalten
|
||||||
|
|
||||||
Pro POI ist höchstens eine Audiodatei vorgesehen. Deshalb bildet die `poiId` den Schlüssel der Audioressource.
|
Pro POI ist höchstens eine Audiodatei vorgesehen. Daher ist `/audio` selbst die Einzelressource; eine zusätzliche Audio-ID ist nicht erforderlich.
|
||||||
|
|
||||||
### `GET /api/routes/:id/audio`
|
### Gemeinsame Pfadparameter
|
||||||
|
|
||||||
Liefert alle vorhandenen Audiodateien der Route. POIs ohne Audio werden nicht ausgegeben.
|
|
||||||
|
|
||||||
#### Query-Parameter
|
|
||||||
|
|
||||||
| Parameter | Typ | Pflicht | Beschreibung |
|
| Parameter | Typ | Pflicht | Beschreibung |
|
||||||
|---|---|---:|---|
|
|---|---|---:|---|
|
||||||
| `poiId` | positive Ganzzahl | nein | schränkt die Liste auf einen POI ein |
|
| `routeId` | positive Ganzzahl | ja | ID der aktiven Route |
|
||||||
|
| `poiId` | positive Ganzzahl | ja | ID eines POIs, der zu dieser Route gehört |
|
||||||
|
|
||||||
```bash
|
### `GET /api/routes/:routeId/pois/:poiId/audio`
|
||||||
curl http://127.0.0.1:47145/api/routes/1/audio
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
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.
|
||||||
curl --get http://127.0.0.1:47145/api/routes/1/audio \
|
|
||||||
--data-urlencode 'poiId=7'
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"audio": [
|
|
||||||
{
|
|
||||||
"routeId": 1,
|
|
||||||
"poiId": 7,
|
|
||||||
"poiTitle": "Die alte Eiche",
|
|
||||||
"url": "/api/routes/1/audio/7",
|
|
||||||
"updatedAt": "2026-06-16 12:35:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `GET /api/routes/:id/audio/:poiId`
|
|
||||||
|
|
||||||
Liefert standardmäßig die Audiodatei des POIs direkt aus. Genau dieser Pfad wird in `audioUrl` und im Feld `url` einer Audioressource ausgegeben. Der Endpunkt unterstützt HTTP-Range-Anfragen, damit Browser innerhalb einer Audiodatei springen können.
|
|
||||||
|
|
||||||
Audiodatei speichern:
|
Audiodatei speichern:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:47145/api/routes/1/audio/7 \
|
curl http://127.0.0.1:47145/api/routes/1/pois/7/audio \
|
||||||
--output ansage-7.mp3
|
--output ansage-7.mp3
|
||||||
```
|
```
|
||||||
|
|
||||||
Metadaten statt Dateidaten abrufen:
|
Metadaten statt Dateidaten abrufen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --get http://127.0.0.1:47145/api/routes/1/audio/7 \
|
curl --get http://127.0.0.1:47145/api/routes/1/pois/7/audio \
|
||||||
--data-urlencode 'metadata=true'
|
--data-urlencode 'metadata=true'
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -615,30 +580,28 @@ curl --get http://127.0.0.1:47145/api/routes/1/audio/7 \
|
|||||||
|---|---|---:|---:|---|
|
|---|---|---:|---:|---|
|
||||||
| `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Audiodatei liefern |
|
| `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Audiodatei liefern |
|
||||||
|
|
||||||
Antwortet mit `404`, wenn der POI keine Audiodatei besitzt.
|
Antwortet mit `404`, wenn Route und POI nicht zusammengehören oder der POI keine Audiodatei besitzt.
|
||||||
|
|
||||||
### `POST /api/routes/:id/audio`
|
### `POST /api/routes/:routeId/pois/:poiId/audio`
|
||||||
|
|
||||||
Legt die Audiodatei eines POIs an.
|
Legt die Audiodatei des im Pfad angegebenen POIs an.
|
||||||
|
|
||||||
Content-Type: `multipart/form-data`
|
Content-Type: `multipart/form-data`
|
||||||
|
|
||||||
| Feld | Typ | Pflicht | Beschreibung |
|
| 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 |
|
||||||
| `poiId` | positive Ganzzahl | ja | POI derselben Route |
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://127.0.0.1:47145/api/routes/1/audio \
|
curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \
|
||||||
-F 'poiId=7' \
|
|
||||||
-F 'audio=@ansage.mp3;type=audio/mpeg'
|
-F 'audio=@ansage.mp3;type=audio/mpeg'
|
||||||
```
|
```
|
||||||
|
|
||||||
Erfolg: `201 Created` und `Location: /api/routes/1/audio/7`.
|
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.
|
Existiert bereits eine Audiodatei, antwortet der Server mit `409`. Zum Ersetzen ist `PUT` zu verwenden.
|
||||||
|
|
||||||
### `PUT /api/routes/:id/audio/:poiId`
|
### `PUT /api/routes/:routeId/pois/:poiId/audio`
|
||||||
|
|
||||||
Ersetzt die vorhandene Audiodatei des POIs.
|
Ersetzt die vorhandene Audiodatei des POIs.
|
||||||
|
|
||||||
@ -647,18 +610,18 @@ Ersetzt die vorhandene Audiodatei des POIs.
|
|||||||
| `audio` | Datei | ja | neue Audiodatei |
|
| `audio` | Datei | ja | neue Audiodatei |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X PUT http://127.0.0.1:47145/api/routes/1/audio/7 \
|
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;type=audio/ogg'
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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/:id/audio/:poiId`
|
### `DELETE /api/routes/:routeId/pois/:poiId/audio`
|
||||||
|
|
||||||
Entfernt die Audiodatei und setzt `audioUrl` des POIs auf `null`.
|
Entfernt die Audiodatei und setzt `audioUrl` des POIs auf `null`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X DELETE http://127.0.0.1:47145/api/routes/1/audio/7
|
curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7/audio
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -706,8 +669,8 @@ Die API veröffentlicht keine internen Speicherpfade und keine `/media/...`-Adre
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
GET /api/routes/1/gpx
|
GET /api/routes/1/gpx
|
||||||
GET /api/routes/1/pictures/15
|
GET /api/routes/1/pois/7/pictures/15
|
||||||
GET /api/routes/1/audio/7
|
GET /api/routes/1/pois/7/audio
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Zuordnung lautet:
|
Die Zuordnung lautet:
|
||||||
@ -715,9 +678,9 @@ Die Zuordnung lautet:
|
|||||||
| JSON-Feld | Datei-Endpunkt |
|
| JSON-Feld | Datei-Endpunkt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `route.gpxUrl` | `/api/routes/:id/gpx` |
|
| `route.gpxUrl` | `/api/routes/:id/gpx` |
|
||||||
| `poi.audioUrl` | `/api/routes/:id/audio/:poiId` |
|
| `poi.audioUrl` | `/api/routes/:routeId/pois/:poiId/audio` |
|
||||||
| `poi.images[].url` | `/api/routes/:id/pictures/:pictureId` |
|
| `poi.images[].url` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` |
|
||||||
| `picture.url` | `/api/routes/:id/pictures/:pictureId` |
|
| `picture.url` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` |
|
||||||
| `audio.url` | `/api/routes/:id/audio/:poiId` |
|
| `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.
|
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.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
|
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
} from '../services/routes-service.js';
|
} from '../services/routes-service.js';
|
||||||
import {
|
import {
|
||||||
listPictures, getPicture, getPictureFile, createPicture, updatePicture, deletePicture,
|
listPictures, getPicture, getPictureFile, createPicture, updatePicture, deletePicture,
|
||||||
listAudio, getAudio, getAudioFile, createAudio, updateAudio, deleteAudio
|
getAudio, getAudioFile, createAudio, updateAudio, deleteAudio
|
||||||
} from '../services/media-service.js';
|
} from '../services/media-service.js';
|
||||||
|
|
||||||
const numberOrUndefined = value => {
|
const numberOrUndefined = value => {
|
||||||
@ -90,41 +90,52 @@ export function createApiRouter(db) {
|
|||||||
res.json(updatePoi(db, Number(req.params.id), req.body));
|
res.json(updatePoi(db, Number(req.params.id), req.body));
|
||||||
});
|
});
|
||||||
|
|
||||||
api.get('/routes/:id/pictures', (req, res) => {
|
api.get('/routes/:routeId/pois/:poiId/pictures', (req, res) => {
|
||||||
res.json({ pictures: listPictures(db, Number(req.params.id), { poiId: req.query.poiId }) });
|
res.json({ pictures: listPictures(db, Number(req.params.routeId), Number(req.params.poiId)) });
|
||||||
});
|
});
|
||||||
|
|
||||||
api.get('/routes/:id/pictures/:pictureId', (req, res) => {
|
api.get('/routes/:routeId/pois/:poiId/pictures/:pictureId', (req, res) => {
|
||||||
const routeId = Number(req.params.id);
|
const routeId = Number(req.params.routeId);
|
||||||
|
const poiId = Number(req.params.poiId);
|
||||||
const pictureId = Number(req.params.pictureId);
|
const pictureId = Number(req.params.pictureId);
|
||||||
if (req.query.metadata === 'true') {
|
if (req.query.metadata === 'true') {
|
||||||
res.json(getPicture(db, routeId, pictureId));
|
res.json(getPicture(db, routeId, poiId, pictureId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendFileResource(res, getPictureFile(db, routeId, pictureId));
|
sendFileResource(res, getPictureFile(db, routeId, poiId, pictureId));
|
||||||
});
|
});
|
||||||
|
|
||||||
api.post('/routes/:id/pictures', upload.single('picture'), (req, res) => {
|
api.post('/routes/:routeId/pois/:poiId/pictures', upload.single('picture'), (req, res) => {
|
||||||
const picture = createPicture(db, Number(req.params.id), req.body, req.file);
|
const routeId = Number(req.params.routeId);
|
||||||
|
const poiId = Number(req.params.poiId);
|
||||||
|
const picture = createPicture(db, routeId, poiId, req.body, req.file);
|
||||||
res.status(201)
|
res.status(201)
|
||||||
.location(`/api/routes/${req.params.id}/pictures/${picture.id}`)
|
.location(`/api/routes/${routeId}/pois/${poiId}/pictures/${picture.id}`)
|
||||||
.json(picture);
|
.json(picture);
|
||||||
});
|
});
|
||||||
|
|
||||||
api.put('/routes/:id/pictures/:pictureId', upload.single('picture'), (req, res) => {
|
api.put('/routes/:routeId/pois/:poiId/pictures/:pictureId', upload.single('picture'), (req, res) => {
|
||||||
res.json(updatePicture(db, Number(req.params.id), Number(req.params.pictureId), req.body, req.file));
|
res.json(updatePicture(
|
||||||
|
db,
|
||||||
|
Number(req.params.routeId),
|
||||||
|
Number(req.params.poiId),
|
||||||
|
Number(req.params.pictureId),
|
||||||
|
req.body,
|
||||||
|
req.file
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
api.delete('/routes/:id/pictures/:pictureId', (req, res) => {
|
api.delete('/routes/:routeId/pois/:poiId/pictures/:pictureId', (req, res) => {
|
||||||
res.json(deletePicture(db, Number(req.params.id), Number(req.params.pictureId)));
|
res.json(deletePicture(
|
||||||
|
db,
|
||||||
|
Number(req.params.routeId),
|
||||||
|
Number(req.params.poiId),
|
||||||
|
Number(req.params.pictureId)
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
api.get('/routes/:id/audio', (req, res) => {
|
api.get('/routes/:routeId/pois/:poiId/audio', (req, res) => {
|
||||||
res.json({ audio: listAudio(db, Number(req.params.id), { poiId: req.query.poiId }) });
|
const routeId = Number(req.params.routeId);
|
||||||
});
|
|
||||||
|
|
||||||
api.get('/routes/:id/audio/:poiId', (req, res) => {
|
|
||||||
const routeId = Number(req.params.id);
|
|
||||||
const poiId = Number(req.params.poiId);
|
const poiId = Number(req.params.poiId);
|
||||||
if (req.query.metadata === 'true') {
|
if (req.query.metadata === 'true') {
|
||||||
res.json(getAudio(db, routeId, poiId));
|
res.json(getAudio(db, routeId, poiId));
|
||||||
@ -133,19 +144,21 @@ export function createApiRouter(db) {
|
|||||||
sendFileResource(res, getAudioFile(db, routeId, poiId));
|
sendFileResource(res, getAudioFile(db, routeId, poiId));
|
||||||
});
|
});
|
||||||
|
|
||||||
api.post('/routes/:id/audio', upload.single('audio'), (req, res) => {
|
api.post('/routes/:routeId/pois/:poiId/audio', upload.single('audio'), (req, res) => {
|
||||||
const audio = createAudio(db, Number(req.params.id), req.body, req.file);
|
const routeId = Number(req.params.routeId);
|
||||||
|
const poiId = Number(req.params.poiId);
|
||||||
|
const audio = createAudio(db, routeId, poiId, req.file);
|
||||||
res.status(201)
|
res.status(201)
|
||||||
.location(`/api/routes/${req.params.id}/audio/${audio.poiId}`)
|
.location(`/api/routes/${routeId}/pois/${poiId}/audio`)
|
||||||
.json(audio);
|
.json(audio);
|
||||||
});
|
});
|
||||||
|
|
||||||
api.put('/routes/:id/audio/:poiId', upload.single('audio'), (req, res) => {
|
api.put('/routes/:routeId/pois/:poiId/audio', upload.single('audio'), (req, res) => {
|
||||||
res.json(updateAudio(db, Number(req.params.id), Number(req.params.poiId), req.file));
|
res.json(updateAudio(db, Number(req.params.routeId), Number(req.params.poiId), req.file));
|
||||||
});
|
});
|
||||||
|
|
||||||
api.delete('/routes/:id/audio/:poiId', (req, res) => {
|
api.delete('/routes/:routeId/pois/:poiId/audio', (req, res) => {
|
||||||
res.json(deleteAudio(db, Number(req.params.id), Number(req.params.poiId)));
|
res.json(deleteAudio(db, Number(req.params.routeId), Number(req.params.poiId)));
|
||||||
});
|
});
|
||||||
|
|
||||||
api.delete('/routes/:id', (req, res) => {
|
api.delete('/routes/:id', (req, res) => {
|
||||||
|
|||||||
@ -24,16 +24,8 @@ const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
|||||||
const AUDIO_EXTENSIONS = new Set(['.mp3', '.mp4', '.m4a', '.aac', '.ogg', '.wav', '.webm']);
|
const AUDIO_EXTENSIONS = new Set(['.mp3', '.mp4', '.m4a', '.aac', '.ogg', '.wav', '.webm']);
|
||||||
|
|
||||||
const uniqueFilename = original => `${crypto.randomUUID()}${path.extname(original).toLowerCase()}`;
|
const uniqueFilename = original => `${crypto.randomUUID()}${path.extname(original).toLowerCase()}`;
|
||||||
const pictureUrl = (routeId, pictureId) => `/api/routes/${routeId}/pictures/${pictureId}`;
|
const pictureUrl = (routeId, poiId, pictureId) => `/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`;
|
||||||
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/audio/${poiId}`;
|
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/pois/${poiId}/audio`;
|
||||||
|
|
||||||
function requirePositiveInteger(value, name) {
|
|
||||||
const parsed = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
||||||
throw new HttpError(400, `${name} muss eine positive Ganzzahl sein.`);
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalSequence(value, fallback) {
|
function optionalSequence(value, fallback) {
|
||||||
if (value == null || value === '') return fallback;
|
if (value == null || value === '') return fallback;
|
||||||
@ -44,15 +36,9 @@ function optionalSequence(value, fallback) {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function activeRoute(db, routeId) {
|
|
||||||
const route = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(routeId);
|
|
||||||
if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
|
|
||||||
function poiOnRoute(db, routeId, poiId) {
|
function poiOnRoute(db, routeId, poiId) {
|
||||||
const poi = db.prepare(`
|
const poi = db.prepare(`
|
||||||
SELECT p.id, p.route_id, p.title, p.audio_path
|
SELECT p.id, p.route_id, p.title, p.audio_path, p.updated_at
|
||||||
FROM pois p
|
FROM pois p
|
||||||
JOIN routes r ON r.id = p.route_id
|
JOIN routes r ON r.id = p.route_id
|
||||||
WHERE p.id = ? AND p.route_id = ? AND r.status = 'active'
|
WHERE p.id = ? AND p.route_id = ? AND r.status = 'active'
|
||||||
@ -85,7 +71,7 @@ function pictureFromRow(row) {
|
|||||||
poiTitle: row.poi_title,
|
poiTitle: row.poi_title,
|
||||||
caption: row.caption,
|
caption: row.caption,
|
||||||
sequence: row.sequence,
|
sequence: row.sequence,
|
||||||
url: pictureUrl(row.route_id, row.id),
|
url: pictureUrl(row.route_id, row.poi_id, row.id),
|
||||||
createdAt: row.created_at
|
createdAt: row.created_at
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -98,40 +84,42 @@ const pictureSelect = `
|
|||||||
JOIN routes r ON r.id = p.route_id
|
JOIN routes r ON r.id = p.route_id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function listPictures(db, routeId, { poiId } = {}) {
|
export function listPictures(db, routeId, poiId) {
|
||||||
activeRoute(db, routeId);
|
poiOnRoute(db, routeId, poiId);
|
||||||
const filterPoiId = poiId == null || poiId === '' ? null : requirePositiveInteger(poiId, 'poiId');
|
return db.prepare(`${pictureSelect}
|
||||||
const rows = filterPoiId == null
|
WHERE p.route_id = ? AND p.id = ? AND r.status = 'active'
|
||||||
? db.prepare(`${pictureSelect} WHERE p.route_id = ? AND r.status = 'active' ORDER BY p.sequence, p.id, i.sequence, i.id`).all(routeId)
|
ORDER BY i.sequence, i.id`)
|
||||||
: db.prepare(`${pictureSelect} WHERE p.route_id = ? AND p.id = ? AND r.status = 'active' ORDER BY i.sequence, i.id`).all(routeId, filterPoiId);
|
.all(routeId, poiId)
|
||||||
return rows.map(pictureFromRow);
|
.map(pictureFromRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPicture(db, routeId, pictureId) {
|
function pictureRow(db, routeId, poiId, pictureId) {
|
||||||
activeRoute(db, routeId);
|
poiOnRoute(db, routeId, poiId);
|
||||||
const row = db.prepare(`${pictureSelect} WHERE p.route_id = ? AND i.id = ? AND r.status = 'active'`).get(routeId, pictureId);
|
const row = db.prepare(`${pictureSelect}
|
||||||
if (!row) throw new HttpError(404, 'Bild auf dieser Strecke nicht gefunden.');
|
WHERE p.route_id = ? AND p.id = ? AND i.id = ? AND r.status = 'active'`)
|
||||||
return pictureFromRow(row);
|
.get(routeId, poiId, pictureId);
|
||||||
|
if (!row) throw new HttpError(404, 'Bild für diesen POI nicht gefunden.');
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPictureFile(db, routeId, pictureId) {
|
export function getPicture(db, routeId, poiId, pictureId) {
|
||||||
activeRoute(db, routeId);
|
return pictureFromRow(pictureRow(db, routeId, poiId, pictureId));
|
||||||
const row = db.prepare(`${pictureSelect} WHERE p.route_id = ? AND i.id = ? AND r.status = 'active'`).get(routeId, pictureId);
|
}
|
||||||
if (!row) throw new HttpError(404, 'Bild auf dieser Strecke nicht gefunden.');
|
|
||||||
|
export function getPictureFile(db, routeId, poiId, pictureId) {
|
||||||
|
const row = pictureRow(db, routeId, poiId, pictureId);
|
||||||
return {
|
return {
|
||||||
absolutePath: resolveStoredFile(row.path),
|
absolutePath: resolveStoredFile(row.path),
|
||||||
filename: path.basename(row.path)
|
filename: path.basename(row.path)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPicture(db, routeId, fields, file) {
|
export function createPicture(db, routeId, poiId, fields, file) {
|
||||||
let relativePath;
|
let relativePath;
|
||||||
try {
|
try {
|
||||||
activeRoute(db, routeId);
|
poiOnRoute(db, routeId, poiId);
|
||||||
validateUpload(file, 'picture');
|
validateUpload(file, 'picture');
|
||||||
|
|
||||||
const poiId = requirePositiveInteger(fields.poiId, 'poiId');
|
|
||||||
poiOnRoute(db, routeId, poiId);
|
|
||||||
const sequence = optionalSequence(fields.sequence,
|
const sequence = optionalSequence(fields.sequence,
|
||||||
db.prepare('SELECT COALESCE(MAX(sequence), -1) + 1 AS value FROM poi_images WHERE poi_id = ?').get(poiId).value);
|
db.prepare('SELECT COALESCE(MAX(sequence), -1) + 1 AS value FROM poi_images WHERE poi_id = ?').get(poiId).value);
|
||||||
const caption = String(fields.caption ?? '');
|
const caption = String(fields.caption ?? '');
|
||||||
@ -140,7 +128,7 @@ export function createPicture(db, routeId, fields, file) {
|
|||||||
relativePath = moveUploadedFile(file, path.join(root, 'images', uniqueFilename(file.originalname)));
|
relativePath = moveUploadedFile(file, path.join(root, 'images', uniqueFilename(file.originalname)));
|
||||||
const result = db.prepare('INSERT INTO poi_images (poi_id, path, caption, sequence) VALUES (?, ?, ?, ?)')
|
const result = db.prepare('INSERT INTO poi_images (poi_id, path, caption, sequence) VALUES (?, ?, ?, ?)')
|
||||||
.run(poiId, relativePath, caption, sequence);
|
.run(poiId, relativePath, caption, sequence);
|
||||||
return getPicture(db, routeId, Number(result.lastInsertRowid));
|
return getPicture(db, routeId, poiId, Number(result.lastInsertRowid));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (relativePath) removeStoredFile(relativePath);
|
if (relativePath) removeStoredFile(relativePath);
|
||||||
else removeUpload(file);
|
else removeUpload(file);
|
||||||
@ -148,17 +136,13 @@ export function createPicture(db, routeId, fields, file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePicture(db, routeId, pictureId, fields, file) {
|
export function updatePicture(db, routeId, poiId, pictureId, fields, file) {
|
||||||
let newPath;
|
let newPath;
|
||||||
let oldPath;
|
let oldPath;
|
||||||
try {
|
try {
|
||||||
const existing = getPicture(db, routeId, pictureId);
|
const existing = getPicture(db, routeId, poiId, pictureId);
|
||||||
const row = db.prepare(`${pictureSelect} WHERE p.route_id = ? AND i.id = ? AND r.status = 'active'`).get(routeId, pictureId);
|
const row = pictureRow(db, routeId, poiId, pictureId);
|
||||||
oldPath = row.path;
|
oldPath = row.path;
|
||||||
const poiId = fields.poiId == null || fields.poiId === ''
|
|
||||||
? existing.poiId
|
|
||||||
: requirePositiveInteger(fields.poiId, 'poiId');
|
|
||||||
poiOnRoute(db, routeId, poiId);
|
|
||||||
const caption = fields.caption == null ? existing.caption : String(fields.caption);
|
const caption = fields.caption == null ? existing.caption : String(fields.caption);
|
||||||
const sequence = optionalSequence(fields.sequence, existing.sequence);
|
const sequence = optionalSequence(fields.sequence, existing.sequence);
|
||||||
|
|
||||||
@ -169,11 +153,11 @@ export function updatePicture(db, routeId, pictureId, fields, file) {
|
|||||||
newPath = moveUploadedFile(file, path.join(root, 'images', uniqueFilename(file.originalname)));
|
newPath = moveUploadedFile(file, path.join(root, 'images', uniqueFilename(file.originalname)));
|
||||||
}
|
}
|
||||||
|
|
||||||
db.prepare('UPDATE poi_images SET poi_id = ?, path = ?, caption = ?, sequence = ? WHERE id = ?')
|
db.prepare('UPDATE poi_images SET path = ?, caption = ?, sequence = ? WHERE id = ? AND poi_id = ?')
|
||||||
.run(poiId, newPath, caption, sequence, pictureId);
|
.run(newPath, caption, sequence, pictureId, poiId);
|
||||||
|
|
||||||
if (file && newPath !== oldPath) removeStoredFile(oldPath);
|
if (file && newPath !== oldPath) removeStoredFile(oldPath);
|
||||||
return getPicture(db, routeId, pictureId);
|
return getPicture(db, routeId, poiId, pictureId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (file) {
|
if (file) {
|
||||||
if (newPath && newPath !== oldPath) removeStoredFile(newPath);
|
if (newPath && newPath !== oldPath) removeStoredFile(newPath);
|
||||||
@ -183,56 +167,36 @@ export function updatePicture(db, routeId, pictureId, fields, file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deletePicture(db, routeId, pictureId) {
|
export function deletePicture(db, routeId, poiId, pictureId) {
|
||||||
const picture = getPicture(db, routeId, pictureId);
|
const picture = getPicture(db, routeId, poiId, pictureId);
|
||||||
const row = db.prepare(`${pictureSelect} WHERE p.route_id = ? AND i.id = ? AND r.status = 'active'`).get(routeId, pictureId);
|
const row = pictureRow(db, routeId, poiId, pictureId);
|
||||||
db.prepare('DELETE FROM poi_images WHERE id = ?').run(pictureId);
|
db.prepare('DELETE FROM poi_images WHERE id = ? AND poi_id = ?').run(pictureId, poiId);
|
||||||
removeStoredFile(row.path);
|
removeStoredFile(row.path);
|
||||||
return { id: picture.id, routeId: picture.routeId, poiId: picture.poiId, deleted: true };
|
return { id: picture.id, routeId: picture.routeId, poiId: picture.poiId, deleted: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
function audioFromRow(row) {
|
function audioFromPoi(poi) {
|
||||||
return {
|
return {
|
||||||
routeId: row.route_id,
|
routeId: poi.route_id,
|
||||||
poiId: row.poi_id,
|
poiId: poi.id,
|
||||||
poiTitle: row.poi_title,
|
poiTitle: poi.title,
|
||||||
url: audioUrl(row.route_id, row.poi_id),
|
url: audioUrl(poi.route_id, poi.id),
|
||||||
updatedAt: row.updated_at
|
updatedAt: poi.updated_at
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioSelect = `
|
|
||||||
SELECT p.id AS poi_id, p.route_id, p.title AS poi_title,
|
|
||||||
p.audio_path, p.updated_at
|
|
||||||
FROM pois p
|
|
||||||
JOIN routes r ON r.id = p.route_id
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function listAudio(db, routeId, { poiId } = {}) {
|
|
||||||
activeRoute(db, routeId);
|
|
||||||
const filterPoiId = poiId == null || poiId === '' ? null : requirePositiveInteger(poiId, 'poiId');
|
|
||||||
const rows = filterPoiId == null
|
|
||||||
? db.prepare(`${audioSelect} WHERE p.route_id = ? AND p.audio_path IS NOT NULL AND r.status = 'active' ORDER BY p.sequence, p.id`).all(routeId)
|
|
||||||
: db.prepare(`${audioSelect} WHERE p.route_id = ? AND p.id = ? AND p.audio_path IS NOT NULL AND r.status = 'active'`).all(routeId, filterPoiId);
|
|
||||||
return rows.map(audioFromRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAudio(db, routeId, poiId) {
|
export function getAudio(db, routeId, poiId) {
|
||||||
activeRoute(db, routeId);
|
const poi = poiOnRoute(db, routeId, poiId);
|
||||||
const row = db.prepare(`${audioSelect} WHERE p.route_id = ? AND p.id = ? AND p.audio_path IS NOT NULL AND r.status = 'active'`)
|
if (!poi.audio_path) throw new HttpError(404, 'Audiodatei für diesen POI nicht gefunden.');
|
||||||
.get(routeId, poiId);
|
return audioFromPoi(poi);
|
||||||
if (!row) throw new HttpError(404, 'Audiodatei für diesen POI nicht gefunden.');
|
|
||||||
return audioFromRow(row);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAudioFile(db, routeId, poiId) {
|
export function getAudioFile(db, routeId, poiId) {
|
||||||
activeRoute(db, routeId);
|
const poi = poiOnRoute(db, routeId, poiId);
|
||||||
const row = db.prepare(`${audioSelect} WHERE p.route_id = ? AND p.id = ? AND p.audio_path IS NOT NULL AND r.status = 'active'`)
|
if (!poi.audio_path) throw new HttpError(404, 'Audiodatei für diesen POI nicht gefunden.');
|
||||||
.get(routeId, poiId);
|
|
||||||
if (!row) throw new HttpError(404, 'Audiodatei für diesen POI nicht gefunden.');
|
|
||||||
return {
|
return {
|
||||||
absolutePath: resolveStoredFile(row.audio_path),
|
absolutePath: resolveStoredFile(poi.audio_path),
|
||||||
filename: path.basename(row.audio_path)
|
filename: path.basename(poi.audio_path)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,7 +204,6 @@ function storeAudio(db, routeId, poiId, file, { requireAbsent, requireExisting }
|
|||||||
let relativePath;
|
let relativePath;
|
||||||
let oldPath;
|
let oldPath;
|
||||||
try {
|
try {
|
||||||
activeRoute(db, routeId);
|
|
||||||
validateUpload(file, 'audio');
|
validateUpload(file, 'audio');
|
||||||
const poi = poiOnRoute(db, routeId, poiId);
|
const poi = poiOnRoute(db, routeId, poiId);
|
||||||
oldPath = poi.audio_path;
|
oldPath = poi.audio_path;
|
||||||
@ -254,8 +217,8 @@ function storeAudio(db, routeId, poiId, file, { requireAbsent, requireExisting }
|
|||||||
|
|
||||||
const root = ensureRouteDirectories(routeId);
|
const root = ensureRouteDirectories(routeId);
|
||||||
relativePath = moveUploadedFile(file, path.join(root, 'audio', uniqueFilename(file.originalname)));
|
relativePath = moveUploadedFile(file, path.join(root, 'audio', uniqueFilename(file.originalname)));
|
||||||
db.prepare('UPDATE pois SET audio_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
db.prepare('UPDATE pois SET audio_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND route_id = ?')
|
||||||
.run(relativePath, poiId);
|
.run(relativePath, poiId, routeId);
|
||||||
|
|
||||||
if (oldPath && oldPath !== relativePath) removeStoredFile(oldPath);
|
if (oldPath && oldPath !== relativePath) removeStoredFile(oldPath);
|
||||||
return getAudio(db, routeId, poiId);
|
return getAudio(db, routeId, poiId);
|
||||||
@ -266,8 +229,7 @@ function storeAudio(db, routeId, poiId, file, { requireAbsent, requireExisting }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAudio(db, routeId, fields, file) {
|
export function createAudio(db, routeId, poiId, file) {
|
||||||
const poiId = requirePositiveInteger(fields.poiId, 'poiId');
|
|
||||||
return storeAudio(db, routeId, poiId, file, { requireAbsent: true, requireExisting: false });
|
return storeAudio(db, routeId, poiId, file, { requireAbsent: true, requireExisting: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +240,8 @@ export function updateAudio(db, routeId, poiId, file) {
|
|||||||
export function deleteAudio(db, routeId, poiId) {
|
export function deleteAudio(db, routeId, poiId) {
|
||||||
const audio = getAudio(db, routeId, poiId);
|
const audio = getAudio(db, routeId, poiId);
|
||||||
const poi = poiOnRoute(db, routeId, poiId);
|
const poi = poiOnRoute(db, routeId, poiId);
|
||||||
db.prepare('UPDATE pois SET audio_path = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(poiId);
|
db.prepare('UPDATE pois SET audio_path = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND route_id = ?')
|
||||||
|
.run(poiId, routeId);
|
||||||
removeStoredFile(poi.audio_path);
|
removeStoredFile(poi.audio_path);
|
||||||
return { routeId: audio.routeId, poiId: audio.poiId, deleted: true };
|
return { routeId: audio.routeId, poiId: audio.poiId, deleted: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import {
|
|||||||
|
|
||||||
const slugify = value => value.toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80) || `route-${Date.now()}`;
|
const slugify = value => value.toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80) || `route-${Date.now()}`;
|
||||||
const routeGpxUrl = routeId => `/api/routes/${routeId}/gpx`;
|
const routeGpxUrl = routeId => `/api/routes/${routeId}/gpx`;
|
||||||
const pictureUrl = (routeId, pictureId) => `/api/routes/${routeId}/pictures/${pictureId}`;
|
const pictureUrl = (routeId, poiId, pictureId) => `/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`;
|
||||||
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/audio/${poiId}`;
|
const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/pois/${poiId}/audio`;
|
||||||
function rowToRoute(row, includeDeleted = false) {
|
function rowToRoute(row, includeDeleted = false) {
|
||||||
if (!row || (!includeDeleted && row.status !== 'active')) return null;
|
if (!row || (!includeDeleted && row.status !== 'active')) return null;
|
||||||
return {
|
return {
|
||||||
@ -146,7 +146,7 @@ export function listPois(db, routeId) {
|
|||||||
id: image.id,
|
id: image.id,
|
||||||
caption: image.caption,
|
caption: image.caption,
|
||||||
sequence: image.sequence,
|
sequence: image.sequence,
|
||||||
url: pictureUrl(poi.route_id, image.id)
|
url: pictureUrl(poi.route_id, poi.id, image.id)
|
||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,16 +26,15 @@ test('complete REST API documentation is kept outside the README', async () => {
|
|||||||
'POST /api/routes/:id/append',
|
'POST /api/routes/:id/append',
|
||||||
'POST /api/routes/:id/pois',
|
'POST /api/routes/:id/pois',
|
||||||
'PUT /api/pois/:id',
|
'PUT /api/pois/:id',
|
||||||
'GET /api/routes/:id/pictures',
|
'GET /api/routes/:routeId/pois/:poiId/pictures',
|
||||||
'GET /api/routes/:id/pictures/:pictureId',
|
'GET /api/routes/:routeId/pois/:poiId/pictures/:pictureId',
|
||||||
'POST /api/routes/:id/pictures',
|
'POST /api/routes/:routeId/pois/:poiId/pictures',
|
||||||
'PUT /api/routes/:id/pictures/:pictureId',
|
'PUT /api/routes/:routeId/pois/:poiId/pictures/:pictureId',
|
||||||
'DELETE /api/routes/:id/pictures/:pictureId',
|
'DELETE /api/routes/:routeId/pois/:poiId/pictures/:pictureId',
|
||||||
'GET /api/routes/:id/audio',
|
'GET /api/routes/:routeId/pois/:poiId/audio',
|
||||||
'GET /api/routes/:id/audio/:poiId',
|
'POST /api/routes/:routeId/pois/:poiId/audio',
|
||||||
'POST /api/routes/:id/audio',
|
'PUT /api/routes/:routeId/pois/:poiId/audio',
|
||||||
'PUT /api/routes/:id/audio/:poiId',
|
'DELETE /api/routes/:routeId/pois/:poiId/audio',
|
||||||
'DELETE /api/routes/:id/audio/:poiId',
|
|
||||||
'DELETE /api/routes/:id',
|
'DELETE /api/routes/:id',
|
||||||
'POST /api/routes/:id/restore'
|
'POST /api/routes/:id/restore'
|
||||||
]) {
|
]) {
|
||||||
@ -51,21 +50,24 @@ test('complete REST API documentation is kept outside the README', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POI media uploads use dedicated single-resource endpoints', async () => {
|
test('POI media uploads use POI-scoped single-resource endpoints', async () => {
|
||||||
const router = await read('src/routes/api.js');
|
const router = await read('src/routes/api.js');
|
||||||
const routesService = await read('src/services/routes-service.js');
|
const routesService = await read('src/services/routes-service.js');
|
||||||
const mediaService = await read('src/services/media-service.js');
|
const mediaService = await read('src/services/media-service.js');
|
||||||
|
|
||||||
assert.match(router, /post\('\/routes\/:id\/pictures', upload\.single\('picture'\)/);
|
assert.match(router, /post\('\/routes\/:routeId\/pois\/:poiId\/pictures', upload\.single\('picture'\)/);
|
||||||
assert.match(router, /put\('\/routes\/:id\/pictures\/:pictureId', upload\.single\('picture'\)/);
|
assert.match(router, /put\('\/routes\/:routeId\/pois\/:poiId\/pictures\/:pictureId', upload\.single\('picture'\)/);
|
||||||
assert.match(router, /delete\('\/routes\/:id\/pictures\/:pictureId'/);
|
assert.match(router, /delete\('\/routes\/:routeId\/pois\/:poiId\/pictures\/:pictureId'/);
|
||||||
assert.match(router, /post\('\/routes\/:id\/audio', upload\.single\('audio'\)/);
|
assert.match(router, /get\('\/routes\/:routeId\/pois\/:poiId\/audio'/);
|
||||||
assert.match(router, /put\('\/routes\/:id\/audio\/:poiId', upload\.single\('audio'\)/);
|
assert.match(router, /post\('\/routes\/:routeId\/pois\/:poiId\/audio', upload\.single\('audio'\)/);
|
||||||
assert.match(router, /delete\('\/routes\/:id\/audio\/:poiId'/);
|
assert.match(router, /put\('\/routes\/:routeId\/pois\/:poiId\/audio', upload\.single\('audio'\)/);
|
||||||
assert.match(router, /post\('\/routes\/:id\/pois', upload\.none\(\)/);
|
assert.match(router, /delete\('\/routes\/:routeId\/pois\/:poiId\/audio'/);
|
||||||
|
assert.doesNotMatch(router, /\/routes\/:id\/(?:pictures|audio)/);
|
||||||
|
assert.match(router, /post\('\/routes\/:id\/pois', upload\.none\(\),/);
|
||||||
assert.doesNotMatch(routesService, /files\.images|files\.audio/);
|
assert.doesNotMatch(routesService, /files\.images|files\.audio/);
|
||||||
assert.match(mediaService, /caption/);
|
assert.match(mediaService, /caption/);
|
||||||
assert.match(mediaService, /removeStoredFile/);
|
assert.match(mediaService, /removeStoredFile/);
|
||||||
|
assert.doesNotMatch(mediaService, /fields\.poiId/);
|
||||||
assert.match(router, /get\('\/routes\/:id\/gpx'/);
|
assert.match(router, /get\('\/routes\/:id\/gpx'/);
|
||||||
assert.match(router, /req\.query\.metadata === 'true'/);
|
assert.match(router, /req\.query\.metadata === 'true'/);
|
||||||
assert.doesNotMatch(router, /['"`]\/media\//);
|
assert.doesNotMatch(router, /['"`]\/media\//);
|
||||||
|
|||||||
@ -102,32 +102,50 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
|
|||||||
})
|
})
|
||||||
}, 201);
|
}, 201);
|
||||||
|
|
||||||
|
const { body: otherPoi } = await requestJson(`${baseUrl}/api/routes/${route.id}/pois`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: 'Andere Teststation',
|
||||||
|
lat: route.start.lat + 0.0001,
|
||||||
|
lon: route.start.lon + 0.0001,
|
||||||
|
triggerRadiusM: 40,
|
||||||
|
sequence: 1
|
||||||
|
})
|
||||||
|
}, 201);
|
||||||
|
|
||||||
const pictureForm = new FormData();
|
const pictureForm = new FormData();
|
||||||
pictureForm.append('poiId', String(poi.id));
|
pictureForm.append('poiId', String(otherPoi.id));
|
||||||
pictureForm.append('caption', 'Erste Bildbeschreibung');
|
pictureForm.append('caption', 'Erste Bildbeschreibung');
|
||||||
pictureForm.append('sequence', '0');
|
pictureForm.append('sequence', '0');
|
||||||
pictureForm.append('picture', new Blob(['picture-one'], { type: 'image/png' }), 'bild.png');
|
pictureForm.append('picture', new Blob(['picture-one'], { type: 'image/png' }), 'bild.png');
|
||||||
const { response: pictureResponse, body: picture } = await requestJson(
|
const { response: pictureResponse, body: picture } = await requestJson(
|
||||||
`${baseUrl}/api/routes/${route.id}/pictures`,
|
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures`,
|
||||||
{ method: 'POST', body: pictureForm },
|
{ method: 'POST', body: pictureForm },
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
assert.equal(pictureResponse.headers.get('location'), `/api/routes/${route.id}/pictures/${picture.id}`);
|
assert.equal(pictureResponse.headers.get('location'), `/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`);
|
||||||
assert.equal(picture.poiId, poi.id);
|
assert.equal(picture.poiId, poi.id);
|
||||||
assert.equal(picture.caption, 'Erste Bildbeschreibung');
|
assert.equal(picture.caption, 'Erste Bildbeschreibung');
|
||||||
assert.equal(picture.url, `/api/routes/${route.id}/pictures/${picture.id}`);
|
assert.equal(picture.url, `/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`);
|
||||||
const { response: pictureFileResponse, body: pictureFile } = await requestBuffer(`${baseUrl}${picture.url}`);
|
const { response: pictureFileResponse, body: pictureFile } = await requestBuffer(`${baseUrl}${picture.url}`);
|
||||||
assert.match(pictureFileResponse.headers.get('content-type') || '', /image\/png/);
|
assert.match(pictureFileResponse.headers.get('content-type') || '', /image\/png/);
|
||||||
assert.equal(pictureFile.toString(), 'picture-one');
|
assert.equal(pictureFile.toString(), 'picture-one');
|
||||||
const { body: pictureMetadata } = await requestJson(`${baseUrl}${picture.url}?metadata=true`);
|
const { body: pictureMetadata } = await requestJson(`${baseUrl}${picture.url}?metadata=true`);
|
||||||
assert.equal(pictureMetadata.caption, 'Erste Bildbeschreibung');
|
assert.equal(pictureMetadata.caption, 'Erste Bildbeschreibung');
|
||||||
|
assert.equal(pictureMetadata.poiId, poi.id);
|
||||||
|
await requestJson(
|
||||||
|
`${baseUrl}/api/routes/${route.id}/pois/${otherPoi.id}/pictures/${picture.id}?metadata=true`,
|
||||||
|
{},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
|
||||||
const { body: pictureList } = await requestJson(`${baseUrl}/api/routes/${route.id}/pictures?poiId=${poi.id}`);
|
const { body: pictureList } = await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures`);
|
||||||
assert.equal(pictureList.pictures.length, 1);
|
assert.equal(pictureList.pictures.length, 1);
|
||||||
assert.equal(pictureList.pictures[0].id, picture.id);
|
assert.equal(pictureList.pictures[0].id, picture.id);
|
||||||
|
|
||||||
const { body: updatedPicture } = await requestJson(
|
const { body: updatedPicture } = await requestJson(
|
||||||
`${baseUrl}/api/routes/${route.id}/pictures/${picture.id}`,
|
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@ -138,16 +156,15 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
|
|||||||
assert.equal(updatedPicture.sequence, 2);
|
assert.equal(updatedPicture.sequence, 2);
|
||||||
|
|
||||||
const audioForm = new FormData();
|
const audioForm = new FormData();
|
||||||
audioForm.append('poiId', String(poi.id));
|
|
||||||
audioForm.append('audio', new Blob(['audio-one'], { type: 'audio/mpeg' }), 'ansage.mp3');
|
audioForm.append('audio', new Blob(['audio-one'], { type: 'audio/mpeg' }), 'ansage.mp3');
|
||||||
const { response: audioResponse, body: audio } = await requestJson(
|
const { response: audioResponse, body: audio } = await requestJson(
|
||||||
`${baseUrl}/api/routes/${route.id}/audio`,
|
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`,
|
||||||
{ method: 'POST', body: audioForm },
|
{ method: 'POST', body: audioForm },
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
assert.equal(audioResponse.headers.get('location'), `/api/routes/${route.id}/audio/${poi.id}`);
|
assert.equal(audioResponse.headers.get('location'), `/api/routes/${route.id}/pois/${poi.id}/audio`);
|
||||||
assert.equal(audio.poiId, poi.id);
|
assert.equal(audio.poiId, poi.id);
|
||||||
assert.equal(audio.url, `/api/routes/${route.id}/audio/${poi.id}`);
|
assert.equal(audio.url, `/api/routes/${route.id}/pois/${poi.id}/audio`);
|
||||||
const { response: audioFileResponse, body: audioFile } = await requestBuffer(`${baseUrl}${audio.url}`);
|
const { response: audioFileResponse, body: audioFile } = await requestBuffer(`${baseUrl}${audio.url}`);
|
||||||
assert.match(audioFileResponse.headers.get('content-type') || '', /audio\/mpeg/);
|
assert.match(audioFileResponse.headers.get('content-type') || '', /audio\/mpeg/);
|
||||||
assert.equal(audioFile.toString(), 'audio-one');
|
assert.equal(audioFile.toString(), 'audio-one');
|
||||||
@ -155,9 +172,8 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
|
|||||||
assert.equal(audioMetadata.poiId, poi.id);
|
assert.equal(audioMetadata.poiId, poi.id);
|
||||||
|
|
||||||
const duplicateAudio = new FormData();
|
const duplicateAudio = new FormData();
|
||||||
duplicateAudio.append('poiId', String(poi.id));
|
|
||||||
duplicateAudio.append('audio', new Blob(['duplicate'], { type: 'audio/mpeg' }), 'doppelt.mp3');
|
duplicateAudio.append('audio', new Blob(['duplicate'], { type: 'audio/mpeg' }), 'doppelt.mp3');
|
||||||
await requestJson(`${baseUrl}/api/routes/${route.id}/audio`, {
|
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: duplicateAudio
|
body: duplicateAudio
|
||||||
}, 409);
|
}, 409);
|
||||||
@ -165,10 +181,10 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
|
|||||||
const replacementAudio = new FormData();
|
const replacementAudio = new FormData();
|
||||||
replacementAudio.append('audio', new Blob(['audio-two'], { type: 'audio/ogg' }), 'ansage-neu.ogg');
|
replacementAudio.append('audio', new Blob(['audio-two'], { type: 'audio/ogg' }), 'ansage-neu.ogg');
|
||||||
const { body: replacedAudio } = await requestJson(
|
const { body: replacedAudio } = await requestJson(
|
||||||
`${baseUrl}/api/routes/${route.id}/audio/${poi.id}`,
|
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`,
|
||||||
{ method: 'PUT', body: replacementAudio }
|
{ method: 'PUT', body: replacementAudio }
|
||||||
);
|
);
|
||||||
assert.equal(replacedAudio.url, `/api/routes/${route.id}/audio/${poi.id}`);
|
assert.equal(replacedAudio.url, `/api/routes/${route.id}/pois/${poi.id}/audio`);
|
||||||
const { response: replacedAudioResponse, body: replacedAudioFile } = await requestBuffer(`${baseUrl}${replacedAudio.url}`);
|
const { response: replacedAudioResponse, body: replacedAudioFile } = await requestBuffer(`${baseUrl}${replacedAudio.url}`);
|
||||||
assert.match(replacedAudioResponse.headers.get('content-type') || '', /audio\/ogg/);
|
assert.match(replacedAudioResponse.headers.get('content-type') || '', /audio\/ogg/);
|
||||||
assert.equal(replacedAudioFile.toString(), 'audio-two');
|
assert.equal(replacedAudioFile.toString(), 'audio-two');
|
||||||
@ -176,24 +192,28 @@ test('pictures and audio are managed as individual REST resources', { timeout: 3
|
|||||||
const { body: routeWithMedia } = await requestJson(`${baseUrl}/api/routes/${route.id}`);
|
const { body: routeWithMedia } = await requestJson(`${baseUrl}/api/routes/${route.id}`);
|
||||||
assert.equal(routeWithMedia.pois[0].images[0].caption, 'Aktualisierte Bildbeschreibung');
|
assert.equal(routeWithMedia.pois[0].images[0].caption, 'Aktualisierte Bildbeschreibung');
|
||||||
assert.equal(routeWithMedia.gpxUrl, `/api/routes/${route.id}/gpx`);
|
assert.equal(routeWithMedia.gpxUrl, `/api/routes/${route.id}/gpx`);
|
||||||
assert.equal(routeWithMedia.pois[0].audioUrl, `/api/routes/${route.id}/audio/${poi.id}`);
|
assert.equal(routeWithMedia.pois[0].audioUrl, `/api/routes/${route.id}/pois/${poi.id}/audio`);
|
||||||
assert.equal(routeWithMedia.pois[0].images[0].url, `/api/routes/${route.id}/pictures/${picture.id}`);
|
assert.equal(routeWithMedia.pois[0].images[0].url, `/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`);
|
||||||
assert.doesNotMatch(JSON.stringify(routeWithMedia), /\/media\//);
|
assert.doesNotMatch(JSON.stringify(routeWithMedia), /\/media\//);
|
||||||
const legacyMediaResponse = await fetch(`${baseUrl}/media/routes/${route.id}/route.gpx`);
|
const legacyMediaResponse = await fetch(`${baseUrl}/media/routes/${route.id}/route.gpx`);
|
||||||
assert.equal(legacyMediaResponse.status, 404);
|
assert.equal(legacyMediaResponse.status, 404);
|
||||||
|
const legacyPictureEndpoint = await fetch(`${baseUrl}/api/routes/${route.id}/pictures`);
|
||||||
|
assert.equal(legacyPictureEndpoint.status, 404);
|
||||||
|
const legacyAudioEndpoint = await fetch(`${baseUrl}/api/routes/${route.id}/audio`);
|
||||||
|
assert.equal(legacyAudioEndpoint.status, 404);
|
||||||
|
|
||||||
const { body: deletedAudio } = await requestJson(
|
const { body: deletedAudio } = await requestJson(
|
||||||
`${baseUrl}/api/routes/${route.id}/audio/${poi.id}`,
|
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
assert.equal(deletedAudio.deleted, true);
|
assert.equal(deletedAudio.deleted, true);
|
||||||
|
|
||||||
const { body: deletedPicture } = await requestJson(
|
const { body: deletedPicture } = await requestJson(
|
||||||
`${baseUrl}/api/routes/${route.id}/pictures/${picture.id}`,
|
`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
assert.equal(deletedPicture.deleted, true);
|
assert.equal(deletedPicture.deleted, true);
|
||||||
|
|
||||||
await requestJson(`${baseUrl}/api/routes/${route.id}/pictures/${picture.id}`, {}, 404);
|
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/pictures/${picture.id}`, {}, 404);
|
||||||
await requestJson(`${baseUrl}/api/routes/${route.id}/audio/${poi.id}`, {}, 404);
|
await requestJson(`${baseUrl}/api/routes/${route.id}/pois/${poi.id}/audio`, {}, 404);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user