Files
ds-galo/openspec/specs/photos/spec.md
T

263 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# photos Specification
## Purpose
Загрузка, хранение в S3 и просмотр фотографий с генерацией вариантов и извлечением EXIF.
## Requirements
### Requirement: Загрузка фото
Пользователь с правом загрузки SHALL иметь возможность загружать медиа (изображения и
видео) по одному и массово; система SHALL принимать ввод `image/*` и `video/*`.
#### Scenario: Успешная загрузка
- **WHEN** участник с правом загрузки отправляет файл изображения
- **THEN** система сохраняет оригинал и создаёт запись фото с метаданными
#### Scenario: Успешная загрузка видео
- **WHEN** участник с правом загрузки отправляет видеофайл
- **THEN** система сохраняет оригинал и создаёт медиа-запись с признаком видео
#### Scenario: Загрузка без права
- **WHEN** наблюдатель пытается загрузить фото
- **THEN** система отклоняет запрос
#### Scenario: Неподдерживаемый файл
- **WHEN** загружается файл, не являющийся изображением или видео
- **THEN** система отклоняет загрузку
### Requirement: Варианты изображений
Система SHALL генерировать четыре варианта: thumb (200×200, квадратный кроп), medium
(800×800, квадратный кроп), preview (1200×1200, вписанный) и оригинал, в WebP, и извлекать
EXIF (дата, координаты, размеры).
#### Scenario: Генерация вариантов
- **WHEN** фото загружено
- **THEN** система асинхронно генерирует thumb, medium и preview и заполняет EXIF-поля
#### Scenario: Форма вариантов сетки
- **WHEN** генерируются thumb и medium
- **THEN** они обрезаны до квадрата, а preview вписан с сохранением пропорций
### Requirement: Листинг фото с пагинацией
Система SHALL возвращать фото альбома с cursor-пагинацией.
#### Scenario: Пагинация по курсору
- **WHEN** клиент запрашивает список с курсором и лимитом
- **THEN** система возвращает не более лимита фото и курсор следующей страницы
### Requirement: Получение фото по требованию
Клиент SHALL иметь возможность получить актуальные данные фото с подписанным URL,
обслуживаемым бэкендом, независимо от бэкенда хранения. Подпись SHALL быть
детерминированной в пределах окна действия, чтобы повторные выдачи одного и того же
объекта давали идентичный URL.
#### Scenario: Выдача свежего URL
- **WHEN** клиент запрашивает фото по id
- **THEN** система возвращает подписанный URL бэкенда для просмотра
#### Scenario: Повторная выдача совпадает
- **WHEN** один и тот же объект запрашивается повторно в пределах окна действия подписи
- **THEN** система возвращает идентичный URL
### Requirement: Скачивание одиночного фото
Любой участник, включая viewer, SHALL иметь возможность скачать одиночное фото.
#### Scenario: Скачивание фото
- **WHEN** участник запрашивает скачивание фото
- **THEN** система возвращает подписанный URL оригинала, обслуживаемый бэкендом
### Requirement: Удаление фото
Автор SHALL иметь возможность удалить своё фото; владелец и админ SHALL иметь возможность удалить любое фото. Удаление SHALL помещать фото в корзину и SHALL не удалять объекты хранилища.
#### Scenario: Удаление своего фото
- **WHEN** автор удаляет своё фото
- **THEN** фото помещается в корзину, а объекты хранилища сохраняются
#### Scenario: Удаление чужого фото участником
- **WHEN** участник с ролью member пытается удалить чужое фото
- **THEN** система отклоняет запрос
### Requirement: Обложка по умолчанию
При загрузке фото система SHALL назначать его обложкой альбома, если обложка ещё не задана,
то есть обложкой становится первое загруженное фото. Когда фото, выбранное обложкой, покидает
альбом (помещено в корзину или перенесено в другой альбом), система SHALL снять обложку.
#### Scenario: Первое фото
- **WHEN** фото загружено, и у альбома нет обложки
- **THEN** система назначает это фото обложкой альбома
#### Scenario: Обложка уже задана
- **WHEN** фото загружено, а обложка уже задана
- **THEN** система не меняет обложку
#### 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** система отклоняет загрузку с явной ошибкой о превышении размера
#### 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
удаляться только когда на них не остаётся ссылок ни в одном альбоме. Окончательное удаление
SHALL быть доступно и для фото без альбома (например, после удаления альбома).
#### Scenario: Удаление в корзину
- **WHEN** пользователь удаляет фото
- **THEN** фото пропадает из альбома и появляется в корзине
#### 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** метка отвязывается от фото
### Requirement: Кэширование объектов
Доставка объектов SHALL помечать ответы как кэшируемые, чтобы браузер переиспользовал
уже полученные превью, а повторное открытие альбома не вызывало повторную загрузку
неизменившихся объектов.
#### Scenario: Заголовки кэширования
- **WHEN** клиент получает объект (оригинал, превью или thumb) по подписанному URL
- **THEN** ответ содержит заголовок кэширования с ограниченным сроком годности
#### Scenario: Повторный показ без перезагрузки
- **WHEN** сетка превью перерисовывается, а URL объектов не изменились
- **THEN** браузер отдаёт объекты из кэша без повторного запроса к бэкенду
### Requirement: Медиа-запись фото
Данные фото SHALL включать вид медиа (`image` | `video`), длительность для видео и адрес
воспроизведения, когда видео-вариант готов; для изображений адрес воспроизведения
отсутствует.
#### Scenario: Запись изображения
- **WHEN** клиент получает данные изображения
- **THEN** вид медиа — изображение, длительность и адрес воспроизведения отсутствуют
#### Scenario: Запись видео без готового варианта
- **WHEN** клиент получает данные видео, а вариант ещё не готов
- **THEN** вид медиа — видео, длительность присутствует, адрес воспроизведения отсутствует
#### Scenario: Запись видео с готовым вариантом
- **WHEN** видео-вариант готов
- **THEN** данные медиа содержат адрес воспроизведения
### Requirement: Очистка корзины
Пользователь SHALL иметь возможность окончательно удалить все элементы корзины одним действием.
Система SHALL удалять только те элементы, которые пользователь вправе удалить навсегда, и SHALL
пропускать остальные. Операция SHALL возвращать число удалённых элементов.
#### Scenario: Очистка корзины
- **WHEN** пользователь очищает корзину
- **THEN** все элементы, которые он вправе удалить навсегда, удаляются
#### Scenario: Чужие элементы
- **WHEN** в корзине есть фото, которые пользователь не вправе удалить навсегда
- **THEN** они остаются в корзине, а остальные удаляются
#### Scenario: Пустая корзина
- **WHEN** пользователь очищает пустую корзину
- **THEN** операция завершается успешно, удалив ноль элементов