---
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 не затронут.
