- archive local-fs-storage-backend, sqlite-metadata-backend, storage-orphan-reconciliation, metadata-managed-jwt-secret, default-local-backends - sync specs: storage-backends, metadata-backends, storage-reconciliation, app-secrets; update photos/share-links - defaults reflected in specs: SQLite and local FS
174 lines
12 KiB
Markdown
174 lines
12 KiB
Markdown
# photos Specification
|
|
|
|
## Purpose
|
|
|
|
Загрузка, хранение в S3 и просмотр фотографий с генерацией вариантов и извлечением EXIF.
|
|
|
|
## Requirements
|
|
|
|
### Requirement: Загрузка фото
|
|
Пользователь с правом загрузки SHALL иметь возможность загружать фото по одному и массово.
|
|
|
|
#### Scenario: Успешная загрузка
|
|
- **WHEN** участник с правом загрузки отправляет файл изображения
|
|
- **THEN** система сохраняет оригинал и создаёт запись фото с метаданными
|
|
|
|
#### Scenario: Загрузка без права
|
|
- **WHEN** наблюдатель пытается загрузить фото
|
|
- **THEN** система отклоняет запрос
|
|
|
|
### Requirement: Варианты изображений
|
|
Система SHALL генерировать три варианта: thumb (200px), preview (1200px) и оригинал, с WebP для превью, и извлекать EXIF (дата, координаты, размеры).
|
|
|
|
#### Scenario: Генерация вариантов
|
|
- **WHEN** фото загружено
|
|
- **THEN** система асинхронно генерирует thumb и preview и заполняет EXIF-поля
|
|
|
|
### Requirement: Листинг фото с пагинацией
|
|
Система SHALL возвращать фото альбома с cursor-пагинацией.
|
|
|
|
#### Scenario: Пагинация по курсору
|
|
- **WHEN** клиент запрашивает список с курсором и лимитом
|
|
- **THEN** система возвращает не более лимита фото и курсор следующей страницы
|
|
|
|
### Requirement: Получение фото по требованию
|
|
Клиент SHALL иметь возможность получить актуальные данные фото с подписанным URL,
|
|
обслуживаемым бэкендом, независимо от бэкенда хранения.
|
|
|
|
#### Scenario: Выдача свежего URL
|
|
- **WHEN** клиент запрашивает фото по id
|
|
- **THEN** система возвращает свежий подписанный URL бэкенда (срок до 1 часа) для просмотра
|
|
|
|
### Requirement: Скачивание одиночного фото
|
|
Любой участник, включая viewer, SHALL иметь возможность скачать одиночное фото.
|
|
|
|
#### Scenario: Скачивание фото
|
|
- **WHEN** участник запрашивает скачивание фото
|
|
- **THEN** система возвращает подписанный URL оригинала, обслуживаемый бэкендом
|
|
|
|
### Requirement: Удаление фото
|
|
Автор SHALL иметь возможность удалить своё фото; владелец и админ SHALL иметь возможность удалить любое фото. Удаление SHALL помещать фото в корзину и SHALL не удалять объекты хранилища.
|
|
|
|
#### Scenario: Удаление своего фото
|
|
- **WHEN** автор удаляет своё фото
|
|
- **THEN** фото помещается в корзину, а объекты хранилища сохраняются
|
|
|
|
#### Scenario: Удаление чужого фото участником
|
|
- **WHEN** участник с ролью member пытается удалить чужое фото
|
|
- **THEN** система отклоняет запрос
|
|
|
|
### Requirement: Обложка по умолчанию
|
|
При обработке загруженного фото система SHALL назначать его обложкой альбома только если
|
|
обложка ещё не задана.
|
|
|
|
#### Scenario: Первое фото
|
|
- **WHEN** фото обработано, и у альбома нет обложки
|
|
- **THEN** система назначает это фото обложкой
|
|
|
|
#### Scenario: Обложка уже задана
|
|
- **WHEN** фото обработано, а обложка уже задана
|
|
- **THEN** система не меняет обложку
|
|
|
|
### Requirement: Переименование фото
|
|
Автор фото, владелец и админ альбома SHALL иметь возможность изменить имя файла фото;
|
|
остальные SHALL получать отказ, а пустое имя SHALL отклоняться.
|
|
|
|
#### Scenario: Переименование автором
|
|
- **WHEN** автор меняет имя файла своего фото на непустое
|
|
- **THEN** система сохраняет новое имя и транслирует изменение подписчикам альбома
|
|
|
|
#### Scenario: Пустое имя
|
|
- **WHEN** пользователь подтверждает пустое имя файла
|
|
- **THEN** система отклоняет запрос
|
|
|
|
#### Scenario: Нет прав
|
|
- **WHEN** участник без прав переименовывает чужое фото
|
|
- **THEN** система отклоняет запрос
|
|
|
|
### Requirement: Ограничение размера загрузки
|
|
Система SHALL ограничивать размер загружаемых файлов и SHALL отвечать понятной ошибкой при
|
|
превышении лимита, а не ошибкой разбора формы.
|
|
|
|
#### Scenario: Превышение лимита
|
|
- **WHEN** загружаемый файл превышает максимальный размер
|
|
- **THEN** система отклоняет загрузку с явной ошибкой о превышении размера
|
|
|
|
#### Scenario: Файл в пределах лимита
|
|
- **WHEN** файл не превышает лимит
|
|
- **THEN** система принимает multipart-загрузку и обрабатывает фото
|
|
|
|
### Requirement: Метаданные фото (EXIF)
|
|
При загрузке фото система SHALL извлекать распознаваемые EXIF-атрибуты и SHALL возвращать их
|
|
вместе с данными фото; при отсутствии метаданных SHALL возвращать пустой набор.
|
|
|
|
#### Scenario: Фото с метаданными
|
|
- **WHEN** загружается фото с EXIF
|
|
- **THEN** данные фото содержат извлечённые атрибуты (камера, экспозиция, ISO, дата съёмки и др.)
|
|
|
|
#### Scenario: Фото без метаданных
|
|
- **WHEN** загружается фото без читаемых EXIF
|
|
- **THEN** данные фото не содержат атрибутов, а загрузка выполняется успешно
|
|
|
|
### Requirement: Перенос и копирование фото между альбомами
|
|
Пользователь с правом удаления в исходном альбоме и правом загрузки в целевом SHALL иметь
|
|
возможность перенести или скопировать фото в другой альбом. Операции SHALL изменять только
|
|
метаданные и SHALL не изменять объекты хранилища.
|
|
|
|
#### Scenario: Перенос
|
|
- **WHEN** пользователь переносит фото в другой альбом
|
|
- **THEN** фото исчезает из исходного альбома и появляется в целевом, объекты хранилища не меняются
|
|
|
|
#### Scenario: Копирование
|
|
- **WHEN** пользователь копирует фото в другой альбом
|
|
- **THEN** в целевом альбоме появляется новая запись, ссылающаяся на те же объекты хранилища
|
|
|
|
### Requirement: Корзина фото
|
|
Удаление фото SHALL помещать его в корзину (мягкое удаление). Восстановление SHALL возвращать
|
|
фото в его альбом. Окончательное удаление SHALL удалять запись, а объекты хранилища SHALL
|
|
удаляться только когда на них не остаётся ссылок ни в одном альбоме.
|
|
|
|
#### Scenario: Удаление в корзину
|
|
- **WHEN** пользователь удаляет фото
|
|
- **THEN** фото пропадает из альбома и появляется в корзине
|
|
|
|
#### Scenario: Восстановление
|
|
- **WHEN** пользователь восстанавливает фото из корзины
|
|
- **THEN** фото возвращается в свой альбом
|
|
|
|
#### Scenario: Окончательное удаление без ссылок
|
|
- **WHEN** пользователь удаляет фото из корзины навсегда, и на те же объекты нет ссылок в других альбомах
|
|
- **THEN** запись и объекты хранилища удаляются
|
|
|
|
#### Scenario: Окончательное удаление при наличии ссылок
|
|
- **WHEN** на объект хранилища ссылается копия в другом альбоме
|
|
- **THEN** запись удаляется, а объект хранилища сохраняется
|
|
|
|
### Requirement: Удаление альбома и восстановление фото без альбома
|
|
Удаление альбома SHALL помещать его фото в корзину, не удаляя объекты хранилища. Если при
|
|
восстановлении фото его альбом отсутствует, система SHALL предложить выбрать существующий
|
|
альбом или создать новый.
|
|
|
|
#### Scenario: Удаление альбома
|
|
- **WHEN** владелец удаляет альбом
|
|
- **THEN** его фото оказываются в корзине без привязки к альбому, файлы сохраняются
|
|
|
|
#### Scenario: Восстановление без альбома
|
|
- **WHEN** пользователь восстанавливает фото, у которого нет альбома
|
|
- **THEN** система предлагает выбрать альбом или создать новый и восстанавливает фото в него
|
|
|
|
### Requirement: Метки фото
|
|
Пользователь SHALL присваивать фото свои метки (область — пользователь): создавать новые и
|
|
выбирать из списка с поиском; метки SHALL отдаваться вместе с фото и сниматься.
|
|
|
|
#### Scenario: Создание метки
|
|
- **WHEN** пользователь вводит новую метку и подтверждает
|
|
- **THEN** метка создаётся и привязывается к фото
|
|
|
|
#### Scenario: Выбор из списка с поиском
|
|
- **WHEN** пользователь вводит часть названия метки
|
|
- **THEN** показываются подходящие метки пользователя для выбора
|
|
|
|
#### Scenario: Снятие метки
|
|
- **WHEN** пользователь убирает метку с фото
|
|
- **THEN** метка отвязывается от фото
|