## MODIFIED Requirements ### Requirement: Файл отдаётся ссылкой Сервис SHALL отдавать файл записи ссылкой, которую строит хранилище по самой записи, **и только узнанному отправителю**. Поле файла MUST быть помечено защищённым: без этого ссылка открывает запись любому, кто её знает, и знание ссылки становится правом. Отданный файл MUST совпадать с принятым по длине. Одной пометки мало: защищённый файл судится **коротким токеном файла**, который узнанный отправитель берёт у хранилища, предъявив сессию, — и правилом просмотра коллекции. Правило MUST пускать всякого узнанного: незаданное означает «только владелец панели», и тогда файла не получит и вошедший. Сужения по владельцу здесь нет — его заводит отдельная задача. Отсюда порядок для потребителя: сессия → токен файла → ссылка с этим токеном. Браузер с одной лишь кукой файла не получит, и это свойство хранилища, а не недосмотр. Ссылка на несуществующую запись MUST отвечать отказом, а не пустым файлом. **Ссылка сама по себе и есть право пройти по ней**, и потому она MUST не попадать ни в журнал, ни в метку метрики, ни в ответ отправителю. Имя, под которым файл лёг в хранилище, из журнала выводимо быть не должно: журнал уезжает в собранные логи, откуда строку не убрать, и оттуда ссылка на чужую запись работала бы бессрочно. Защищённое поле сужает это право, но не отменяет запрета: право пройти теперь требует ещё и сессии, а строка журнала со ссылкой по-прежнему собирала бы половину ключа. Отсюда требование к отказам: сообщение об отказе хранилища MUST не выходить за пределы хранилища дословно. Отказ чтения и отказ укладки называют ключ файла целиком, а отказ выгрузки во внешнее хранилище — полный адрес объекта; и то и другое кончается в журнале и собирает ссылку не хуже успешного пути. Конвейер расшифровки этим не затронут: он читает файл из файловой системы хранилища, а не по ссылке. Что именно журнал приёма пишет ради прослеживаемости, нормирует capability `intake`. #### Scenario: Файл забирают по ссылке - **GIVEN** запись принята и её файл лежит в хранилище - **AND** забирающий предъявил сессию и взял по ней токен файла - **WHEN** ссылку на файл запрашивают с этим токеном - **THEN** приходит тот же файл, и его длина совпадает с длиной принятого #### Scenario: Без сессии файл не отдаётся - **GIVEN** запись принята и её файл лежит в хранилище - **WHEN** ссылку на файл запрашивают без сессии - **THEN** приходит отказ, а содержимого записи в ответе нет #### Scenario: Ссылка ведёт в никуда - **WHEN** запрашивают ссылку на запись, которой нет - **THEN** приходит отказ, а не пустой ответ #### Scenario: По журналу ссылку не собрать - **GIVEN** запись принята и прошла конвейер - **WHEN** читают журнал сервиса целиком - **THEN** имени, под которым файл лёг в хранилище, в нём нет #### Scenario: Отказ чтения файла не называет его ключ - **GIVEN** файл записи не читается из хранилища - **WHEN** шаг конвейера берётся за эту запись и отказывает - **THEN** отказ называет запись её идентификатором и не несёт имени файла #### Scenario: Конвейер читает файл без сессии - **GIVEN** запись принята и ждёт расшифровки - **WHEN** шаг конвейера берётся за неё - **THEN** файл читается из файловой системы хранилища и шаг проходит