feat: add full snapshot downloads and safe manual restore

This commit is contained in:
2026-10-17 01:36:21 +08:00
parent c6615faa49
commit f77f3d77c0
12 changed files with 1318 additions and 7 deletions
+16 -1
View File
@@ -13,9 +13,19 @@
## 实例备份与快照管理
### 完整 home 快照恢复
`GET /api/instances/:id/snapshots` 的每项另含 `restorable:boolean`、`restore_reason:string`、可选 `source_version` 与 `source_image`。来源记录在独立的 `snapshot_metadata` 表中,不会从当前实例推断旧快照版本。已知镜像或版本不一致时不可恢复;来源未知时 `restorable:true` 但 `restore_reason` 为 `snapshot source image/version unknown; explicit acknowledgement required`,客户端必须显示风险警告并要求额外确认。保护状态及大小读取错误时不可恢复。列表只是预检提示,最终恢复还会检查实时配额、容器布局、镜像准入与读者占用。
`POST /api/instances/:id/snapshots` 接收 `{id,password,confirm:true,allow_unknown_version?:true}`(JSON 最大4096字节,在获取锁前读取)。`password` 是当前登录密码,不是旧备份密码;需正常登录、同源 Origin 与 CSRF。来源未知必须明确传入 `allow_unknown_version:true`;此开关不能绕过已知不兼容。成功返回 `200 {ok:true,snapshot_id:"pre-restore-<32hex>"}`,源快照保持不变。
恢复替换整个 home,不是仅覆盖配置。仅支持经核验的模板7独立 /workspace bind 挂载与当前实例镜像;拒绝旧共享布局、被阻止镜像、恢复保护状态及活跃文件读者。先进行配额和保留数量检查,再使用 rooted 路径验证复制到同文件系统暂存目录;外部/绝对/悬空符号链接及特殊文件拒绝。停止运行时后重新检查配额,将当前 home 重命名保留为 pre-restore 快照,再安装暂存目录并验证就绪;原来停止的实例会临时启动验证后再次停止。失败时独立超时上下文执行回滚,失败的新 home 保留为 failed-restore 快照;无法安全回滚时保持 error,要求管理员恢复。
非法请求或不安全内容400,当前密码错误403,目标不存在/所有权不符404,保护/读者占用/不兼容/缺少未知版本确认/保留上限409,请求或配额超限413,存储/数据库/回滚故障500,容器检查/停止失败502。正常恢复前会记录 snapshot.restore.started,完成或回滚分别记录 snapshot.restore.completed / snapshot.restore.rolled_back。此接口不改变实例镜像。
- `DELETE /api/instances/:id/backups?id=<32hex>` 删除当前用户实例的加密 `.dshbak` 包及其索引,记录 `backup.delete` 审计。ID 为32位小写十六进制;支持无索引的旧包,以及文件已不存在的残留索引。
- `GET /api/instances/:id/snapshots` 返回数组 `{id,size,created_at,protected,protection_reason,size_error?}`。`size` 为普通文件逻辑字节总和(与配额口径相同,硬链接逐路径计数,不跟随符号链接);扫描失败为 `-1` 并提供 `size_error`,不能显示为零。`created_at` 为目录修改时间的 RFC3339 字符串,不保证真实创建时间。未保护时 `protection_reason` 是空字符串。
- `DELETE /api/instances/:id/snapshots?id=<id>` 删除选定快照。先持久化 `snapshot.delete.started`,删除成功后记录 `snapshot.delete.completed`,删除失败则记录 `snapshot.delete.failed`;target 为实例 ID,detail 为快照 ID。初始审计失败时不会删除,终态审计失败返回500并保留 started 记录。不提供快照下载。
- `DELETE /api/instances/:id/snapshots?id=<id>` 删除选定快照。先持久化 `snapshot.delete.started`,删除成功后记录 `snapshot.delete.completed`,删除失败则记录 `snapshot.delete.failed`;target 为实例 ID,detail 为快照 ID。初始审计失败时不会删除,终态审计失败返回500并保留 started 记录。快照另支持 GET 下载,见下方接口清单及 OPERATIONS.md 的归档限制。
仅识别实例目录直属的 `pre-restore-<32hex>`、`failed-restore-<32hex>`、`upgrade-<32hex>-snapshot`、`failed-home-<32hex>` 真实目录;不列出或删除 `home`、其它命名或符号链接目录。删除成功返回 `200 {ok:true}`,非法ID/不安全路径400,不存在/所有权不符404,恢复保护409,存储/索引/审计错误500。不支持的方法405。删除仍要求同源 Origin 与 CSRF,不要求备份密码,不调用 Docker inspect/stop/restart。
@@ -141,6 +151,11 @@ GET /api/orphans 返回可回收的完整容器ID数组。DELETE /api/orphans?id
- POST /api/instances/{id}/backups multipart file,password(导入外部包,password为原包密码;不立即覆盖home)
- GET /api/instances/{id}/backups
- GET /api/instances/{id}/backups?download={backup_id}
- DELETE /api/instances/{id}/backups?id={backup_id} → {ok:true}
- GET /api/instances/{id}/snapshots → [{id,created_at,size,protected,protection_reason,restorable,restore_reason,source_version?,source_image?,size_error?}]
- DELETE /api/instances/{id}/snapshots?id={snapshot_id} → {ok:true}
- GET /api/instances/{id}/snapshots?download={snapshot_id} → 完整 home 的 `.tar.gz` 附件(未加密,可能含凭据)
- POST /api/instances/{id}/snapshots {id,password,confirm:true,allow_unknown_version?} → {ok:true,snapshot_id}(当前登录密码;snapshot_id 为切换前 home 的保留快照)
- POST /api/instances/{id}/restore {backup_id,password}(原备份密码;旧平台key包可留空)
- POST /api/instances/{id}/upgrade {image_id,password}(当前登录密码,加密pre-upgrade备份)