Files
transcriber/openspec/changes/archive/2026-08-12-pocketbase-storage/specs/intake/spec.md
T
av 01cc31d45f хранилище, файлы записей и очередь переведены на встроенную PocketBase
- записи, метаданные и файлы съехались под один каталог данных; появилась
  панель владельца, а gin, goqu, goose и требование CGO ушли
- захват задачи стал одним запросом с RETURNING; заведены число попыток,
  состояние dead и нарастающая пауза вместо признака is_error
- имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с
  идентификатором записи оно собирало бы ссылку на скачивание
2026-08-12 08:31:59 +03:00

13 KiB
Raw Blame History

MODIFIED Requirements

Requirement: Приём записи по HTTP

Сервис SHALL принимать запись от внешней программы запросом POST /api/audio с телом multipart/form-data и полем audio. Принятая запись MUST быть сохранена и получить заведённую под неё задачу расшифровки в состоянии created; ответ MUST нести идентификатор задачи полем job_id и её состояние полем status.

Имена полей ответа нормативны: контракт HTTP API объявлен проектом необратимым, и переименование поля ломает внешнюю программу молча.

Приём не судит о годности записи сам: расширение он берёт из имени файла, а пригодность содержимого узнаёт у источника метаданных.

Куда именно ложится принятая запись, приёму не принадлежит: раскладку выбирает хранилище, и нормирует её capability storage.

Scenario: Запись принята

  • GIVEN источник метаданных читает запись и отдаёт её длительность
  • WHEN программа шлёт POST /api/audio с полем audio
  • THEN ответ имеет код 201, а в теле лежат непустой job_id и status со значением created
  • AND содержимое записи целиком лежит в хранилище одним файлом

Scenario: Поля с записью нет

  • WHEN программа шлёт POST /api/audio без поля audio
  • THEN ответ имеет код 400 и сообщение об отсутствии записи
  • AND ни файла, ни задачи не заводится

Scenario: Размеру записи приём не судья

  • GIVEN источник метаданных читает запись и отдаёт её длительность
  • WHEN программа шлёт запись нулевой длины
  • THEN ответ имеет код 201: собственного порога по размеру у приёма нет

Requirement: Имя файла в хранилище

Сервис SHALL сохранять принятую запись под собственным именем — идентификатором, к которому приписано расширение из имени файла отправителя. Имя, данное отправителем, MUST не попадать в хранилище: оно приходит извне и содержимым своим приёму не подконтрольно.

Расширения в присланном имени нет — сервис MUST подставить .audio, чтобы у файла в хранилище расширение было всегда.

Требование переживает смену раскладки. Умолчание хранилища, строящее имя из имени отправителя, MUST не применяться: имя отправителя в журнал не пишется по инварианту приватности, а изъятие из него кончается расширением — хвостом после последней точки.

Scenario: Расширение взято из имени отправителя

  • WHEN программа шлёт запись с именем test.mp3
  • THEN имя файла в хранилище оканчивается на .mp3

Scenario: Имени без расширения назначено своё

  • WHEN программа шлёт запись с именем test без расширения
  • THEN имя файла в хранилище оканчивается на .audio

Scenario: Имя отправителя в хранилище не попало

  • WHEN программа шлёт запись с именем секретное-слово.mp3
  • THEN имя файла в хранилище не содержит секретное-слово
  • AND путь к этому файлу не содержит его тоже

Requirement: Имя файла, данное отправителем, не попадает в журнал

Приём SHALL не писать имя файла, данное отправителем, ни в одну свою журнальную запись — ни на успешном пути, ни на пути отказа, где имя могло бы приехать текстом ошибки. Имя приходит извне вместе с записью и принадлежит содержимому личной переписки наравне с текстом расшифровки; журнал уезжает в собранные логи, откуда строку не убрать.

Расширение, взятое из этого имени, в журнале остаётся собственным полем: по нему прослеживается путь записи. Что именно попадает в журнал ради прослеживаемости, нормирует требование ниже; наружу расширение выходит только приведённым к известному виду — этому отдано отдельное требование.

Сценарии судят приём по HTTP, потому что имя, данное отправителем, доходит до сервиса только оттуда: из Telegram приходит путь, выданный самим Telegram, а не имя человека. Правка при этом ложится на общий шаг заведения задачи, через который идут оба входа, поэтому своей нормы приём из Telegram здесь не получает — её напишет задача, которая тронет его поведение.

Scenario: Имя записи не видно в журнале принятой записи

  • GIVEN источник метаданных читает запись и отдаёт её длительность
  • WHEN программа шлёт POST /api/audio с записью, чья основа имени несёт опознаваемую строку при обычном расширении .mp3
  • THEN ни одна журнальная запись приёма этой строки не содержит
  • AND расширение .mp3 в журнале допустимо

Scenario: Имя записи не видно в журнале при отказе приёма

  • GIVEN источник метаданных не может прочитать запись
  • WHEN программа шлёт POST /api/audio с записью, чья основа имени несёт опознаваемую строку
  • THEN ни одна журнальная запись приёма, включая запись об ошибке, этой строки не содержит

Requirement: Журнал приёма прослеживает запись

Приём SHALL писать в журнал идентификатор заведённого файла, расширение принятой записи и её размер в байтах. По ним путь записи собирается отбором по журналу, и удаление имени отправителя прослеживаемости не отнимает.

Расширение засчитывается собственным полем журнальной строки. Имя, под которым файл лёг в хранилище, приём MUST в журнал не писать: это имя — последняя часть ссылки на скачивание, и записанное вместе с идентификатором записи оно собирает ссылку целиком. Норму держит capability storage.

Scenario: Идентификатор, расширение и размер на месте

  • GIVEN источник метаданных читает запись и отдаёт её длительность
  • WHEN программа шлёт POST /api/audio с записью
  • THEN журнал приёма несёт идентификатор заведённого файла, расширение принятой записи и её размер в байтах

Scenario: Имени файла в хранилище в журнале нет

  • GIVEN источник метаданных читает запись и отдаёт её длительность
  • WHEN программа шлёт POST /api/audio с записью
  • THEN имени, под которым файл лёг в хранилище, в журнале приёма нет

Requirement: Метка метрики несёт только известное расширение

Сервис SHALL приводить расширение принятой записи к известному виду прежде, чем употребить его меткой метрики: расширение приводится к нижнему регистру и сверяется с закрытым перечнем; совпавшее идёт приведённым, всякое другое MUST заменяться единым значением other. Перечень — mp3, wav, ogg, oga, opus, flac, m4a, aac, wma, mp4, mkv, mov, avi, webm, плюс audio: последнее не формат, а собственное умолчание сервиса на случай имени без расширения, и различать его от чужого хвоста метка обязана.

Страница метрик отдаётся без проверки отправителя, поэтому метка — поверхность пошире журнала: её читает кто угодно. Тем же ограничением снимается и рост числа временных рядов, которым иначе распоряжается анонимный отправитель.

Требование намеренно шире приёма: под него подпадает и метка шага конвертации. Когда конвертацию нормируют своей capability, обязанность переезжает туда вместе с ней.

Имя файла в хранилище это требование не трогает: там расширение остаётся тем, каким пришло, — это уже нормировано требованием «Имя файла в хранилище».

Настоящий формат записи, попавшей в other, остаётся видимым в журнале: значение other в метке означает «расширение не из перечня», а само оно стоит полем журнальной строки приёма и полем формата строки конвертации.

Scenario: Незнакомое расширение наружу не выходит

  • GIVEN источник метаданных читает запись и отдаёт её длительность
  • WHEN программа шлёт запись с именем, чей хвост после последней точки не принадлежит перечню known-форматов
  • THEN метка метрики принимает значение other
  • AND имя файла в хранилище сохраняет пришедшее расширение

Scenario: Известное расширение идёт как есть

  • GIVEN источник метаданных читает запись и отдаёт её длительность
  • WHEN программа шлёт запись с именем sample.MP3
  • THEN метка метрики принимает значение mp3