Эта страница переведена машинным способом. Заметили ошибку?Помогите её улучшить.
Skip to content

База данных

SnapOtter использует PostgreSQL 17 с Drizzle ORM (pg-core / node-postgres) для хранения данных. Схема определена в apps/api/src/db/schema.ts.

Подключение настраивается через переменную окружения DATABASE_URL (по умолчанию postgres://snapotter:snapotter@postgres:5432/snapotter). В Docker Compose контейнер Postgres хранит свои данные в именованном томе SnapOtter-pgdata. Запросы обслуживаются под ролью, которая может только читать и записывать строки; подробнее об этом в разделе Роли с минимальными привилегиями ниже.

Таблицы

users

Хранит учётные записи пользователей. Создаётся автоматически при первом запуске из DEFAULT_USERNAME и DEFAULT_PASSWORD.

СтолбецТипПримечания
iduuidПервичный ключ
usernamevarcharУникальный, обязательный
passwordHashvarcharscrypt-хеш
rolevarcharadmin, editor или user
mustChangePasswordbooleanФлаг принудительного сброса пароля
createdAttimestampВремя создания
updatedAttimestampВремя последнего обновления

sessions

Активные сессии входа. Каждая строка связывает токен сессии с пользователем.

СтолбецТипПримечания
idvarcharПервичный ключ (токен сессии)
userIduuidВнешний ключ к users.id
expiresAttimestampВремя истечения
createdAttimestampВремя создания

teams

Группы для организации пользователей. Администраторы могут назначать пользователей в команды.

СтолбецТипОписание
iduuidПервичный ключ
namevarchar (уникальный, макс. 50 символов)Название команды
createdAttimestampВремя создания

api_keys

API-ключи для программного доступа. Необработанный ключ показывается один раз при создании; хранится только хеш.

СтолбецТипПримечания
iduuidПервичный ключ
userIduuidВнешний ключ к users.id
keyHashvarcharscrypt-хеш ключа
namevarcharМетка, заданная пользователем
createdAttimestampВремя создания
lastUsedAttimestampОбновляется при каждом аутентифицированном запросе

Ключи имеют префикс si_, за которым следуют 96 hex-символов (48 случайных байт).

pipelines

Сохранённые цепочки инструментов, которые пользователи создают в интерфейсе.

СтолбецТипПримечания
iduuidПервичный ключ
namevarcharНазвание пайплайна
descriptionvarcharНеобязательное описание
stepsjsonbМассив объектов { toolId, settings }
createdAttimestampВремя создания

user_files

Постоянная библиотека файлов. По умолчанию сохранённое изменение вставляется как независимая корневая строка («сохранить как новый»: version 1, parentId null, поэтому оригинал остаётся в списке) или как связанная с родителем версия, когда вы перезаписываете оригинал (parentId задан, version увеличивается, вытесняя его). Столбец toolChain записывает применённые инструменты.

СтолбецТипОписание
iduuidПервичный ключ
userIduuidFK к users (CASCADE DELETE)
originalNamevarcharИмя исходного загруженного файла
storedNamevarcharИмя файла на диске
mimeTypevarcharMIME-тип
sizeintegerРазмер файла в байтах
widthintegerШирина изображения в px
heightintegerВысота изображения в px
versionintegerНомер версии (1 = оригинал)
parentIduuid или nullFK к user_files (родительская версия)
toolChainjsonbID инструментов, применённых по порядку для создания этой версии
createdAttimestampВремя создания

jobs

Отслеживает задачи обработки для отчётности о прогрессе и очистки.

СтолбецТипПримечания
iduuidПервичный ключ
typevarcharИдентификатор инструмента или пайплайна
statusvarcharqueued, processing, completed или failed
progressrealДоля от 0.0 до 1.0
inputFilesjsonbМассив путей к входным файлам
outputPathvarcharПуть к файлу результата
settingsjsonbИспользованные настройки инструмента
errorvarcharСообщение об ошибке при сбое
createdAttimestampВремя создания
completedAttimestampВремя завершения

settings

Хранилище «ключ-значение» для общесерверных настроек, которые администраторы могут менять из интерфейса.

СтолбецТипПримечания
keyvarcharПервичный ключ
valuevarcharЗначение настройки
updatedAttimestampВремя последнего обновления

roles

Пользовательские роли с гранулярными разрешениями.

СтолбецТипПримечания
iduuidПервичный ключ
namevarcharУникальное имя роли
descriptionvarcharНеобязательное описание
permissionsjsonbМассив строк разрешений
createdAttimestampВремя создания

audit_log

Журнал действий, значимых для безопасности.

СтолбецТипПримечания
iduuidПервичный ключ
userIduuidFK к users
actionvarcharТип действия
detailsjsonbДанные, специфичные для действия
createdAttimestampВремя действия

user_preferences

Состояние интерфейса для каждого пользователя, ключом служит имя настройки. Хранит закреплённые инструменты главной страницы, которые записываются через PUT /api/v1/preferences.

СтолбецТипПримечания
userIdtextFK к users, каскадное удаление. Первичный ключ вместе с key
keytextИмя настройки. Первичный ключ вместе с userId
valuejsonbСодержимое настройки
updatedAttimestampВремя последней записи

Миграции

Drizzle управляет миграциями схемы. Файлы миграций находятся в apps/api/drizzle/. Во время разработки:

bash
cd apps/api
npx drizzle-kit generate   # generate a migration from schema changes
npx drizzle-kit migrate    # apply pending migrations

В продакшене ожидающие миграции применяются автоматически при запуске.

Роли с минимальными привилегиями

Две роли, две задачи. DATABASE_URL обслуживает запросы и имеет права SELECT, INSERT, UPDATE, DELETE на таблицы приложения, а также USAGE и SELECT на их последовательности. Это весь список. Эта роль не может создать или удалить таблицу, установить расширение, выполнить TRUNCATE, прочитать pg_authid, создать базу данных, изменить роль или обратиться к схеме drizzle, где хранится история миграций.

Привилегированная роль задаётся в DATABASE_MIGRATION_URL. Она применяет миграции и выдаёт права рабочей роли во время запуска, после чего её соединение закрывается ещё до обслуживания первого запроса.

В Compose и в образе «всё в одном» всё уже настроено именно так, включая существующие установки. При запуске SnapOtter создаёт рабочую роль, если её нет, выдаёт ей права, применяет миграции, а затем распространяет права на таблицы, которые существовали раньше. Для обновления не нужно вручную выполнять SQL.

Если оставить DATABASE_MIGRATION_URL пустым, экземпляр работает с одной ролью, и DATABASE_URL выполняет обе задачи ровно так же, как до разделения. Это поддерживаемая конфигурация, а не устаревшая. Именно она подходит для управляемого PostgreSQL, где создание ролей часто вам недоступно.

Внешний и управляемый PostgreSQL

В RDS, Supabase, Cloud SQL или любом кластере, который вы обслуживаете сами, разделение включается по вашему выбору. Создайте рабочую роль один раз:

sql
CREATE ROLE snapotter_app LOGIN PASSWORD 'choose-a-strong-password'
  NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;

Затем передайте SnapOtter обе строки подключения, указывающие на один и тот же хост, порт и базу данных:

bash
DATABASE_URL=postgres://snapotter_app:[email protected]:5432/snapotter
DATABASE_MIGRATION_URL=postgres://snapotter:[email protected]:5432/snapotter

На этом остановитесь. SnapOtter сам выдаёт права и повторяет выдачу после каждой миграции, поэтому таблица, добавленная в будущем выпуске, будет охвачена без единой строки SQL с вашей стороны.

Роль в DATABASE_MIGRATION_URL должна владеть таблицами SnapOtter, потому что выдавать права на таблицу может только её владелец. На существующей установке это та роль, под которой вы всё это время запускали SnapOtter, а не новая, созданная специально для этого. Если указать новую роль, которая ничем не владеет, запуск завершится ошибкой ровно с таким сообщением. Ей также нужен CREATEROLE, чтобы создавать и поддерживать рабочую роль, и право создать схему drizzle.

Если в обоих URL указана одна и та же роль, разделение не действует, и SnapOtter прямо пишет об этом в журнал, а не делает вид, что всё в порядке. Если провайдер не даёт роли, которая одновременно владеет таблицами и имеет CREATEROLE, работайте с одной ролью.

Почему бит суперпользователя остаётся нетронутым

SnapOtter никогда не снимает SUPERUSER с роли самостоятельно. На установке, созданной до разделения, единственный суперпользователь кластера это snapotter, и понижение его прав оставило бы кластер вовсе без суперпользователя, а вернуть его удалось бы только через однопользовательский режим с остановленным сервером. Защиту вместо этого даёт перевод долгоживущего соединения на ограниченную роль. Суперпользователь присутствует на линии те несколько секунд, что длится запуск, и затем исчезает.

У новых установок из образа «всё в одном» такой проблемы нет. В них три роли: postgres (суперпользователь начальной настройки, которого нет ни в одной строке подключения SnapOtter), snapotter (NOSUPERUSER, владеет данными, подключается только при запуске) и snapotter_app (только строки, обслуживает запросы).

Если вы всё же хотите понизить права старой роли snapotter, сначала создайте второго суперпользователя и войдите под ним, чтобы убедиться, что он работает. Затем выполните ALTER ROLE snapotter NOSUPERUSER.

Резервное копирование и восстановление

Реляционная база данных находится в томе SnapOtter-pgdata контейнера Postgres, а не в томе /data приложения.

Логическая резервная копия с проверкой (рекомендуется)

bash
# Dump into PostgreSQL's portable custom archive format
docker exec SnapOtter-postgres \
  pg_dump --format=custom --no-owner -U snapotter snapotter > snapotter.dump
test -s snapotter.dump
docker exec -i SnapOtter-postgres pg_restore --list < snapotter.dump >/dev/null

# Restore into a fresh/disposable target first and fail on the first SQL error
docker exec -i SnapOtter-postgres \
  pg_restore --exit-on-error --clean --if-exists --no-owner \
  -U snapotter -d snapotter < snapotter.dump

Обе команды подключаются от имени владельца, роли snapotter, и так и должно оставаться. Рабочая роль не видит схему drizzle, поэтому дамп, снятый под ней, окажется неполным. --no-owner оставляет восстановленные объекты во владении того, кто запускает восстановление, поэтому запуск от имени владельца ставит владение туда, где его ожидают выданные права. Одна тонкость на чистом кластере: pg_dump переносит права, но не роли, которые в них названы, поэтому создайте snapotter_app до восстановления, иначе --exit-on-error остановится на первом GRANT. Права SnapOtter в любом случае выдаст заново при следующем запуске.

Этот дамп базы данных не содержит сохраненных объектов библиотеки в /data/files или устойчивого состояния BullMQ в Redis. Создайте резервные копии и восстановите их, выполнив согласованную процедуру в разделе Безопасность и усиление защиты.

Снимок холодного тома

bash
# Stop every service first, then use your storage platform to snapshot the
# PostgreSQL, app-data, and Redis volumes as one crash-consistent set.
docker compose -f docker/docker-compose.yml stop

Не копируйте действующий каталог данных PostgreSQL с помощью tar. Составляйте префиксы имен томов по проектам, поэтому разрешайте идентификаторы подключенных томов из docker inspect или вашей платформы хранения, а не принимайте буквальную метку SnapOtter-pgdata.

Миграция с 1.x (SQLite)

Обновление со SnapOtter 1.x описано в отдельном руководстве: см. Обновление с 1.x до 2.0. Вкратце: переиспользуйте существующий том /data, и 2.0 автоматически обнаружит и импортирует /data/snapotter.db при первом запуске (или задайте SQLITE_MIGRATE_PATH, чтобы явно указать путь к нему). Сначала сделайте резервную копию всего тома /data, а не только snapotter.db: 1.x использует режим SQLite WAL, поэтому остановленный контейнер часто оставляет большую часть данных в snapotter.db-wal рядом с почти пустым snapotter.db.