хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
## 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** путь к этому файлу не содержит его тоже
|
||||
Reference in New Issue
Block a user