Files
transcriber/openspec/changes/archive/2026-08-23-storage-without-pocketbase/specs/intake/spec.md
T
av c9b7765646 хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose
  под файловым замком, одна миграция начальной схемы вместо семи прежних
- транспорт переписан на net/http: свои слои, свой ограничитель частоты,
  отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли
- по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета
  читается справа налево, узнавание известного идёт читающим пулом
2026-08-23 08:06:04 +03:00

9.6 KiB
Raw Blame History

MODIFIED Requirements

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

Сервис SHALL принимать запись запросом POST /app/audiorecords с телом multipart/form-data и полем audio только от узнанного отправителя. Запрос от неузнанного MUST получать код 401, и по нему MUST не заводиться ни файл, ни аудиозапись. Принятая запись от узнанного отправителя MUST быть сохранена и получить заведённую под неё аудиозапись на рубеже uploaded.

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

Ответ MUST нести список заведённых записей и место под признак повторного файла у каждой, даже когда файл в запросе один. Форма согласована один раз и вперёд: приём, отдающий одну запись, пришлось бы переписывать вместе с приёмом нескольких файлов и с распознаванием повтора по содержимому, а экран загрузки — переделывать под вторую форму. Число файлов в запросе при этом остаётся прежним: меняется форма ответа, не число файлов.

Элемент списка MUST нести те же поля, что и карточка записи, плюс признак повторного файла полем duplicate: две формы одной вещи разошлись бы молча. Состав карточки нормирует capability archive.

Прежние имена полей ответа — job_id и status — MUST не употребляться: идентификатор записи зовётся id.

Значение рубежа в ответе MUST принадлежать перечню рубежей конвейера и MUST не перечисляться этой нормой порознь: рубеж объявлен одним дескриптором, и перечисленный здесь второй раз он разошёлся бы с ним молча. Рубеж называет достигнутое, а не предстоящее, и created в перечне отсутствует вовсе.

Запись сверх потолка размера MUST отвергаться до заведения файла и аудиозаписи, и код с телом такого отказа нормирует capability archive наравне с прочими ветвями.

Отказ неузнанному наступает раньше чтения тела: запись, за которую не заплатит узнанный отправитель, не должна попасть даже в память.

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

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

Владельцем принятой записи приём SHALL назначать узнанного предъявителя. Обязательность владельца при этом MUST держаться и схемой: колонка владельца пустого значения не принимает вовсе, и норму эту держит capability storage. Проверка в приёме от этого не лишняя — она отвечает отправителю понятным отказом до того, как запись попадёт в память, а схема отвечала бы отказом сохранения после укладки файла.

Отдельной ветви «узнан, а учётной записи нет» у приёма больше нет: узнавание заводит учётную запись само, а предъявителя с собственным токеном хранилища не существует — токенов сервис не выдаёт и не принимает. Ветвь ушла вместе со своим единственным случаем.

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

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

  • GIVEN источник метаданных читает запись и отдаёт её длительность
  • AND отправитель узнан
  • WHEN программа шлёт POST /app/audiorecords с полем audio
  • THEN ответ имеет код 201, а в теле лежит список из одного элемента
  • AND элемент несёт непустой id, поле state со значением uploaded и место под признак повторного файла
  • AND содержимое записи целиком лежит в каталоге данных одним файлом
  • AND владельцем заведённой аудиозаписи стоит узнанный предъявитель

Scenario: Пришедший не узнан

  • WHEN программа шлёт POST /app/audiorecords с полем audio неузнанной
  • THEN ответ имеет код 401
  • AND ни файла, ни аудиозаписи не заводится
  • AND тело ответа не несёт данных записи

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

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

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

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

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

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

Норма сужена: имя отправителя доходит теперь до самой аудиозаписи собственной колонкой — по нему человек узнаёт свою запись, — но не до имени файла и не до журнала. Что с ним делает приём, нормирует требование «Имя файла отправителя подписывает запись».

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

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

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

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

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

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

Scenario: Имя отправителя в имя файла не попало

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