@whispering233/ai-editor-server 0.0.21 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client-dist/assets/index-0zEqGbv5.css +1 -0
- package/client-dist/assets/index-DMtHdMMq.js +521 -0
- package/client-dist/index.html +3 -3
- package/dist/backup.d.ts +78 -78
- package/dist/backup.d.ts.map +1 -1
- package/dist/backup.js +138 -139
- package/dist/backup.js.map +1 -1
- package/dist/consistency.d.ts +3 -3
- package/dist/consistency.d.ts.map +1 -1
- package/dist/consistency.js +4 -4
- package/dist/consistency.js.map +1 -1
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +7 -7
- package/dist/debug.js.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -37
- package/dist/index.js.map +1 -1
- package/dist/middleware/error.d.ts +4 -4
- package/dist/middleware/error.d.ts.map +1 -1
- package/dist/middleware/error.js +7 -9
- package/dist/middleware/error.js.map +1 -1
- package/dist/middleware/project.d.ts +9 -9
- package/dist/middleware/project.d.ts.map +1 -1
- package/dist/middleware/project.js +23 -23
- package/dist/middleware/project.js.map +1 -1
- package/dist/reference-files.d.ts +12 -12
- package/dist/reference-files.d.ts.map +1 -1
- package/dist/reference-files.js +20 -21
- package/dist/reference-files.js.map +1 -1
- package/dist/routes/chat.d.ts +18 -18
- package/dist/routes/chat.d.ts.map +1 -1
- package/dist/routes/chat.js +70 -83
- package/dist/routes/chat.js.map +1 -1
- package/dist/routes/delta.d.ts.map +1 -1
- package/dist/routes/delta.js +27 -28
- package/dist/routes/delta.js.map +1 -1
- package/dist/routes/entity.d.ts.map +1 -1
- package/dist/routes/entity.js +44 -45
- package/dist/routes/entity.js.map +1 -1
- package/dist/routes/names.d.ts.map +1 -1
- package/dist/routes/names.js +8 -9
- package/dist/routes/names.js.map +1 -1
- package/dist/routes/outline.d.ts +1 -1
- package/dist/routes/outline.d.ts.map +1 -1
- package/dist/routes/outline.js +23 -24
- package/dist/routes/outline.js.map +1 -1
- package/dist/routes/project.d.ts +6 -6
- package/dist/routes/project.d.ts.map +1 -1
- package/dist/routes/project.js +91 -95
- package/dist/routes/project.js.map +1 -1
- package/dist/routes/proposal.d.ts.map +1 -1
- package/dist/routes/proposal.js +40 -42
- package/dist/routes/proposal.js.map +1 -1
- package/dist/routes/reference.d.ts.map +1 -1
- package/dist/routes/reference.js +1 -2
- package/dist/routes/reference.js.map +1 -1
- package/dist/routes/relation.d.ts.map +1 -1
- package/dist/routes/relation.js +14 -15
- package/dist/routes/relation.js.map +1 -1
- package/dist/routes/settings.d.ts +35 -17
- package/dist/routes/settings.d.ts.map +1 -1
- package/dist/routes/settings.js +157 -52
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/trash.d.ts.map +1 -1
- package/dist/routes/trash.js +15 -16
- package/dist/routes/trash.js.map +1 -1
- package/package.json +7 -7
- package/client-dist/assets/index-B51Kcb1K.css +0 -1
- package/client-dist/assets/index-zgl3whdc.js +0 -104
package/dist/backup.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
// 自动备份与恢复(B2.2
|
|
1
|
+
// 自动备份与恢复(B2.2 + B2.5 + B2.6 )
|
|
2
2
|
//
|
|
3
|
-
// 单一事实来源:doc/design/decisions.md 决策 27/28/29、doc/api/endpoints.md「备份管理」节。
|
|
4
3
|
// 职责:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
4
|
+
// - 备份管道:三文件 + wal_checkpoint → .backups/<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip
|
|
5
|
+
// (复用导出打包;毫秒精度;kind 段:手动备份落 -m[-<名称>]、
|
|
6
|
+
// 自动备份/覆盖前快照纯时间戳、重命名后自动备份落 -a-<名称>)
|
|
7
|
+
// - 保留策略:每项目保留最近 MAX_BACKUPS_PER_PROJECT 份,超出删除最旧(含覆盖前快照;
|
|
8
|
+
// 新旧格式文件名均参与——parseBackupFileName 兼容解析)
|
|
9
|
+
// - 自动定时器:跟随当前项目生命周期(middleware/project.ts setCurrentProject 挂载启停),
|
|
10
|
+
// 有变更才备份(三文件 + data.db-wal 伴生文件 mtime 与 .backups/ 最新备份时间比较,
|
|
11
|
+
// 无状态、服务重启不丢;F2:WAL 模式写只刷新 -wal,补查避免漏检 data.db 变更)
|
|
12
|
+
// - 备份包校验(restore 与 import 共用):zip 解析/白名单/三文件齐全/顶层/
|
|
13
|
+
// data.db user_version 三态分流(绝不静默重建)
|
|
14
|
+
// - restore:fileName 白名单 → 覆盖前自动快照 → 校验 → 原子替换三文件
|
|
15
|
+
// - rename:只改名称段(时间戳与 kind 保持)
|
|
17
16
|
//
|
|
18
17
|
// 依赖方向:本模块不依赖 middleware(避免循环依赖——定时器持有所调度项目引用,
|
|
19
18
|
// 由 setCurrentProject 显式启停);仅 import type ProjectContext(类型擦除)。
|
|
@@ -25,11 +24,11 @@ import { BACKUP_FREQUENCIES, DEFAULT_BACKUP_FREQUENCY_MINUTES, formatBackupFileN
|
|
|
25
24
|
import { PROJECT_EXPORT_FILE_NAMES } from "@whispering233/ai-editor-shared/schemas";
|
|
26
25
|
import { closeDatabase, checkpointWal, DATA_DB_FILE_NAME, ensureSchemaCompatible, getUserVersion, hasMigrationPath, migrateChatMessagesProject, openDatabase, OUTLINE_FILE_NAME, PROJECT_FILE_NAME, readAgentsFile, readProjectFile, SCHEMA_VERSION, writeAgentsFile, } from "@whispering233/ai-editor-db";
|
|
27
26
|
import { HttpError } from "./middleware/error.js";
|
|
28
|
-
/**
|
|
27
|
+
/** 备份目录名(项目目录内 .backups/,随书籍移动自然携带) */
|
|
29
28
|
export const BACKUPS_DIR_NAME = ".backups";
|
|
30
|
-
/**
|
|
29
|
+
/** 参考资料目录名(项目目录内 references/,随备份 zip 打包) */
|
|
31
30
|
const REFERENCE_DIR_NAME = "references";
|
|
32
|
-
/**
|
|
31
|
+
/** 备份包条目白名单判定(三文件 + references/ 前缀——逐名比对天然防 zip 路径穿越;
|
|
33
32
|
* references/ 子路径拒绝含 `..` 的条目防相对路径逃逸) */
|
|
34
33
|
export function isAllowedBackupEntry(name) {
|
|
35
34
|
if (PROJECT_EXPORT_FILE_NAMES.includes(name))
|
|
@@ -59,7 +58,7 @@ function listReferenceDirFiles(root) {
|
|
|
59
58
|
const MAX_UNZIP_BUDGET = 200 * 1024 * 1024;
|
|
60
59
|
/**
|
|
61
60
|
* mtime 变更判定容差(毫秒):备份管道内 wal_checkpoint 会把 WAL 合并回 data.db 主文件,
|
|
62
|
-
* 其 mtime
|
|
61
|
+
* 其 mtime 刷新到「备份时刻」(毫秒精度)。 起文件名时间戳为毫秒精度,
|
|
63
62
|
* 文件名截断误差已消除——但容差**保留 1s**:粗粒度 mtime 文件系统(如 FAT/exFAT 2s 粒度)
|
|
64
63
|
* 下,严格 `mtime > lastBackupAt` 仍可能误判,1s 容差是必要防御;最小 tick 5 分钟,
|
|
65
64
|
* 用户变更必然超出容差窗口。
|
|
@@ -80,12 +79,12 @@ function assertBackupFileNameFormat(fileName) {
|
|
|
80
79
|
}
|
|
81
80
|
return parsed;
|
|
82
81
|
}
|
|
83
|
-
// ============ 备份管道(复用
|
|
82
|
+
// ============ 备份管道(复用 打包,避免复制) ============
|
|
84
83
|
/**
|
|
85
|
-
* 打包当前项目为 zip(
|
|
84
|
+
* 打包当前项目为 zip( export 同款管道):
|
|
86
85
|
* wal_checkpoint(TRUNCATE) 把 WAL 合并回主文件(zip 内 data.db 为完整快照,
|
|
87
86
|
* 无需附带 -wal/-shm)→ zipSync 打包(键序稳定:project.json → outline.json → data.db
|
|
88
|
-
* → references
|
|
87
|
+
* → references/**(参考资料目录含 .trash/ 随包——项目自包含))。
|
|
89
88
|
* 三文件缺失任一 → 抛错(打开的项目三文件必然齐全,缺失即损坏,不导出半成品包)。
|
|
90
89
|
*/
|
|
91
90
|
export function createBackupZip(project) {
|
|
@@ -96,23 +95,23 @@ export function createBackupZip(project) {
|
|
|
96
95
|
[OUTLINE_FILE_NAME]: readFileSync(join(dir, OUTLINE_FILE_NAME)),
|
|
97
96
|
[DATA_DB_FILE_NAME]: readFileSync(join(dir, DATA_DB_FILE_NAME)),
|
|
98
97
|
};
|
|
99
|
-
//
|
|
98
|
+
// references/ 目录随包(存在则递归打包;不存在跳过——旧项目无目录不报错)
|
|
100
99
|
for (const rel of listReferenceDirFiles(dir)) {
|
|
101
100
|
zipEntries[`${REFERENCE_DIR_NAME}/${rel}`] = readFileSync(join(dir, REFERENCE_DIR_NAME, rel));
|
|
102
101
|
}
|
|
103
102
|
return zipSync(zipEntries, { level: 6 });
|
|
104
103
|
}
|
|
105
104
|
/**
|
|
106
|
-
* 生成不冲突的备份文件名:`<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip
|
|
107
|
-
*
|
|
105
|
+
* 生成不冲突的备份文件名:`<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip`( 毫秒精度 +
|
|
106
|
+
* kind 段;shared formatBackupFileName;kind 缺省 auto——纯时间戳;带自定义名称 →
|
|
108
107
|
* `-a-<名称>`(auto)/ `-m-<名称>`(manual))。
|
|
109
108
|
*
|
|
110
109
|
* 同毫秒冲突(如「立即备份 + restore 覆盖前快照」连续触发,理论罕见)处理:
|
|
111
|
-
* 时间戳 +1 毫秒循环去重,**保持 <YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip
|
|
110
|
+
* 时间戳 +1 毫秒循环去重,**保持 <YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip 格式**——
|
|
112
111
|
* parseBackupFileName 解析与 restore 白名单校验不受影响。
|
|
113
112
|
*
|
|
114
113
|
* @returns { fileName, date }——date 为最终去重后的时间戳(与 fileName 时间戳段一致,
|
|
115
|
-
*
|
|
114
|
+
* 调用方直接用于构造 createdAt,免 parse 回读)
|
|
116
115
|
*/
|
|
117
116
|
function uniqueBackupFileName(backupsDir, date, opts) {
|
|
118
117
|
const kind = opts?.kind ?? "auto";
|
|
@@ -128,14 +127,14 @@ function uniqueBackupFileName(backupsDir, date, opts) {
|
|
|
128
127
|
* 立即备份当前项目(手动触发 / 自动定时器 / restore 覆盖前快照共用):
|
|
129
128
|
* 打包 → 写入 .backups/<时间戳>[-<kind>][-<名称>].zip(毫秒精度;同毫秒 +1ms 去重)→
|
|
130
129
|
* 触发保留策略清理(失败不阻塞)。
|
|
131
|
-
* 写盘失败向上抛(errorHandler → 500 INTERNAL_ERROR,
|
|
130
|
+
* 写盘失败向上抛(errorHandler → 500 INTERNAL_ERROR, POST /backup 语义)。
|
|
132
131
|
*
|
|
133
|
-
* @param opts.kind
|
|
134
|
-
*
|
|
135
|
-
* @param opts.name
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
132
|
+
* @param opts.kind 备份类型(缺省 "auto"):手动触发传 "manual"(文件名落 -m 段);
|
|
133
|
+
* 自动备份/覆盖前快照不传(auto,纯时间戳)
|
|
134
|
+
* @param opts.name 手动备份自定义名称(仅带名称的手动/重命名场景传入):
|
|
135
|
+
* sanitizeBackupName 是名称校验/规范化**唯一执行点**——非法(含路径分隔符/超长/纯点)→
|
|
136
|
+
* 400 VALIDATION_ERROR;自动备份/覆盖前快照不传 name,文件名保持纯时间戳。
|
|
137
|
+
* 注意:name 仅在 opts.name !== undefined 且 sanitize 通过后传入 formatBackupFileName。
|
|
139
138
|
*/
|
|
140
139
|
export function writeBackup(project, opts) {
|
|
141
140
|
const kind = opts?.kind ?? "auto";
|
|
@@ -156,43 +155,43 @@ export function writeBackup(project, opts) {
|
|
|
156
155
|
return {
|
|
157
156
|
fileName,
|
|
158
157
|
size: zip.length,
|
|
159
|
-
createdAt: toIso(date), // date = 最终去重后的时间戳,与 fileName
|
|
158
|
+
createdAt: toIso(date), // date = 最终去重后的时间戳,与 fileName 时间戳段一致( 无状态语义)
|
|
160
159
|
kind,
|
|
161
160
|
...(name !== undefined ? { name } : {}),
|
|
162
161
|
};
|
|
163
162
|
}
|
|
164
163
|
/**
|
|
165
|
-
* 重命名备份(POST /project/backup/rename
|
|
164
|
+
* 重命名备份(POST /project/backup/rename):**只改名称段**,时间戳与 kind 保持。
|
|
165
|
+
*
|
|
166
166
|
*
|
|
167
|
-
* 契约:
|
|
168
167
|
* 1. fileName 白名单校验:仅接受 parseBackupFileName 可解析的时间戳格式
|
|
169
|
-
*
|
|
170
|
-
*
|
|
168
|
+
* `<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip`(含旧格式兼容,^$ 锚定 + 名称部分拒绝 /\\,
|
|
169
|
+
* 天然防路径分隔符与 `..` 穿越)→ 非法 400 VALIDATION_ERROR
|
|
171
170
|
* 2. 文件不存在 → 404 VALIDATION_ERROR
|
|
172
|
-
* 3.
|
|
173
|
-
*
|
|
171
|
+
* 3. 新名称解析:请求未传 name → 清除名称段;传空串/纯空白 → 清除名称段;
|
|
172
|
+
* 非空 → sanitizeBackupName 规范化(非法 → 400 VALIDATION_ERROR,文案同 writeBackup)
|
|
174
173
|
* 4. 新文件名 = formatBackupFileName(parsed.time, { kind: parsed.kind, name: 新名称 })——
|
|
175
|
-
*
|
|
174
|
+
* kind 不随重命名改变(auto 重命名后仍落 -a- 段、manual 仍落 -m- 段)
|
|
176
175
|
* 5. 幂等:新文件名 === 原文件名(如重命名为相同名称)→ 不移动文件,直接返回当前条目
|
|
177
|
-
*
|
|
178
|
-
* 6. **目标冲突防御(oracle P1-1
|
|
179
|
-
*
|
|
180
|
-
*
|
|
176
|
+
* (重新 stat 取 size;stat 失败 → 404「备份不存在」)
|
|
177
|
+
* 6. **目标冲突防御(oracle P1-1)**:新文件名已存在(≠ 原文件)→ 409
|
|
178
|
+
* BACKUP_TARGET_EXISTS——POSIX rename 目标存在时静默替换,可达路径:旧秒级改名后毫秒补
|
|
179
|
+
* 000 撞上毫秒为 0 的自动备份、同毫秒双 manual(T-m.zip + T-m-X.zip)清名覆盖;显式拒绝
|
|
181
180
|
* 7. renameSync 同目录原子改名(失败向上抛 → 500);改名后统一 stat 取 size
|
|
182
|
-
*
|
|
181
|
+
* (改名瞬间被删等竞态 → 404「备份不存在」)
|
|
183
182
|
*
|
|
184
183
|
* @throws HttpError 400(文件名非法/名称非法)、404(备份不存在)、409 BACKUP_TARGET_EXISTS
|
|
185
184
|
*/
|
|
186
185
|
export function renameBackup(project, fileName, name) {
|
|
187
186
|
// 1. 白名单校验(assertBackupFileNameFormat:parseBackupFileName 全格式校验,防路径穿越;
|
|
188
|
-
//
|
|
187
|
+
// 非法 → 400;返回解析结果直接取 time/kind)
|
|
189
188
|
const parsed = assertBackupFileNameFormat(fileName);
|
|
190
189
|
const backupsDir = join(project.root, BACKUPS_DIR_NAME);
|
|
191
190
|
if (!existsSync(join(backupsDir, fileName))) {
|
|
192
191
|
throw new HttpError(404, "VALIDATION_ERROR", `备份不存在: ${fileName}`);
|
|
193
192
|
}
|
|
194
193
|
// 2. 新名称解析:请求未传 name → 清除名称;传空串/纯空白 → 清除名称(sanitize null 且 trim 空);
|
|
195
|
-
//
|
|
194
|
+
// 非空 → sanitize 规范化(null 且 trim 非空 → 400,文案同 writeBackup)
|
|
196
195
|
let nextName;
|
|
197
196
|
if (name !== undefined) {
|
|
198
197
|
const sanitized = sanitizeBackupName(name);
|
|
@@ -202,7 +201,7 @@ export function renameBackup(project, fileName, name) {
|
|
|
202
201
|
if (sanitized !== null)
|
|
203
202
|
nextName = sanitized;
|
|
204
203
|
}
|
|
205
|
-
// 3. 新文件名(时间戳与 kind 保持原备份——kind
|
|
204
|
+
// 3. 新文件名(时间戳与 kind 保持原备份——kind 不随重命名改变)
|
|
206
205
|
const newFileName = formatBackupFileName(parsed.time, { kind: parsed.kind, name: nextName });
|
|
207
206
|
// 4. 幂等:名称未变 → 不移动文件,返回当前条目(重新 stat 取 size;读不到 → 404)
|
|
208
207
|
if (newFileName === fileName) {
|
|
@@ -219,9 +218,9 @@ export function renameBackup(project, fileName, name) {
|
|
|
219
218
|
throw new HttpError(404, "VALIDATION_ERROR", `备份不存在: ${fileName}`);
|
|
220
219
|
}
|
|
221
220
|
}
|
|
222
|
-
// 5. 目标冲突防御(oracle P1-1
|
|
223
|
-
//
|
|
224
|
-
//
|
|
221
|
+
// 5. 目标冲突防御(oracle P1-1):POSIX rename 目标存在时静默替换(数据丢失风险)——
|
|
222
|
+
// 可达路径:旧秒级改名后毫秒补 000 撞上毫秒为 0 的自动备份、同毫秒双 manual 清名覆盖等;
|
|
223
|
+
// 显式 409 拒绝。幂等分支(target === fileName)已在第 4 步提前返回,此处必然 target ≠ fileName。
|
|
225
224
|
if (existsSync(join(backupsDir, newFileName))) {
|
|
226
225
|
throw new HttpError(409, "BACKUP_TARGET_EXISTS", `目标备份文件名已存在: ${newFileName}`);
|
|
227
226
|
}
|
|
@@ -244,9 +243,9 @@ export function renameBackup(project, fileName, name) {
|
|
|
244
243
|
};
|
|
245
244
|
}
|
|
246
245
|
/**
|
|
247
|
-
*
|
|
246
|
+
* 保留策略:每项目保留最近 MAX_BACKUPS_PER_PROJECT 份,超出删除最旧。
|
|
248
247
|
* 排序依据 = 文件名时间戳(可解析的才参与;非法文件名不参与保留判定)。
|
|
249
|
-
*
|
|
248
|
+
* 清理失败不阻塞调用方(记日志即可,「清理失败不阻塞备份主流程」)。
|
|
250
249
|
*/
|
|
251
250
|
export function pruneBackups(backupsDir) {
|
|
252
251
|
let files;
|
|
@@ -319,7 +318,7 @@ function concatChunks(chunks, totalSize) {
|
|
|
319
318
|
return out;
|
|
320
319
|
}
|
|
321
320
|
/**
|
|
322
|
-
* 带预算的 zip 解压(ora-4 zip 炸弹防御,自
|
|
321
|
+
* 带预算的 zip 解压(ora-4 zip 炸弹防御,自 import 提取):
|
|
323
322
|
* fflate Unzip 流式解压(同步 push)——onfile 回调中**只解压白名单条目**(未知条目
|
|
324
323
|
* 仅记名不 start,省预算;白名单检查由调用方用 names 列表执行),ondata 累计解压总字节,
|
|
325
324
|
* 超过预算抛 HttpError 中止。返回 { entries(白名单条目解压结果), names(全部条目名)}。
|
|
@@ -331,7 +330,7 @@ function unzipWithBudget(zipData, budget) {
|
|
|
331
330
|
const unzipper = new Unzip((file) => {
|
|
332
331
|
names.push(file.name);
|
|
333
332
|
if (!isAllowedBackupEntry(file.name))
|
|
334
|
-
return; // 未知条目不解压(白名单 = 三文件 + references
|
|
333
|
+
return; // 未知条目不解压(白名单 = 三文件 + references/)
|
|
335
334
|
const chunks = [];
|
|
336
335
|
let size = 0;
|
|
337
336
|
file.ondata = (err, chunk, final) => {
|
|
@@ -348,8 +347,8 @@ function unzipWithBudget(zipData, budget) {
|
|
|
348
347
|
};
|
|
349
348
|
file.start();
|
|
350
349
|
});
|
|
351
|
-
// 关键(fflate API
|
|
352
|
-
// zip(zipSync 默认)必须显式 register(UnzipInflate),否则 start
|
|
350
|
+
// 关键(fflate API ):Unzip 默认仅注册 stored(0) 解码器——deflate(8) 压缩的
|
|
351
|
+
// zip(zipSync 默认)必须显式 register(UnzipInflate),否则 start 报 unknown
|
|
353
352
|
// compression type(fflate README 明确要求)
|
|
354
353
|
unzipper.register(UnzipInflate);
|
|
355
354
|
unzipper.push(zipData, true);
|
|
@@ -360,14 +359,14 @@ function unzipWithBudget(zipData, budget) {
|
|
|
360
359
|
}
|
|
361
360
|
return { entries, names };
|
|
362
361
|
}
|
|
363
|
-
/** project.json
|
|
362
|
+
/** project.json 顶层最小校验(shared 无文件形态 schema,不扩范围) */
|
|
364
363
|
function isValidProjectFile(parsed) {
|
|
365
364
|
if (typeof parsed !== "object" || parsed === null)
|
|
366
365
|
return false;
|
|
367
366
|
const p = parsed;
|
|
368
367
|
return typeof p.id === "string" && p.id.length > 0 && typeof p.name === "string" && typeof p.schema_version === "number";
|
|
369
368
|
}
|
|
370
|
-
/** outline.json
|
|
369
|
+
/** outline.json 顶层校验(与 db 包 validateOutlineFile 同构:root 根 + schema_version 数字 + children 数组) */
|
|
371
370
|
function isValidOutlineFile(parsed) {
|
|
372
371
|
if (typeof parsed !== "object" || parsed === null)
|
|
373
372
|
return false;
|
|
@@ -375,26 +374,26 @@ function isValidOutlineFile(parsed) {
|
|
|
375
374
|
return p.id === "root" && p.type === "root" && typeof p.schema_version === "number" && Array.isArray(p.children);
|
|
376
375
|
}
|
|
377
376
|
/**
|
|
378
|
-
* 备份包完整校验(自
|
|
377
|
+
* 备份包完整校验(自 import 校验顺序 3-7 提取,restore 与 import 共用):
|
|
379
378
|
*
|
|
380
379
|
* 1. zip 解析(流式 + 解压总字节预算 200MB,zip 炸弹防御;失败 400「不是有效的项目备份包」)
|
|
381
380
|
* 2. 条目白名单(只接受 PROJECT_EXPORT_FILE_NAMES 三文件名,逐名比对天然防 zip 路径穿越)
|
|
382
381
|
* 3. 三文件齐全
|
|
383
|
-
* 4. 临时目录写入 → project.json / outline.json
|
|
382
|
+
* 4. 临时目录写入 → project.json / outline.json 顶层(JSON 损坏 / 不符 → 400)
|
|
384
383
|
* 5. data.db 校验(**大小 > 0 → 打开成功 → user_version**):
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
384
|
+
* - v === SCHEMA_VERSION → 接受(现状)
|
|
385
|
+
* - v < SCHEMA_VERSION → **有迁移路径** → 接受(open/restore 后自动前向迁移);
|
|
386
|
+
* **无迁移路径** → 409(文案标注版本过旧无路径)
|
|
387
|
+
* - v > SCHEMA_VERSION → 409(备份来自更高版本程序)
|
|
388
|
+
* 拒绝均不静默重建——恢复备份须明示版本不兼容。
|
|
390
389
|
*
|
|
391
390
|
* 校验在 mkdtemp 临时目录完成(finally 清理,无论成败);通过后返回解压内容与
|
|
392
391
|
* project_id(调用方负责搬入/替换)。**校验通过前不触碰目标项目任何数据**(零触碰)。
|
|
393
392
|
*
|
|
394
|
-
* @throws HttpError 400
|
|
393
|
+
* @throws HttpError 400(坏包/缺文件/未知条目/不符)/ 409 SCHEMA_VERSION_MISMATCH
|
|
395
394
|
*/
|
|
396
395
|
export function validateBackupPackage(zipData) {
|
|
397
|
-
// 1/2. 解压 + 白名单(严格拒绝):三数据文件名 + references/
|
|
396
|
+
// 1/2. 解压 + 白名单(严格拒绝):三数据文件名 + references/ 前缀
|
|
398
397
|
let entries;
|
|
399
398
|
let entryNames;
|
|
400
399
|
try {
|
|
@@ -421,7 +420,7 @@ export function validateBackupPackage(zipData) {
|
|
|
421
420
|
for (const f of PROJECT_EXPORT_FILE_NAMES) {
|
|
422
421
|
writeFileSync(join(tmpDir, f), entries[f]);
|
|
423
422
|
}
|
|
424
|
-
// project.json
|
|
423
|
+
// project.json 顶层(JSON 可解析 + id/name/schema_version)
|
|
425
424
|
let projectConfig;
|
|
426
425
|
try {
|
|
427
426
|
projectConfig = JSON.parse(readFileSync(join(tmpDir, PROJECT_FILE_NAME), "utf8"));
|
|
@@ -430,13 +429,13 @@ export function validateBackupPackage(zipData) {
|
|
|
430
429
|
throw new HttpError(400, "VALIDATION_ERROR", "备份包内 project.json 不是合法 JSON");
|
|
431
430
|
}
|
|
432
431
|
if (!isValidProjectFile(projectConfig)) {
|
|
433
|
-
throw new HttpError(400, "VALIDATION_ERROR", "备份包内 project.json
|
|
432
|
+
throw new HttpError(400, "VALIDATION_ERROR", "备份包内 project.json 顶层不符(需 id/name/schema_version)");
|
|
434
433
|
}
|
|
435
|
-
// outline.json
|
|
434
|
+
// outline.json 顶层({id:"root",type:"root",schema_version,children[]})
|
|
436
435
|
try {
|
|
437
436
|
const outline = JSON.parse(readFileSync(join(tmpDir, OUTLINE_FILE_NAME), "utf8"));
|
|
438
437
|
if (!isValidOutlineFile(outline)) {
|
|
439
|
-
throw new HttpError(400, "VALIDATION_ERROR", "备份包内 outline.json
|
|
438
|
+
throw new HttpError(400, "VALIDATION_ERROR", "备份包内 outline.json 顶层不符(需 {id:root,type:root,schema_version,children[]})");
|
|
440
439
|
}
|
|
441
440
|
}
|
|
442
441
|
catch (err) {
|
|
@@ -447,11 +446,11 @@ export function validateBackupPackage(zipData) {
|
|
|
447
446
|
// data.db 校验顺序(ora-4):**文件大小 > 0 → 打开成功 → user_version**——
|
|
448
447
|
// 0 字节文件 SQLite 会当新库打开(user_version=0),必须先按坏包拒绝(400 而非 409);
|
|
449
448
|
// 非 SQLite 内容打开失败 → 400 坏包。
|
|
450
|
-
// user_version 判定(
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
449
|
+
// user_version 判定( 卡规格追加句):
|
|
450
|
+
// v === SCHEMA_VERSION → 接受(现状)
|
|
451
|
+
// v < SCHEMA_VERSION → **有迁移路径**(MIGRATIONS 存在连续链,替换后 open 时自动前向
|
|
452
|
+
// 迁移,数据保全完整)→ 接受;**无迁移路径** → 409(文案标注版本过旧无路径)
|
|
453
|
+
// v > SCHEMA_VERSION → 409(备份来自更高版本程序)
|
|
455
454
|
const dbPath = join(tmpDir, DATA_DB_FILE_NAME);
|
|
456
455
|
if (statSync(dbPath).size === 0) {
|
|
457
456
|
throw new HttpError(400, "VALIDATION_ERROR", "备份包内 data.db 为空文件(损坏包)");
|
|
@@ -481,7 +480,7 @@ export function validateBackupPackage(zipData) {
|
|
|
481
480
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
482
481
|
}
|
|
483
482
|
}
|
|
484
|
-
// ============
|
|
483
|
+
// ============ 自动定时器(有变更才备份,跟随项目生命周期) ============
|
|
485
484
|
/** 备份频率读侧语义(与 shared 读映射一致 + B2.1 疑问裁决 2):缺失 → 缺省 10;null/0/非枚举 → 关闭(null) */
|
|
486
485
|
function resolveBackupFrequency(value) {
|
|
487
486
|
if (value === undefined)
|
|
@@ -508,7 +507,7 @@ function latestBackupTime(backupsDir) {
|
|
|
508
507
|
return latest;
|
|
509
508
|
}
|
|
510
509
|
/**
|
|
511
|
-
* 三文件 + data.db-wal 伴生文件是否在 since
|
|
510
|
+
* 三文件 + data.db-wal 伴生文件是否在 since 之后有变更(「任一 mtime 晚于
|
|
512
511
|
* 上次备份时刻」;F2 修订:data.db 以 WAL 模式运行,普通写事务只追加 -wal 伴生文件、
|
|
513
512
|
* 主文件 mtime 不变,故 data.db-wal 一并纳入判定):
|
|
514
513
|
* 任一文件 mtime > since + 容差 → 有变更;**主文件缺失 → 视为有变更**(防御:不静默
|
|
@@ -539,7 +538,7 @@ function hasFileChangesSince(project, since) {
|
|
|
539
538
|
if (err.code !== "ENOENT")
|
|
540
539
|
return true;
|
|
541
540
|
}
|
|
542
|
-
// references/
|
|
541
|
+
// references/ 目录:内文件(含 .trash/)任一 mtime 晚于 limit → 有变更——
|
|
543
542
|
// 本地新增/外部编辑 md 文档同样触发自动备份;目录缺失 = 无文件跳过;
|
|
544
543
|
// 遍历竞态(读取中删除)→ 防御视为无变更(下一 tick 重检)
|
|
545
544
|
try {
|
|
@@ -556,11 +555,11 @@ function hasFileChangesSince(project, since) {
|
|
|
556
555
|
/**
|
|
557
556
|
* 自动备份单次检查(定时器 tick 核心,纯同步、可单测):
|
|
558
557
|
*
|
|
559
|
-
* 1. 频率判定:关闭(null/0/非枚举)→ 直接返回 false
|
|
560
|
-
*
|
|
558
|
+
* 1. 频率判定:关闭(null/0/非枚举)→ 直接返回 false(null/0 = 关闭;
|
|
559
|
+
* B2.1 疑问裁决 2:读侧非枚举值按关闭处理,不开启自动备份)
|
|
561
560
|
* 2. 变更判定:`.backups/` 为空(无上次备份时刻)→ 需要备份;否则三文件 +
|
|
562
|
-
*
|
|
563
|
-
*
|
|
561
|
+
* data.db-wal 伴生文件(F2:WAL 模式写只刷新 -wal)mtime 均早于
|
|
562
|
+
* 「上次备份时刻 + 容差」→ 跳过(无变更不产生垃圾备份)
|
|
564
563
|
* 3. 有变更 → writeBackup(含保留策略清理)
|
|
565
564
|
*
|
|
566
565
|
* @returns 本次是否生成了备份
|
|
@@ -618,11 +617,11 @@ function scheduleNext(project) {
|
|
|
618
617
|
/**
|
|
619
618
|
* 用备份包内容替换目录三文件(restore 与 import 分流共用,B2.3 提取):
|
|
620
619
|
*
|
|
621
|
-
* - project.json:JSON 解析后按 opts 覆盖——`keepId
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
620
|
+
* - project.json:JSON 解析后按 opts 覆盖——`keepId`(覆盖恢复以 project_id 为
|
|
621
|
+
* 唯一 key,换 id 即断连 chat_messages 会话历史——;备份包可能来自异项目,
|
|
622
|
+
* 防御性强制保留)与 `name`(「目录名 = 书名」不变式:import 覆盖归一为目录名、
|
|
623
|
+
* 新书导入同步为去重名);其余字段随备份替换。序列化走 2 空格缩进 + 尾换行
|
|
624
|
+
* (与 db 包 writeJsonAtomic 同款格式惯例)
|
|
626
625
|
* - outline.json / data.db:原样字节原子写
|
|
627
626
|
*
|
|
628
627
|
* 注意:本函数只管文件层,不含 data.db 连接管理(重连见 overwriteProjectFiles)。
|
|
@@ -645,7 +644,7 @@ export function writeProjectFilesFromBackup(dir, entries, opts = {}) {
|
|
|
645
644
|
replaced.push(OUTLINE_FILE_NAME);
|
|
646
645
|
writeFileAtomic(join(dir, DATA_DB_FILE_NAME), entries[DATA_DB_FILE_NAME]);
|
|
647
646
|
replaced.push(DATA_DB_FILE_NAME);
|
|
648
|
-
//
|
|
647
|
+
// references/ 目录整体覆盖(清空现有 → 写回备份条目;含 .trash/)——
|
|
649
648
|
// 恢复/导入是「整体还原」语义,目录以备份内容为准,本地残留不混入
|
|
650
649
|
const refDir = join(dir, REFERENCE_DIR_NAME);
|
|
651
650
|
if (existsSync(refDir))
|
|
@@ -669,15 +668,15 @@ export function writeProjectFilesFromBackup(dir, entries, opts = {}) {
|
|
|
669
668
|
}
|
|
670
669
|
}
|
|
671
670
|
/**
|
|
672
|
-
*
|
|
673
|
-
* 将 prompt 内容**原样**写入
|
|
674
|
-
* 迁移后
|
|
671
|
+
* 自动迁移:project.json 存在非空 `prompt` 且项目目录无 →
|
|
672
|
+
* 将 prompt 内容**原样**写入 (原子写, 同款),一次性——
|
|
673
|
+
* 迁移后 存在,条件不再满足,prompt 不再使用(字段可保留为遗留数据,宽松读取)。
|
|
675
674
|
*
|
|
676
675
|
* 触发点:打开项目时(detectProject 启动即打开 + open 路由)+ restore/import 覆盖路径
|
|
677
|
-
* (overwriteProjectFiles 同步 config 后——向无
|
|
678
|
-
* 内存 config 有 prompt 但
|
|
679
|
-
* open 前不注入)。**幂等**(prompt 非空且无
|
|
680
|
-
* 下次 open
|
|
676
|
+
* (overwriteProjectFiles 同步 config 后——向无 的项目恢复含遗留 prompt 的旧备份时,
|
|
677
|
+
* 内存 config 有 prompt 但 未创建,chat 读 为 null,恢复的 prompt 在下次
|
|
678
|
+
* open 前不注入)。**幂等**(prompt 非空且无 才迁移),**失败不阻塞**(记录日志,
|
|
679
|
+
* 下次 open 重试——「迁移在 open 流程内完成,失败不阻塞打开」)。
|
|
681
680
|
*
|
|
682
681
|
* 注:本函数置于 backup.ts 而非 middleware/project.ts——backup 模块不依赖 middleware
|
|
683
682
|
* (避免运行时循环依赖,见文件头注释),middleware 侧经 ../backup.js 导入复用。
|
|
@@ -689,7 +688,7 @@ export function migratePromptToAgents(project) {
|
|
|
689
688
|
if (typeof prompt !== "string" || prompt.trim() === "")
|
|
690
689
|
return; // 无 prompt 无需迁移
|
|
691
690
|
if (readAgentsFile(project.root) !== null)
|
|
692
|
-
return; // 已有
|
|
691
|
+
return; // 已有 不覆盖(一次性)
|
|
693
692
|
try {
|
|
694
693
|
writeAgentsFile(project.root, prompt);
|
|
695
694
|
}
|
|
@@ -702,20 +701,20 @@ export function migratePromptToAgents(project) {
|
|
|
702
701
|
* 覆盖管道(restore 与 import 覆盖当前打开的书共用,B2.3 从 restoreBackup 提取):
|
|
703
702
|
*
|
|
704
703
|
* 1. 释放当前 data.db 连接(替换 data.db 前必须;顺带清理陈旧 WAL/SHM 残留——
|
|
705
|
-
*
|
|
704
|
+
* 替换后新库不得复用旧 WAL)
|
|
706
705
|
* 2. writeProjectFilesFromBackup 原子替换三文件(保留当前项目 id;opts.name 归一)
|
|
707
|
-
* 3. 重连 + 版本对齐(v < 当前且有迁移路径 → 前向迁移,
|
|
708
|
-
*
|
|
706
|
+
* 3. 重连 + 版本对齐(v < 当前且有迁移路径 → 前向迁移,v === 当前 → 原样。
|
|
707
|
+
* 校验阶段已拒绝 v > 当前与无路径旧版,此处不会触发重建/拒绝)
|
|
709
708
|
* 4. 同步内存 config(刚原子写入,readProjectFile 必非 null;损坏抛错由 catch 恢复连接)
|
|
710
|
-
* 5.
|
|
709
|
+
* 5. migratePromptToAgents(恢复的 project.json 含遗留 prompt 且无 → 迁移)
|
|
711
710
|
* 6. 重启定时器(备份包内频率可能不同:5 → 60 等)
|
|
712
711
|
*
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
712
|
+
* 调用方职责:覆盖前自动快照 + 备份包校验(validateBackupPackage)。
|
|
713
|
+
* 失败时尝试恢复连接(原库可能未被替换或已替换为校验过的有效库),保证后续请求
|
|
714
|
+
* 不因悬挂连接报 500「connection not open」;**重连失败时连接悬挂**——由路由层
|
|
715
|
+
* (restore/import)在 catch 后检查 project.db.open 清空 currentProject 单例
|
|
716
|
+
* (backup 模块不依赖 middleware,避免运行时循环依赖)。
|
|
717
|
+
*/
|
|
719
718
|
export function overwriteProjectFiles(project, entries, opts = {}) {
|
|
720
719
|
const dbPath = join(project.root, DATA_DB_FILE_NAME);
|
|
721
720
|
closeDatabase(project.db); // 释放当前连接(替换 data.db 前必须;替换失败恢复见 catch)
|
|
@@ -746,10 +745,10 @@ export function overwriteProjectFiles(project, entries, opts = {}) {
|
|
|
746
745
|
throw new HttpError(500, "INTERNAL_ERROR", "覆盖后 project.json 读取失败");
|
|
747
746
|
}
|
|
748
747
|
project.config = restoredConfig;
|
|
749
|
-
//
|
|
750
|
-
// 向无
|
|
751
|
-
// 未创建,chat 读
|
|
752
|
-
// migratePromptToAgents 幂等(prompt 非空且无
|
|
748
|
+
// (oracle 评审修复):restore/import 覆盖路径同步触发 迁移——
|
|
749
|
+
// 向无 的项目恢复含遗留 prompt 的旧备份时,内存 config 有 prompt 但
|
|
750
|
+
// 未创建,chat 读 为 null,恢复的 prompt 在下次 open 前不注入。
|
|
751
|
+
// migratePromptToAgents 幂等(prompt 非空且无 才迁移),失败不阻塞覆盖。
|
|
753
752
|
migratePromptToAgents(project);
|
|
754
753
|
}
|
|
755
754
|
catch (err) {
|
|
@@ -769,7 +768,7 @@ export function overwriteProjectFiles(project, entries, opts = {}) {
|
|
|
769
768
|
startAutoBackup(project); // 重启定时器(覆盖包内频率可能不同)
|
|
770
769
|
}
|
|
771
770
|
/**
|
|
772
|
-
* 为**未打开的书**生成覆盖前快照(import
|
|
771
|
+
* 为**未打开的书**生成覆盖前快照(import 覆盖书架其他书场景,「覆盖前自动快照」):
|
|
773
772
|
* 临时打开 data.db 连接走备份管道(wal_checkpoint 保证快照完整性),完成后关闭连接。
|
|
774
773
|
* 已打开的书走 writeBackup(project)(复用现有连接,见 import 路由分流)。
|
|
775
774
|
*/
|
|
@@ -787,7 +786,7 @@ export function snapshotBookDir(dir) {
|
|
|
787
786
|
}
|
|
788
787
|
}
|
|
789
788
|
/**
|
|
790
|
-
*
|
|
789
|
+
* 原子写任意字节( 同款流程:写同目录临时文件 → fsync → rename 覆盖;
|
|
791
790
|
* 供 restore/import 替换三文件用——JSON 版见 db 包 writeJsonAtomic,此处为通用 bytes 版)。
|
|
792
791
|
* 失败保留临时文件供排查,原文件未被触碰。
|
|
793
792
|
*/
|
|
@@ -823,27 +822,27 @@ function writeFileAtomic(filePath, data) {
|
|
|
823
822
|
}
|
|
824
823
|
}
|
|
825
824
|
/**
|
|
826
|
-
* 从备份恢复当前项目(POST /project/backup/restore
|
|
825
|
+
* 从备份恢复当前项目(POST /project/backup/restore):
|
|
827
826
|
*
|
|
828
|
-
* 1. fileName 白名单校验:仅允许 `.backups/`
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
827
|
+
* 1. fileName 白名单校验:仅允许 `.backups/` 下时间戳格式( 兼容四类:
|
|
828
|
+
* `<YYYYMMDD-HHmmssSSS>[-<kind>][-<名称>].zip` 毫秒级(含 kind 段)/ `<YYYYMMDD-HHmmssSSS>-<名称>.zip`
|
|
829
|
+
* 旧带名称 / 旧秒级 `<YYYYMMDD-HHmmss>.zip`——shared parseBackupFileName,^$ 锚定 + 名称部分
|
|
830
|
+
* 拒绝 /\\,天然防路径分隔符与 `..` 穿越);格式合法但文件不存在 → 404
|
|
832
831
|
* 2. **覆盖前自动快照**:当前三文件打包为快照存入 .backups/(复用备份管道,
|
|
833
|
-
*
|
|
834
|
-
* 3. 备份包校验(validateBackupPackage:zip
|
|
835
|
-
*
|
|
832
|
+
* 就是普通备份文件,自然参与保留策略——后悔药)
|
|
833
|
+
* 3. 备份包校验(validateBackupPackage:zip/白名单//user_version 三态,
|
|
834
|
+
* 拒绝时数据零触碰——校验通过前不触碰任何数据文件)
|
|
836
835
|
* 4. **原子替换**:closeDatabase(释放当前连接)→ 三文件逐文件原子写
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
* 5. 项目 id
|
|
836
|
+
* (临时文件 + rename;data.db 顺带清理 -wal/-shm 残留)→ 重新 openDatabase
|
|
837
|
+
* + ensureSchemaCompatible(校验阶段已保证 v ≤ 当前且有迁移路径或匹配——
|
|
838
|
+
* v < 有路径时此处前向迁移)→ project.config 同步新 project.json
|
|
839
|
+
* 5. 项目 id 保留(当前项目引用不变);重启定时器(频率可能随备份变化)
|
|
841
840
|
*
|
|
842
|
-
|
|
843
|
-
|
|
841
|
+
* @throws HttpError 400(文件名非法/坏包)、404(备份不存在)、409 SCHEMA_VERSION_MISMATCH
|
|
842
|
+
*/
|
|
844
843
|
export function restoreBackup(project, fileName) {
|
|
845
844
|
// 1. 白名单校验(assertBackupFileNameFormat:parseBackupFileName 全格式校验,防路径穿越;
|
|
846
|
-
//
|
|
845
|
+
// 兼容毫秒级/带 kind 段/旧带名称/旧秒级文件名)
|
|
847
846
|
assertBackupFileNameFormat(fileName);
|
|
848
847
|
const backupPath = join(project.root, BACKUPS_DIR_NAME, fileName);
|
|
849
848
|
if (!existsSync(backupPath)) {
|
|
@@ -855,19 +854,19 @@ export function restoreBackup(project, fileName) {
|
|
|
855
854
|
const zip = readFileSync(backupPath);
|
|
856
855
|
const { entries, projectId: zipProjectId } = validateBackupPackage(zip);
|
|
857
856
|
// 4. 覆盖管道(B2.3 提取,import 覆盖复用):原子替换三文件(保留当前 id)+
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
//
|
|
857
|
+
// 重连 data.db + 同步 config + 重启定时器。
|
|
858
|
+
// name 归一为当前目录名(审核裁决:与 import 覆盖一致,维持「目录名 = 书名」
|
|
859
|
+
// 不变式——id 是身份、name 是展示名;改名需求走 /project/rename)
|
|
861
860
|
overwriteProjectFiles(project, entries, { name: basename(project.root), snapshotFileName: snapshot.fileName });
|
|
862
|
-
// 5. 会话归属迁移(B2.2 审核 P1-1
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
//
|
|
861
|
+
// 5. 会话归属迁移(B2.2 审核 P1-1):备份包内 project_id ≠ 当前项目 id
|
|
862
|
+
// (跨项目恢复,如手工放入 .backups/ 的异项目备份)→ chat_messages 旧 id 行迁移为
|
|
863
|
+
// 当前 id——「保留 id 保会话」的理由在跨项目场景同样成立:不迁移则恢复后聊天面板
|
|
864
|
+
// 静默为空、旧会话行成孤儿数据。同项目恢复(id 相等)跳过,不执行多余迁移。
|
|
865
|
+
// (import 覆盖无需迁移:id 匹配才走覆盖分支,zip id = 书架 id)
|
|
867
866
|
if (zipProjectId !== project.config.id) {
|
|
868
867
|
migrateChatMessagesProject(project.db, zipProjectId, project.config.id);
|
|
869
868
|
}
|
|
870
|
-
//
|
|
869
|
+
// ():snapshot 仅含 fileName/createdAt(size 属内部信息不暴露)
|
|
871
870
|
return { snapshot: { fileName: snapshot.fileName, createdAt: snapshot.createdAt } };
|
|
872
871
|
}
|
|
873
872
|
//# sourceMappingURL=backup.js.map
|