хранилище, файлы записей и очередь переведены на встроенную PocketBase

- записи, метаданные и файлы съехались под один каталог данных; появилась
  панель владельца, а gin, goqu, goose и требование CGO ушли
- захват задачи стал одним запросом с RETURNING; заведены число попыток,
  состояние dead и нарастающая пауза вместо признака is_error
- имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с
  идентификатором записи оно собирало бы ссылку на скачивание
This commit is contained in:
av
2026-08-12 08:31:59 +03:00
parent 09cedc4e61
commit 01cc31d45f
55 changed files with 5238 additions and 1235 deletions
+37 -16
View File
@@ -10,7 +10,6 @@
Telegram делит с ним общий шаг заведения задачи, но требований на него нет:
требование, написанное без проверки, — предположение, а не норма. Первая задача,
которая трогает поведение приёма из Telegram, дописывает его сюда.
## Requirements
### Requirement: Приём записи по HTTP
@@ -25,13 +24,16 @@ MUST нести идентификатор задачи полем `job_id` и
Приём не судит о годности записи сам: расширение он берёт из имени файла, а
пригодность содержимого узнаёт у источника метаданных.
Куда именно ложится принятая запись, приёму не принадлежит: раскладку выбирает
хранилище, и нормирует её capability `storage`.
#### Scenario: Запись принята
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
- **WHEN** программа шлёт `POST /api/audio` с полем `audio`
- **THEN** ответ имеет код `201`, а в теле лежат непустой `job_id` и `status`
со значением `created`
- **AND** содержимое записи целиком лежит в каталоге хранения одним файлом
- **AND** содержимое записи целиком лежит в хранилище одним файлом
#### Scenario: Поля с записью нет
@@ -53,17 +55,28 @@ MUST нести идентификатор задачи полем `job_id` и
своим приёму не подконтрольно.
Расширения в присланном имени нет — сервис MUST подставить `.audio`, чтобы у
файла на диске расширение было всегда.
файла в хранилище расширение было всегда.
Требование переживает смену раскладки. Умолчание хранилища, строящее имя из
имени отправителя, MUST не применяться: имя отправителя в журнал не пишется по
инварианту приватности, а изъятие из него кончается расширением — хвостом после
последней точки.
#### Scenario: Расширение взято из имени отправителя
- **WHEN** программа шлёт запись с именем `test.mp3`
- **THEN** файл в каталоге хранения имеет расширение `.mp3`
- **THEN** имя файла в хранилище оканчивается на `.mp3`
#### Scenario: Имени без расширения назначено своё
- **WHEN** программа шлёт запись с именем `test` без расширения
- **THEN** файл в каталоге хранения имеет расширение `.audio`
- **THEN** имя файла в хранилище оканчивается на `.audio`
#### Scenario: Имя отправителя в хранилище не попало
- **WHEN** программа шлёт запись с именем `секретное-слово.mp3`
- **THEN** имя файла в хранилище не содержит `секретное-слово`
- **AND** путь к этому файлу не содержит его тоже
### Requirement: Отказ чтения метаданных
@@ -86,10 +99,10 @@ MUST нести идентификатор задачи полем `job_id` и
личной переписки наравне с текстом расшифровки; журнал уезжает в собранные логи,
откуда строку не убрать.
Расширение, взятое из этого имени, в журнале остаётся: оно стоит в собственном
имени файла на диске, и по нему прослеживается путь записи. Что именно попадает в
журнал ради прослеживаемости, нормирует требование ниже; наружу расширение
выходит только приведённым к известному виду — этому отдано отдельное требование.
Расширение, взятое из этого имени, в журнале остаётся собственным полем: по нему
прослеживается путь записи. Что именно попадает в журнал ради прослеживаемости,
нормирует требование ниже; наружу расширение выходит только приведённым к
известному виду — этому отдано отдельное требование.
Сценарии судят приём по HTTP, потому что имя, данное отправителем, доходит до
сервиса только оттуда: из Telegram приходит путь, выданный самим Telegram, а не
@@ -119,8 +132,10 @@ MUST нести идентификатор задачи полем `job_id` и
записи и её размер в байтах. По ним путь записи собирается отбором по журналу, и
удаление имени отправителя прослеживаемости не отнимает.
Расширение засчитывается присутствием собственного имени файла в хранилище:
отдельного поля под него приём не заводит.
Расширение засчитывается собственным полем журнальной строки. Имя, под которым
файл лёг в хранилище, приём MUST в журнал не писать: это имя — последняя часть
ссылки на скачивание, и записанное вместе с идентификатором записи оно собирает
ссылку целиком. Норму держит capability `storage`.
#### Scenario: Идентификатор, расширение и размер на месте
@@ -129,6 +144,12 @@ MUST нести идентификатор задачи полем `job_id` и
- **THEN** журнал приёма несёт идентификатор заведённого файла, расширение
принятой записи и её размер в байтах
#### Scenario: Имени файла в хранилище в журнале нет
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
- **WHEN** программа шлёт `POST /api/audio` с записью
- **THEN** имени, под которым файл лёг в хранилище, в журнале приёма нет
### Requirement: Метка метрики несёт только известное расширение
Сервис SHALL приводить расширение принятой записи к известному виду прежде, чем
@@ -147,12 +168,12 @@ MUST нести идентификатор задачи полем `job_id` и
Когда конвертацию нормируют своей capability, обязанность переезжает туда вместе
с ней.
Имя файла на диске это требование не трогает: там расширение остаётся тем, каким
пришло, — это уже нормировано требованием «Имя файла в хранилище».
Имя файла в хранилище это требование не трогает: там расширение остаётся тем,
каким пришло, — это уже нормировано требованием «Имя файла в хранилище».
Настоящий формат записи, попавшей в `other`, остаётся видимым в журнале: значение
`other` в метке означает «расширение не из перечня», а само оно стоит в поле
пути журнальной строки приёма и в поле формата строки конвертации.
`other` в метке означает «расширение не из перечня», а само оно стоит полем
журнальной строки приёма и полем формата строки конвертации.
#### Scenario: Незнакомое расширение наружу не выходит
@@ -160,7 +181,7 @@ MUST нести идентификатор задачи полем `job_id` и
- **WHEN** программа шлёт запись с именем, чей хвост после последней точки не
принадлежит перечню known-форматов
- **THEN** метка метрики принимает значение `other`
- **AND** файл в каталоге хранения сохраняет пришедшее расширение
- **AND** имя файла в хранилище сохраняет пришедшее расширение
#### Scenario: Известное расширение идёт как есть