A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch.
7.7 KiB
description, i18n_source_hash, i18n_provenance, i18n_output_hash, i18n_hash_version
| description | i18n_source_hash | i18n_provenance | i18n_output_hash | i18n_hash_version |
|---|---|---|---|---|
| SnapOtter 的 PostgreSQL 数据库架构、表、迁移和备份流程。 | a68264552836 | machine | 127adc94ef4c | 2 |
数据库
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 自动创建。
| 列 | 类型 | 备注 |
|---|---|---|
id |
uuid | 主键 |
username |
varchar | 唯一,必填 |
passwordHash |
varchar | scrypt 哈希 |
role |
varchar | admin、editor 或 user |
mustChangePassword |
boolean | 强制重置密码标志 |
createdAt |
timestamp | 创建时间 |
updatedAt |
timestamp | 上次更新时间 |
sessions
活动登录会话。每一行将一个会话令牌与一个用户关联。
| 列 | 类型 | 备注 |
|---|---|---|
id |
varchar | 主键(会话令牌) |
userId |
uuid | 指向 users.id 的外键 |
expiresAt |
timestamp | 过期时间 |
createdAt |
timestamp | 创建时间 |
teams
用于组织用户的分组。管理员可以将用户分配到团队。
| 列 | 类型 | 描述 |
|---|---|---|
id |
uuid | 主键 |
name |
varchar(唯一,最多 50 个字符) | 团队名称 |
createdAt |
timestamp | 创建时间 |
api_keys
用于程序化访问的 API 密钥。原始密钥仅在创建时显示一次;仅存储其哈希。
| 列 | 类型 | 备注 |
|---|---|---|
id |
uuid | 主键 |
userId |
uuid | 指向 users.id 的外键 |
keyHash |
varchar | 密钥的 scrypt 哈希 |
name |
varchar | 用户提供的标签 |
createdAt |
timestamp | 创建时间 |
lastUsedAt |
timestamp | 每次经过身份验证的请求时更新 |
密钥以 si_ 为前缀,后跟 96 个十六进制字符(48 个随机字节)。
pipelines
用户在 UI 中创建的已保存工具链。
| 列 | 类型 | 备注 |
|---|---|---|
id |
uuid | 主键 |
name |
varchar | 流水线名称 |
description |
varchar | 可选描述 |
steps |
jsonb | { toolId, settings } 对象数组 |
createdAt |
timestamp | 创建时间 |
user_files
持久化文件库。默认情况下,保存的编辑会作为一个独立的根行插入(“保存为新文件”:version 为 1、parentId 为 null,因此原文件仍会保留在列表中);而当你覆盖原文件时,则作为一个与父行链接的版本(设置 parentId、递增 version,并取代原文件)。toolChain 列记录所应用的工具。
| 列 | 类型 | 描述 |
|---|---|---|
id |
uuid | 主键 |
userId |
uuid | 指向 users 的外键(CASCADE DELETE) |
originalName |
varchar | 原始上传文件名 |
storedName |
varchar | 磁盘上的文件名 |
mimeType |
varchar | MIME 类型 |
size |
integer | 文件大小(字节) |
width |
integer | 图像宽度(像素) |
height |
integer | 图像高度(像素) |
version |
integer | 版本号(1 = 原始版本) |
parentId |
uuid 或 null | 指向 user_files 的外键(父版本) |
toolChain |
jsonb | 按顺序应用以生成此版本的工具 ID |
createdAt |
timestamp | 创建时间 |
jobs
跟踪处理作业,用于进度报告和清理。
| 列 | 类型 | 备注 |
|---|---|---|
id |
uuid | 主键 |
type |
varchar | 工具或流水线标识符 |
status |
varchar | queued、processing、completed 或 failed |
progress |
real | 0.0-1.0 的分数 |
inputFiles |
jsonb | 输入文件路径数组 |
outputPath |
varchar | 结果文件的路径 |
settings |
jsonb | 所用的工具设置 |
error |
varchar | 失败时的错误消息 |
createdAt |
timestamp | 创建时间 |
completedAt |
timestamp | 完成时间 |
settings
用于存储全服务器范围设置的键值存储,管理员可从 UI 更改这些设置。
| 列 | 类型 | 备注 |
|---|---|---|
key |
varchar | 主键 |
value |
varchar | 设置值 |
updatedAt |
timestamp | 上次更新时间 |
roles
具有细粒度权限的自定义角色。
| 列 | 类型 | 备注 |
|---|---|---|
id |
uuid | 主键 |
name |
varchar | 唯一的角色名称 |
description |
varchar | 可选描述 |
permissions |
jsonb | 权限字符串数组 |
createdAt |
timestamp | 创建时间 |
audit_log
安全相关的操作日志。
| 列 | 类型 | 备注 |
|---|---|---|
id |
uuid | 主键 |
userId |
uuid | 指向 users 的外键 |
action |
varchar | 操作类型 |
details |
jsonb | 特定于操作的数据 |
createdAt |
timestamp | 操作时间 |
user_preferences
按偏好名称存储的每用户界面状态。首页的已固定工具通过 PUT /api/v1/preferences 写入这里。
| 列 | 类型 | 备注 |
|---|---|---|
userId |
text | 指向 users 的外键,级联删除。与 key 共同构成主键 |
key |
text | 偏好名称。与 userId 共同构成主键 |
value |
jsonb | 偏好内容 |
updatedAt |
timestamp | 最后写入时间 |
迁移
Drizzle 负责处理架构迁移。迁移文件位于 apps/api/drizzle/。开发期间:
cd apps/api
npx drizzle-kit generate # generate a migration from schema changes
npx drizzle-kit migrate # apply pending migrations
在生产环境中,待处理的迁移会在启动时自动应用。
备份和恢复
关系数据库位于 Postgres 容器的 SnapOtter-pgdata 卷中,而不是应用程序的 /data 卷中。
带验证的逻辑备份(推荐)
# 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
此数据库转储不包含以 /data/files 保存的库对象或 Redis 中的持久 BullMQ 状态。使用安全与强化 中的协调程序备份和恢复这些内容。
冷卷快照
# 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
不要使用 tar 复制实时 PostgreSQL 数据目录。按项目编写卷名称前缀,因此从 docker inspect 或您的存储平台解析已安装的卷 ID,而不是假设文字标签 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。