342 lines
18 KiB
Markdown
342 lines
18 KiB
Markdown
---
|
||
type: data-requirements
|
||
work_item_id: ET-011
|
||
title: "Требования к данным — ET-011: Скачивание трека из popup"
|
||
version: 1
|
||
status: approved
|
||
created_at: 2026-06-03
|
||
authors:
|
||
- "agent:architect"
|
||
---
|
||
|
||
# Требования к данным — ET-011
|
||
|
||
## 1. Резюме
|
||
|
||
ET-011 — **read-only data event**. Никаких изменений схемы БД,
|
||
никаких новых таблиц, индексов, миграций, localStorage-ключей. Эндпоинт
|
||
`GET /api/gps-tracks/{id}/download` собирает GPX-файл из существующих
|
||
полей таблицы `tracks` (ET-008 / ADR-005), переиспользует существующий
|
||
WKB-парсер (`mvt.py::_wkb_to_coords`), не пишет ни в одну таблицу.
|
||
|
||
**Меняется:**
|
||
- Содержимое `config/gps_sources.yaml` (одно optional-поле
|
||
`download_allowed: bool` per-source; см. ADR-015).
|
||
- Контракт API расширяется одним новым endpoint'ом (`/download`).
|
||
|
||
**Не меняется:**
|
||
- Schema таблиц `tracks`, `pipeline_runs`;
|
||
- Контракты существующих API `/api/gps-tracks`, `/tiles/...`, `/health`,
|
||
`/cache/clear`;
|
||
- localStorage ключи и значения клиента;
|
||
- Dedup-алгоритм (`compute_dedup_key`);
|
||
- ACTIVITY_TYPES enum;
|
||
- Маппинги `SOURCE_ATTRIBUTIONS`, `SOURCE_LABELS`.
|
||
|
||
## 2. Архитектурные границы данных
|
||
|
||
| Слой данных | Тип | Расположение | Изменения в ET-011 |
|
||
|---|---|---|---|
|
||
| OSM-vector (`trails`) | существующий | `/app/data/centralfederal.sqlite` | **нет** |
|
||
| Личные GPX треки (ET-006) | существующий | браузер (memory) | **нет** |
|
||
| Публичные GPS треки (ET-008) | существующий | `/app/data/gps_tracks.sqlite` | **read-only**: новый запрос на скачивание; никаких INSERT/UPDATE/DELETE |
|
||
| OSRM-граф | существующий | `/app/data/enduro.osrm.*` | **нет** |
|
||
| User UI state | существующий | `localStorage` | **нет** новых ключей |
|
||
| Скачанный GPX-файл | **новое (выход)** | downloads-папка браузера пользователя | формат GPX 1.1, см. §4 |
|
||
|
||
## 3. Серверные данные — `gps_tracks.sqlite`
|
||
|
||
### 3.1 Schema
|
||
|
||
**Без изменений vs ET-008.** См. `docs/work-items/ET-008/08-data-requirements.md`
|
||
§3.1, §3.5. Никаких ALTER TABLE / DROP COLUMN / CREATE INDEX.
|
||
|
||
### 3.2 Используемые поля в SELECT для /download
|
||
|
||
| Поле | Использование |
|
||
|---|---|
|
||
| `id` | Path-параметр запроса; PK lookup |
|
||
| `name` | `<metadata><name>` и `<trk><name>` в GPX; имя файла |
|
||
| `description` | `<metadata><desc>` (если не null) |
|
||
| `activity_type` | `<trk><type>` |
|
||
| `user` | `<metadata><author><name>` (если не null; для OSM по ADR-009) |
|
||
| `created_at` | `<metadata><time>` (если не null; ISO-8601 UTC) |
|
||
| `length_m` | информативно, в GPX не входит |
|
||
| `points_count` | проверка cap REQ-NF-02 (> 200000 → 413) |
|
||
| `geom` (WKB) | парсится через `_wkb_to_coords()` в `[(lon, lat), ...]`; каждая пара → один `<trkpt>` |
|
||
| `sources_json` | license-guard ADR-015; `<link>` элементы в `<metadata>` |
|
||
| `external_urls_json` | `<link href=…>` элементы; ответ 403 для CTA |
|
||
| `dedup_key`, `tags_json`, `inserted_at`, `updated_at`, `min_lon..max_lat` | не используется в /download |
|
||
|
||
### 3.3 SQL-запрос
|
||
|
||
```sql
|
||
SELECT id, name, description, activity_type, user, created_at,
|
||
length_m, points_count, geom, sources_json, external_urls_json
|
||
FROM tracks WHERE id = ?
|
||
```
|
||
|
||
Один параметр `?` — integer, валидируется FastAPI. Использует
|
||
автоматический PRIMARY KEY-индекс. Стоимость: ~1 ms.
|
||
|
||
### 3.4 Кэширование на стороне сервера
|
||
|
||
**Не вводим.** Mvt-кэш ET-008 — другой механизм (по `(z,x,y)`). Для
|
||
скачивания одиночного трека:
|
||
- Кэш-хит редкий (пользователь обычно качает один раз).
|
||
- Размер GPX до 20 МБ × N треков — раздуло бы LRU-кэш и заняло RAM.
|
||
- Производительность сборки и так в бюджете (REQ-NF-01 = 300 ms p95).
|
||
|
||
Клиентский кэш — через заголовок `Cache-Control: private, max-age=3600`
|
||
(см. ADR-014 §6). Браузер сам кэширует blob.
|
||
|
||
### 3.5 Изменения объёма БД
|
||
|
||
**Нет.** Эндпоинт read-only.
|
||
|
||
### 3.6 Backup retention
|
||
|
||
Без изменений (см. ET-008 §9).
|
||
|
||
## 4. Контракт GPX-файла (выходные данные)
|
||
|
||
### 4.1 Структура XML
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<gpx version="1.1"
|
||
creator="Enduro Trails"
|
||
xmlns="http://www.topografix.com/GPX/1/1"
|
||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
|
||
<metadata>
|
||
<name>{tracks.name | "Без названия"}</name>
|
||
<desc>{tracks.description}</desc> <!-- если не null -->
|
||
<author>
|
||
<name>{tracks.user}</name> <!-- если не null -->
|
||
</author>
|
||
<link href="{external_urls[0]}">
|
||
<text>Источник: {sources[0]}</text>
|
||
</link>
|
||
<!-- ... по одному <link> на каждый external_url -->
|
||
<time>{tracks.created_at | ISO-8601 UTC}</time> <!-- если не null -->
|
||
<copyright author="Enduro Trails"> <!-- если "osm" ∈ sources -->
|
||
<license>https://www.openstreetmap.org/copyright</license>
|
||
</copyright>
|
||
</metadata>
|
||
<trk>
|
||
<name>{tracks.name | "Без названия"}</name>
|
||
<type>{tracks.activity_type | "other"}</type>
|
||
<trkseg>
|
||
<trkpt lat="55.123456" lon="37.654321" />
|
||
<!-- ... по одному <trkpt> на каждую координату из geom -->
|
||
</trkseg>
|
||
</trk>
|
||
</gpx>
|
||
```
|
||
|
||
### 4.2 Соответствие схеме
|
||
|
||
Валидируется по `http://www.topografix.com/GPX/1/1/gpx.xsd` без
|
||
ошибок и warnings (REQ-NF-03, AC-5). Тестовая фикстура
|
||
`tests/fixtures/gpx-1.1/gpx.xsd` (snapshot схемы).
|
||
|
||
### 4.3 Размер и плотность
|
||
|
||
| Кол-во точек | Типичный размер | Время сборки (4-worker uvicorn) |
|
||
|---|---|---|
|
||
| 100 | ~ 15 КБ | < 5 мс |
|
||
| 1 000 | ~ 130 КБ | < 20 мс |
|
||
| 5 000 | ~ 650 КБ | < 50 мс |
|
||
| 50 000 | ~ 6.5 МБ | 80–150 мс |
|
||
| 200 000 (cap) | ~ 26 МБ | 400–500 мс |
|
||
| > 200 000 | — | **413 Payload Too Large** |
|
||
|
||
Округление координат `%.6f` — точность ≈ 0.11 м (более чем достаточно
|
||
для эндуро-навигации; экономит ~30% bytes vs Python-default float repr).
|
||
|
||
### 4.4 Кодировка
|
||
|
||
UTF-8 строго. `Content-Type: application/gpx+xml; charset=utf-8`.
|
||
ElementTree сам выдаёт UTF-8 при `tostring(root, encoding="utf-8",
|
||
xml_declaration=True)`.
|
||
|
||
### 4.5 Что НЕ попадает в GPX
|
||
|
||
| Поле | Причина |
|
||
|---|---|
|
||
| `<ele>` (высота) | Не хранится в БД (BRD A2 / ET-008 ограничение) |
|
||
| `<time>` в каждом `<trkpt>` | Не хранится в БД (BRD A2) |
|
||
| `<wpt>` (waypoints) | Не moнодим из треков |
|
||
| `<rte>` (роуты) | Не применимо для public GPS-tracks |
|
||
| `<extensions>` | Минимализм; кастомные расширения — отдельная фича |
|
||
| `tracks.dedup_key`, `tracks.length_m`, `tracks.points_count` | Внутренние метаданные, не часть GPX-стандарта |
|
||
| `tracks.tags_json` | В этой итерации не нужны; если потребуется — `<keywords>` в metadata |
|
||
|
||
## 5. Конфигурация — `gps_sources.yaml`
|
||
|
||
### 5.1 Новое поле `download_allowed`
|
||
|
||
| Поле | Тип | Default | Назначение |
|
||
|---|---|---|---|
|
||
| `download_allowed` | bool | `false` (если отсутствует — deny) | Управляет ответом 403 в `/download` эндпоинте |
|
||
|
||
Финальные значения для ET-011 (закрытие BRD Q-1):
|
||
|
||
| `source.id` | `download_allowed` | Юридическое основание |
|
||
|---|---|---|
|
||
| `osm` | `true` | ODbL разрешает реэкспорт с атрибуцией (ADR-009 + ADR-015 §«Решение D») |
|
||
| `enduro_russia` | `false` | Default-deny; ADR-010 ничего не говорит про реэкспорт |
|
||
| `wikiloc` | `false` | ToS Wikiloc запрещает массовый ре-экспорт (ADR-012) |
|
||
| `ttrails` | `false` | ADR-011 в `proposed`; не собирается и не отдаётся |
|
||
|
||
### 5.2 Влияние на pipeline
|
||
|
||
`gps-collector` **игнорирует** новое поле (pipeline-код не обращается к
|
||
`download_allowed`). Это redistribution-only флаг.
|
||
|
||
## 6. Контракт публичного API
|
||
|
||
### 6.1 `GET /api/gps-tracks/{track_id}/download` — **новый**
|
||
|
||
#### Параметры
|
||
|
||
| Параметр | Тип | Где | Обязательный | Default |
|
||
|---|---|---|---|---|
|
||
| `track_id` | int (ge=1) | path | да | — |
|
||
| `format` | str | query | нет | `"gpx"` (whitelist `{"gpx"}`) |
|
||
|
||
#### Ответы
|
||
|
||
| Статус | Body | Headers (ключевые) | Триггер |
|
||
|---|---|---|---|
|
||
| 200 | XML (GPX 1.1) | `Content-Type: application/gpx+xml; charset=utf-8`<br>`Content-Disposition: attachment; filename="…"; filename*=UTF-8''…`<br>`Cache-Control: private, max-age=3600`<br>`Content-Length: <bytes>` | happy path |
|
||
| 400 | `{"detail": "unsupported_format"}` | стандартные | `format` не в whitelist |
|
||
| 403 | `{"detail": "source_forbidden", "external_urls": [...]}` | стандартные | Ни один source трека не в `download_allowed` whitelist (ADR-015) |
|
||
| 404 | `{"detail": "track_not_found"}` | стандартные | Трек с указанным `id` отсутствует в БД |
|
||
| 413 | `{"detail": "track_too_large"}` | стандартные | `tracks.points_count > 200000` |
|
||
| 500 | `{"detail": "internal_error"}` | стандартные | необработанное исключение (db read fail, XML build fail) |
|
||
|
||
#### Кодирование имени файла
|
||
|
||
RFC 5987:
|
||
- `filename="<ascii_fallback>.gpx"` — ASCII-printable санитизированное
|
||
имя (см. ADR-014 §F).
|
||
- `filename*=UTF-8''<percent_encoded>.gpx` — UTF-8 имя через
|
||
`urllib.parse.quote(name, safe='', encoding='utf-8')`.
|
||
|
||
Пример (`name = "По грязи к Чёрному озеру"`):
|
||
```
|
||
Content-Disposition: attachment; filename="track-42.gpx"; filename*=UTF-8''%D0%9F%D0%BE%20%D0%B3%D1%80%D1%8F%D0%B7%D0%B8%20%D0%BA%20%D0%A7%D1%91%D1%80%D0%BD%D0%BE%D0%BC%D1%83%20%D0%BE%D0%B7%D0%B5%D1%80%D1%83.gpx
|
||
```
|
||
|
||
ASCII-fallback `track-42.gpx` используется только если у пользователя
|
||
браузер не понимает `filename*` (последние 10+ лет — не встречается).
|
||
|
||
### 6.2 Существующие эндпоинты — без изменений
|
||
|
||
`GET /api/gps-tracks`, `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt`,
|
||
`GET /api/gps-tracks/health`, `POST /api/gps-tracks/cache/clear` —
|
||
без изменений.
|
||
|
||
## 7. Клиентское хранилище
|
||
|
||
### 7.1 localStorage
|
||
|
||
**Без изменений.** Никаких новых ключей. Существующие ключи ET-008
|
||
(`gps-tracks-enabled`, `gps-tracks-activities`, `gps-tracks-sources`,
|
||
`gps-tracks-color-mode`) — без изменений.
|
||
|
||
### 7.2 Не-персистентное состояние
|
||
|
||
`window.gpsTracksLayer` — без изменений.
|
||
|
||
`SOURCE_ATTRIBUTIONS`, `SOURCE_LABELS` маппинги — без изменений.
|
||
|
||
## 8. Персональные данные (PII)
|
||
|
||
| Канал | PII | Обработка в ET-011 |
|
||
|---|---|---|
|
||
| `<author><name>` в скачанном GPX | возможно (OSM user-name) | попадает только для OSM (ADR-009 collect_user_field: true). Для EnduroRussia/Wikiloc/ttrails — null в БД, элемент опускается |
|
||
| `<metadata><desc>` | возможно (свободный текст автора) | только для OSM-источника при ANY-rule ADR-015 трек качается; для не-OSM — `<copyright>` не указывается, но `<desc>` может содержать merged-text. Это **сознательный** компромисс ADR-015 §B (см. R-3 в `10-tech-risks.md`) |
|
||
| `<link href=…>` external_urls | URL-ы могут указывать на профиль автора | сохранены как есть в `external_urls_json` (паттерн ET-008) |
|
||
| IP клиента в логах скачивания | стандартный uvicorn access-log | без изменений; ротация в Docker |
|
||
|
||
### 8.1 Право на удаление
|
||
|
||
Без изменений. Удаление записи из `tracks` (ET-008 §7.1) автоматически
|
||
делает её недоступной через `/download` (404).
|
||
|
||
### 8.2 GDPR / РФ ФЗ-152
|
||
|
||
Обрабатываются только публично выложенные данные с условием
|
||
`download_allowed: true`. ODbL OSM покрывает реэкспорт (ADR-009).
|
||
|
||
## 9. Атрибуция
|
||
|
||
В скачанном GPX:
|
||
- `<copyright>` с OSM-license URL — если `"osm" ∈ sources`.
|
||
- `<link>` для каждого `external_url` — атрибуция в виде ссылок,
|
||
кликабельная в любом GPX-просмотрщике (OsmAnd, Garmin BaseCamp, QGIS).
|
||
- `creator="Enduro Trails"` в корневом `<gpx>` — атрибуция нашего
|
||
сервиса.
|
||
|
||
В UI: без изменений (MapLibre Attribution control остаётся как в ET-008).
|
||
|
||
## 10. Backup и retention
|
||
|
||
**Не применимо** к ET-011. Эндпоинт read-only, не создаёт persistent-
|
||
артефактов.
|
||
|
||
## 11. Тестовые данные (фикстуры)
|
||
|
||
### 11.1 Новые фикстуры
|
||
|
||
| Файл | Содержимое | Использование |
|
||
|---|---|---|
|
||
| `tests/fixtures/gpx-1.1/gpx.xsd` | XSD-схема topografix 1.1 (~30 КБ), скачана один раз | UT-03, IT-07 (валидация выходного GPX) |
|
||
| `tests/fixtures/gps-tracks/sample-tracks-fixture.sql` | (опц.) набор INSERT для трёх кейсов: OSM-трек 5 точек, EnduroRussia-трек 50 точек, Wikiloc-трек 100 точек | IT-01..08 |
|
||
|
||
`gpx.xsd` коммитится один раз; не зависит от внешних сервисов в
|
||
runtime (только на момент UT-теста).
|
||
|
||
### 11.2 Юридический статус фикстур
|
||
|
||
`gpx.xsd` — открытый XML Schema от `topografix.com`, свободно
|
||
распространяемый (см. footer на topografix.com). Хранение в репо для
|
||
тестирования — стандартная практика.
|
||
|
||
Тестовые SQL-фикстуры с координатами — синтетические (рандомные),
|
||
не содержат реальных треков от публикаторов.
|
||
|
||
## 12. Контракты, которые нельзя ломать
|
||
|
||
1. **Schema `tracks`, `pipeline_runs`** — не меняются (read-only
|
||
эндпоинт).
|
||
2. **Структура GeoJSON и MVT** на других эндпоинтах — не меняется.
|
||
3. **GPX 1.1 формат выходного файла** — соответствует topografix XSD;
|
||
изменение структуры (например, добавление `<extensions>`) — breaking
|
||
change для пользователей, которые уже импортировали в свои навигаторы;
|
||
требует minor-bump в `creator="Enduro Trails"` или отдельной фичи.
|
||
4. **`download_allowed` поле в `gps_sources.yaml`** — optional, default
|
||
`false`; никогда не делать его required (поломает все существующие
|
||
конфиги). Pipeline не должен начать читать это поле в будущем —
|
||
разделение confidently distinct concerns.
|
||
5. **Ответ 403 schema** — `{"detail": "source_forbidden", "external_urls": [...]}`
|
||
— клиент использует `external_urls[0]` для CTA. Удаление поля
|
||
сломает UX.
|
||
|
||
## 13. Вывод
|
||
|
||
ET-011 — **read-only data event**:
|
||
|
||
- Не меняет схему БД, не добавляет миграции, не вводит новые таблицы;
|
||
- Использует существующие данные в `tracks` через один SELECT;
|
||
- Возвращает новый артефакт (GPX-файл) пользователю — не сохраняет на
|
||
сервер;
|
||
- Расширяет один конфиг-файл одним optional-полем;
|
||
- Поддерживает default-deny для лицензионной чистоты.
|
||
|
||
Юридически защищён через ADR-009 (OSM ODbL) + ADR-015 (default-deny
|
||
whitelist). Pipeline-collector не затронут.
|