diff --git a/README.md b/README.md index 69e5ddf..9dd73cd 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,72 @@ -
-

- p1ctos4ve -

-

- selfhosted image preservation service -

-
+# p1ctos4ve +Проект в рамках дисциплины «Конструирование программного обеспечения»: сервис сохранения и организации медиа со внешних ресурсов +## Участники +- Железняков Марк Викторович - `5130904/30105` +- Михеев Егор Романович - `5130904/30105` +- Михальченко Владислав Сергеевич - `5130904/30105` +- Ботыгин Иван Алексеевич - `5130904/30105` + +## Определение проблемы +Пользователи, сохраняющие большое количество медиафайлов (фотографий, видео, GIF) из разных источников (социальные сети, мессенджеры и другие специализированные платформы), сталкиваются с трудностями их систематизации и последующего поиска. Файлы скапливаются в беспорядке, отсутствует единая система тегов и категорий, что делает быстрый поиск нужного материала практически невозможным и требует значительных ручных усилий по организации. При этом всегда присутствует риск удаления материалов из первоисточника. + +## Выработка требований + +### Пользовательские истории +1. Как пользователь, я хочу добавлять медиафайлы (фото, видео, GIF) в свою библиотеку из различных источников (прямая загрузка, по ссылке, через интеграцию с Tenor/Pinterest), чтобы централизованно хранить все материалы. +2. Как пользователь, я хочу иметь мощную систему поиска по библиотеке с фильтрацией по типу файла, тегам, категориям и дате добавления, чтобы быстро находить конкретные материалы. +3. Как пользователь, я хочу иметь возможность просматривать свою медиатеку в удобном интерфейсе (галерея, список) с предпросмотром, чтобы легко ориентироваться в содержимом. + +## Разработка архитектуры и детальное проектирование + +### Оценка масштаба +> Оценка масштаба производится при условии нагрузки на сервис в размере 10 000 активных пользователей в сутки. При этом сервис подазумевает опцию селфхостинга, что может снизить требования к ресурсам центрального экземпляра (инстанса) сервиса. + +#### Характер нагрузки + +##### Соотношение R/W нагрузки +**70% на чтение и 30% на запись.** Обосновать это можно тем, что запись метаданных и загрузка файлов будет занимать большую долю операций в сервисе с пиками во время массовых импортов из Tenor/Pinterest. При этом чтение данных будет производится сильно реже + +##### Трафик +Для входящего трафика предположим, что один пользователь в среднем будем генерировать 10 МБ данных. Так, 10 000 пользователей * 10 МБ = 100 ГБ/день. + +##### Объемы дисковой системы +При начальном размере файла ~5 МБ и 10 загрузках на пользователя в день: 10 000 * 10 * 5 МБ = 500 ГБ/день. Поэтому может потребоваться дешевое S3-хранилище. + +### Диаграммы C4 Model + +#### Контекст +![[assets/С4-1context.jpg]] + +#### Контейнеры +![[assets/С4-2containers.jpg]] + +#### Компоненты +![[С4-3components.jpg]] + +#### Код +> WIP + +### Контракты API +Краткая информация о методах API доступна в [assets/API.md](API.md), а полная документация - в [Scalar](https://scalar.com/) на эндпоинте `/openapi`. + +### Схема БД и оправдание с точки зрения нефункциональных требований +> WIP + +### Схема масштабирования сервиса при росте нагрузки в 10 раз +Для возможности масштабирования можно использовать следующие подходы: +- Размещение бекенда за балансировщиком нагрузки (например, Nginx, Traefik или Caddy): трафик будет распределяться равномерно между двумя или более экземплярами API +- Репликация БД - основная база принимает запись, а одна или несколько реплик получают изменения по журналу (WAL) и обслуживают преимущественно чтение. +- Кеширование частозапрашиваемых данных через Redis: результаты чтения (например, списки, карточки по ID, агрегации, результаты поиска с популярными фильтрами) складываются в KV хранилище с некоторым TTL, что позволяет сократить время ответа на повторяющиеся запросы + +## Кодирование и отладка +> WIP + +## Unit-тестирование +> WIP + +## Интеграционное тестирование +> WIP + +## Сборка и запуск +> WIP diff --git a/assets/API.md b/assets/API.md new file mode 100644 index 0000000..19100f3 --- /dev/null +++ b/assets/API.md @@ -0,0 +1,283 @@ +# p1ctos4ve - контракты API + +## Better Auth +API использует фреймворк [Better Auth](https://better-auth.com) для управления пользователями и сессиями. + +Для работы с авторизацией необходимо использовать [клиентскую библиотеку](https://www.better-auth.com/docs/concepts/client) Better Auth, прямой вызов методов авторизации из клиентов не предусматривается. + +### `POST /auth/api/sign-up/email` + +Регистрация нового пользователя с почтой и паролем. + +**Параметры запроса:** +```json +{ + "name": "string", + "email": "string", + "password": "string", + "image": "string (опционально)", + "callbackURL": "string (опционально)", + "rememberMe": "boolean (опционально, по умолчанию true)" +} +``` + +**Ответ (200):** +```json +{ + "token": "string | null", + "user": { + "id": "string", + "email": "string", + "name": "string", + "image": "string | null", + "emailVerified": "boolean", + "createdAt": "string (date-time)", + "updatedAt": "string (date-time)" + } +} +``` + +### `POST /auth/api/sign-in/email` + +Вход с использованием почты и пароля. + +**Параметры запроса:** +```json +{ + "email": "string", + "password": "string", + "callbackURL": "string (опционально)", + "rememberMe": "string (опционально)" +} +``` + +**Ответ (200):** +```json +{ + "redirect": false, + "token": "string", + "url": null, + "user": { + "id": "string", + "email": "string", + "name": "string | null", + "image": "string | null", + "emailVerified": "boolean", + "createdAt": "string (date-time)", + "updatedAt": "string (date-time)" + } +} +``` + +### `POST /auth/api/sign-out` + +Выход из системы и завершение текущей сессии. + +**Ответ (200):** +```json +{ + "success": "boolean" +} +``` + +## Сейвы +API для работы с сохраненными медиафайлами (видео, фото). Каждый сейв имеет настройку видимости, определяющую режим доступа. + +### Режимы видимости сейва +- `public` - сейв доступен всем пользователям, отображается в публичных списках и профиле владельца +- `link` - сейв доступен только по прямой ссылке (shareUrl), не отображается в публичных списках. Владелец всегда видит его в своих списках + +### `GET /saves/my` + +Возвращает все сейвы текущего пользователя независимо от режима видимости. + +**Аутентификация:** Опциональна + +**Ответ (200):** +```json +[ + { + "id": "number", + "name": "string", + "type": "string", + "description": "string", + "tags": ["string"], + "visibility": "public" | "link", + "shareUrl": "string (только для visibility: link)", + "createdAt": "string (date-time)", + "updatedAt": "string (date-time)" + } +] +``` + +### `GET /saves/u/{slug}` +Возвращает **только публичные сейвы** (visibility: public) конкретного пользователя по его никнейму (slug). Сейвы с видимостью "link" не включаются в этот список, даже если запрашивает владелец. + +**Параметры пути:** +- `slug` (string, обязательный) - никнейм пользователя + +**Аутентификация:** Опциональна + +**Ответ (200):** +```json +[ + { + "id": "number", + "name": "string", + "type": "string", + "description": "string", + "tags": ["string"], + "visibility": "public", + "createdAt": "string (date-time)", + "updatedAt": "string (date-time)" + } +] +``` + +### `GET /saves/{id}` +Возвращает информацию о конкретном сейве. + +**Параметры пути:** +- `id` (number/string, обязательный) - ID сейва + +**Аутентификация:** Опциональна + +**Правила доступа:** +- **Владелец:** полный доступ ко всем сейвам (public и link) +- **Другие пользователи:** + - **public сейвы:** доступны всем + - **link сейвы:** доступны только при обращении по корректной shareUrl; запрос по голому ID вернет `404 Not Found` + +**Ответ (200):** +```json +{ + "id": "number", + "name": "string", + "type": "string", + "description": "string", + "tags": ["string"], + "visibility": "public" | "link", + "shareUrl": "string (только для visibility: link)", + "userId": "string", + "createdAt": "string (date-time)", + "updatedAt": "string (date-time)" +} +``` + +**Коды ошибок:** +- `404 Not Found` - сейв не найден или недоступен (для link сейвов без shareUrl) +- `403 Forbidden` - нет прав доступа + +### `DELETE /saves/{id}` +Удаляет сейв по его ID. Доступно только владельцу. + +**Параметры пути:** +- `id` (number/string, обязательный) - ID сейва + +**Аутентификация:** Обязательна + +**Ответ (200):** +```json +{ + "success": true, + "message": "Сейв успешно удален" +} +``` + +### `PATCH /saves/{id}` +Обновляет метаданные сейва (название, описание, теги, видимость). Доступно только владельцу. + +**Параметры пути:** +- `id` (number/string, обязательный) - ID сейва + +**Параметры запроса:** +```json +{ + "name": "string (опционально)", + "description": "string (опционально)", + "tags": ["string"] (опционально), + "visibility": "public" | "link" (опционально) +} +``` + +**Аутентификация:** Обязательна + +**Content-Type:** `application/json`, `application/x-www-form-urlencoded`, или `multipart/form-data` + +**Ответ (200):** +```json +{ + "id": "number", + "name": "string", + "type": "string", + "description": "string", + "tags": ["string"], + "visibility": "public" | "link", + "shareUrl": "string (если visibility: link)", + "updatedAt": "string (date-time)" +} +``` + +**Примечание:** При установке `visibility: "link"` сервер автоматически генерирует или возвращает существующую `shareUrl`. При смене на `visibility: "public"` поле `shareUrl` становится неактуальным. + +### `POST /saves/external` +Скачивает и сохраняет медиафайл по URL из стороннего источника. + +**Параметры запроса:** +```json +{ + "url": "string (URI, обязательный)", + "name": "string (опционально)", + "description": "string (опционально)", + "tags": ["string"] (опционально), + "visibility": "public" | "link" (опционально, по умолчанию: link)" +} +``` + +**Аутентификация:** Обязательна + +**Content-Type:** `application/json`, `application/x-www-form-urlencoded`, или `multipart/form-data` + +**Ответ (200):** +```json +{ + "id": "number", + "name": "string", + "type": "string", + "url": "string", + "visibility": "public" | "link", + "shareUrl": "string (если visibility: link)", + "createdAt": "string (date-time)" +} +``` + +Если `visibility` не указана, по умолчанию устанавливается `link`. + +### `POST /saves/upload` +Загружает медиафайл с устройства. + +**Параметры запроса (multipart/form-data):** +- `file` (`binary`, обязательный) - файл для загрузки +- `name` (`string`, опционально) - название сейва +- `description` (`string`, опционально) - описание сейва +- `tags` (`array[string]`, опционально) - теги +- `visibility` (`"public" | "link"`, опционально, по умолчанию: "link") - режим видимости + +**Аутентификация:** Обязательна + +**Content-Type:** `multipart/form-data` + +**Ответ (200):** +```json +{ + "id": "number", + "name": "string", + "type": "string", + "url": "string", + "visibility": "public" | "link", + "shareUrl": "string (если visibility: link)", + "createdAt": "string (date-time)" +} +``` + +Если `visibility` не указана, по умолчанию устанавливается `link`. diff --git a/assets/С4-1context.jpg b/assets/С4-1context.jpg new file mode 100644 index 0000000..3988d3b Binary files /dev/null and b/assets/С4-1context.jpg differ diff --git a/assets/С4-2containers.jpg b/assets/С4-2containers.jpg new file mode 100644 index 0000000..ace05a0 Binary files /dev/null and b/assets/С4-2containers.jpg differ diff --git a/assets/С4-3components.jpg b/assets/С4-3components.jpg new file mode 100644 index 0000000..53a981d Binary files /dev/null and b/assets/С4-3components.jpg differ