хранилище, файлы записей и очередь переведены на встроенную PocketBase
- записи, метаданные и файлы съехались под один каталог данных; появилась панель владельца, а gin, goqu, goose и требование CGO ушли - захват задачи стал одним запросом с RETURNING; заведены число попыток, состояние dead и нарастающая пауза вместо признака is_error - имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с идентификатором записи оно собирало бы ссылку на скачивание
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# Ссылка на файл открыта знанием записи, а защищает её отсутствие имени в журнале
|
||||
|
||||
- **Дата:** 2026-08-12
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md](../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md),
|
||||
раздел «Поле файла не помечаем защищённым, но ссылка не уезжает в журнал»
|
||||
|
||||
## Решение
|
||||
|
||||
Поле файла в хранилище **не помечается защищённым**: ссылка
|
||||
`/api/files/<коллекция>/<запись>/<имя>` работает без токена, и право пройти по
|
||||
ней даёт знание самой ссылки. Взамен имя файла в хранилище **не пишется в журнал
|
||||
ни в каком виде** — ни на успешном пути, ни в тексте отказа.
|
||||
|
||||
## Почему
|
||||
|
||||
Очевидный подход к файлам, отдаваемым в интернет, — закрыть их: у хранилища для
|
||||
этого есть пометка «защищённое поле», и тогда файл отдаётся только по отдельному
|
||||
файловому токену. Мы от неё отказываемся, и цитата из источника называет причину:
|
||||
|
||||
> Защищённое поле требует отдельного файлового токена. Не помечаем: сегодня право
|
||||
> прочитать задачу даёт знание её идентификатора, и файл встаёт вровень с
|
||||
> `GET /api/status/:id`, а не ниже.
|
||||
|
||||
Отказ дешёв ровно до тех пор, пока ссылку неоткуда взять. Перевод хранилища это
|
||||
условие сломал, и вот чем:
|
||||
|
||||
> Изъятие выписано под путь на диске: `data/files/<uuid>.ogg` читателю журнала
|
||||
> бесполезен. После перевода имя файла в хранилище — это последняя часть ссылки
|
||||
> `/api/files/...`, по которой запись скачивает кто угодно; строка журнала стала
|
||||
> бы бессрочным ключом к чужому аудио.
|
||||
|
||||
Путь был построен ревью и прогнан: отказ чтения из хранилища нёс ключ файла
|
||||
целиком, строка уходила в журнал, а анонимный запрос по собранному адресу
|
||||
отвечал `200` с телом записи. Второй путь шёл через отказ выгрузки в Object
|
||||
Storage — тот несёт полный URL объекта.
|
||||
|
||||
Отсюда вторая половина решения, без которой первая недопустима: **отказы
|
||||
обрываются**. Наружу идёт свой текст с идентификатором записи, а чужая цепочка
|
||||
`%w` — нет. В журнал приёма вместо имени идёт расширение собственным полем;
|
||||
прослеживаемость от этого не страдает.
|
||||
|
||||
Запись попадает в журнал как **намеренный отказ от очевидного подхода**: закрыть
|
||||
файлы токеном предложат снова, и без записанной причины предложение выглядит
|
||||
бесплатным.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` ссылка работает без токена, и приёмка проверяется обычным запросом; будущее
|
||||
приложение получает файл без отдельного механизма выдачи токенов.
|
||||
- `+` изъятие из инварианта приватности не расширилось: в журнале по-прежнему
|
||||
только расширение, а не имя.
|
||||
- `−` ссылка, единожды утёкшая, работает бессрочно: отзыва у неё нет, а файлы не
|
||||
удаляются вовсе. Утечка возможна не только журналом — любой будущий экран,
|
||||
показывающий ссылку, наследует это свойство.
|
||||
- `−` появилась норма, которую держит не построение, а внимание: всякий новый
|
||||
отказ хранилища надо обрывать руками. Норму сторожат требование capability
|
||||
`storage` и проверка журнала, но компилятор — нет.
|
||||
- `−` диагностируемость отказов упала: обрывая цепочку, мы теряем причину. У
|
||||
выгрузки в Object Storage это смягчено — сохраняется класс отказа SDK
|
||||
(`AccessDenied`, `NoSuchBucket`), в котором адреса не бывает.
|
||||
- Решение действует до разграничения доступа: задачи `oidc-login` и
|
||||
`record-ownership` меняют условие, и тогда пометку стоит пересмотреть новой
|
||||
записью.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Каталог данных задаётся одним ключом `[storage] data_dir`
|
||||
|
||||
- **Дата:** 2026-08-12
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md](../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md),
|
||||
раздел «Ключи конфигурации: два пути заменяются одним каталогом»
|
||||
|
||||
## Решение
|
||||
|
||||
Ключи `[database] path` и `[storage] path` уходят. Вместо них — один
|
||||
`[storage] data_dir` со значением `data`: база и файлы записей лежат под одним
|
||||
каталогом, и по-другому хранилище не умеет.
|
||||
|
||||
Выбор сделан человеком 2026-08-12 из трёх названных вариантов.
|
||||
|
||||
## Почему
|
||||
|
||||
Имя ключа конфигурации проект объявил необратимым
|
||||
([../../CLAUDE.md](../../CLAUDE.md), «Работа»): переименование правится не в
|
||||
одном файле, а в конфигурации на сервере и в выкладке, и молча ломает запуск.
|
||||
Поэтому выбор ушёл человеку, а не был принят по ходу.
|
||||
|
||||
Цитата источника о цене каждого варианта:
|
||||
|
||||
> - `[storage] data_dir` — **выбрано**. Ключ назван по назначению, как названы и
|
||||
> сегодняшние; смена библиотеки через год имени не тронет. Слово `storage` при
|
||||
> этом уже занято capability, но в конфигурации оно значит ровно то же — где
|
||||
> лежат данные;
|
||||
> - `[pocketbase] data_dir` — прямее всего читается тем, кто знает библиотеку, и
|
||||
> вписывает имя поставщика в необратимый ключ. Смена библиотеки потребует
|
||||
> второго необратимого переименования;
|
||||
> - `[data] dir` — короче и нейтральнее всех, но `data` в проекте уже значит
|
||||
> каталог на диске, и секция с таким именем читается как «настройки каталога»,
|
||||
> а не «настройки хранилища».
|
||||
|
||||
Запись попадает в журнал по **дорогому откату**: переименование ключа стоит
|
||||
правки конфигурации на сервере и в выкладке, а ошибка проявляется отказом старта.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` имя ключа не называет поставщика, и смена библиотеки хранилища второго
|
||||
необратимого переименования не потребует.
|
||||
- `+` каталог данных один, и запрет «боевой каталог не трогать» покрывает и базу,
|
||||
и записи одной строкой.
|
||||
- `−` слово `storage` в проекте теперь значит три вещи: capability, само
|
||||
хранилище и секцию конфигурации. Поле записи о файле от этого переименовано в
|
||||
`location` — чтобы смыслов было три, а не четыре.
|
||||
- `−` прежние конфигурации несовместимы: сервис на старом `config.toml`
|
||||
поднимется на умолчании `data`, а не на прежних путях. Данные при этом не
|
||||
переносятся по решению задачи, так что цена нулевая ровно сейчас и была бы не
|
||||
нулевой при переносе.
|
||||
@@ -32,6 +32,8 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-12 | [Ссылка на файл открыта знанием записи, а защищает её отсутствие имени в журнале](ADR-2026-08-12-file-link-open-but-not-logged.md) | |
|
||||
| 2026-08-12 | [Каталог данных задаётся одним ключом `[storage] data_dir`](ADR-2026-08-12-single-data-dir-config-key.md) | |
|
||||
| 2026-08-11 | [Границу распознавания доменного признака держит норма, а не код](ADR-2026-08-11-domain-marker-boundary-by-norm.md) | |
|
||||
| 2026-08-11 | [Отказ, который решено не проверять, объявляется поимённо](ADR-2026-08-11-errcheck-check-blank.md) | |
|
||||
| 2026-08-11 | [Наружу расширение выходит только приведённым к перечню](ADR-2026-08-11-known-format-label.md) | |
|
||||
|
||||
Reference in New Issue
Block a user