@whispering233/ai-editor-server 0.0.33 → 0.0.38
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-oxaM6dHH.css → index-IF0z2BR1.css} +1 -1
- package/client-dist/assets/{index-tfvJfexp.js → index-Sx1BGdOS.js} +196 -196
- package/client-dist/index.html +2 -2
- package/dist/backup.d.ts +135 -25
- package/dist/backup.d.ts.map +1 -1
- package/dist/backup.js +348 -68
- package/dist/backup.js.map +1 -1
- package/dist/cloud/auto-push.d.ts +39 -0
- package/dist/cloud/auto-push.d.ts.map +1 -0
- package/dist/cloud/auto-push.js +145 -0
- package/dist/cloud/auto-push.js.map +1 -0
- package/dist/cloud/device.d.ts +3 -0
- package/dist/cloud/device.d.ts.map +1 -0
- package/dist/cloud/device.js +15 -0
- package/dist/cloud/device.js.map +1 -0
- package/dist/cloud/state.d.ts +59 -0
- package/dist/cloud/state.d.ts.map +1 -0
- package/dist/cloud/state.js +162 -0
- package/dist/cloud/state.js.map +1 -0
- package/dist/cloud/sync.d.ts +102 -0
- package/dist/cloud/sync.d.ts.map +1 -0
- package/dist/cloud/sync.js +447 -0
- package/dist/cloud/sync.js.map +1 -0
- package/dist/cloud/webdav.d.ts +69 -0
- package/dist/cloud/webdav.d.ts.map +1 -0
- package/dist/cloud/webdav.js +326 -0
- package/dist/cloud/webdav.js.map +1 -0
- package/dist/device-name.d.ts +3 -0
- package/dist/device-name.d.ts.map +1 -0
- package/dist/device-name.js +13 -0
- package/dist/device-name.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/error.d.ts +8 -2
- package/dist/middleware/error.d.ts.map +1 -1
- package/dist/middleware/error.js +18 -1
- package/dist/middleware/error.js.map +1 -1
- package/dist/middleware/project.d.ts +27 -3
- package/dist/middleware/project.d.ts.map +1 -1
- package/dist/middleware/project.js +79 -6
- package/dist/middleware/project.js.map +1 -1
- package/dist/routes/cloud.d.ts +9 -0
- package/dist/routes/cloud.d.ts.map +1 -0
- package/dist/routes/cloud.js +206 -0
- package/dist/routes/cloud.js.map +1 -0
- package/dist/routes/delta.d.ts.map +1 -1
- package/dist/routes/delta.js +20 -2
- package/dist/routes/delta.js.map +1 -1
- package/dist/routes/project.d.ts.map +1 -1
- package/dist/routes/project.js +35 -18
- package/dist/routes/project.js.map +1 -1
- package/dist/routes/relation.d.ts.map +1 -1
- package/dist/routes/relation.js +33 -3
- package/dist/routes/relation.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type CloudBackupEntry, type CloudLocalState, type CloudPullResult, type CloudPushResult, type CloudSyncState } from "@whispering233/ai-editor-shared";
|
|
2
|
+
import type { ProjectContext } from "../middleware/project.js";
|
|
3
|
+
import { type DavEntry, type WebdavClient } from "./webdav.js";
|
|
4
|
+
/** 云盘单文件上限(实测口径:坚果云 WebDAV 500MB,见设计文档 §2 限制表) */
|
|
5
|
+
export declare const MAX_CLOUD_FILE_BYTES: number;
|
|
6
|
+
/** 云端保留份数(**不带标签**的份;带标签的永不清理) */
|
|
7
|
+
export declare const MAX_CLOUD_BACKUPS = 5;
|
|
8
|
+
/** 上传中/中断遗留的临时文件名前缀(正式名与保留策略都不认它们;推送前清理) */
|
|
9
|
+
export declare const CLOUD_TMP_PREFIX = ".tmp-";
|
|
10
|
+
/** 推送结果 = shared `CloudPushResult`(响应形状单一来源;client 读同一份类型) */
|
|
11
|
+
export type PushResult = CloudPushResult;
|
|
12
|
+
export interface PushOptions {
|
|
13
|
+
/** 要推送的本地备份文件名(缺省 = 最新一份);须通过 parseBackupFileName 白名单 */
|
|
14
|
+
fileName?: string;
|
|
15
|
+
/** true = 云端 head ≠ lastPushed 时「用本机强推」(先把云端那份下载存进本地 .backups/) */
|
|
16
|
+
force?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** 云端条目 → 备份投影(只认能解析出时间戳的文件;目录与非本程序命名的文件跳过) */
|
|
19
|
+
export declare function toCloudBackups(entries: readonly DavEntry[]): CloudBackupEntry[];
|
|
20
|
+
/** zip 内两个打包目录的条目名(`baseEntries` 基线;**只读名字不碰数据**,零解压开销) */
|
|
21
|
+
export declare function packedEntriesOfZip(zip: Uint8Array): string[];
|
|
22
|
+
/**
|
|
23
|
+
* 定位云端书目录名:
|
|
24
|
+
* ① 缓存 `dirName` 且目录存在 → 用它;
|
|
25
|
+
* ② 否则回退扫描云盘根目录,取名字以 `-<projectId>` 结尾的集合(改名后仍能认出来);
|
|
26
|
+
* ③ 都没有 → 用预期名 `<书名>-<完整 id>`(后续 `MKCOL` 创建;旧目录留存不影响正确性)。
|
|
27
|
+
*/
|
|
28
|
+
export declare function findExistingCloudDir(client: WebdavClient, project: ProjectContext, cachedDirName?: string): Promise<string | null>;
|
|
29
|
+
/** 推送用:已存在则用它,否则回落到预期名(由 `MKCOL` 创建) */
|
|
30
|
+
/**
|
|
31
|
+
* 幂等建书目录;**名字被云盘拒绝时回退短名**(坚果云实测:`<书名>-<id>` 可能触发
|
|
32
|
+
* `400 IllegalArgument / sandbox name is too long`,其单段名字限制比文档里的 255 字符路径上限严得多)。
|
|
33
|
+
*
|
|
34
|
+
* 回退名务必**仍以 `-<id>` 结尾**——`findExistingCloudDir` 的回退扫描就是按这个后缀找目录的;
|
|
35
|
+
* 长度 = `ai-editor-` (10) + `-` + id(26)≈ 37 字符纯 ASCII,比任意中文书名短且稳定。
|
|
36
|
+
*
|
|
37
|
+
* @returns 实际生效的目录名(正常 = 传入名;回退 = 短名)
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureBookDir(client: WebdavClient, dirName: string, project: ProjectContext): Promise<string>;
|
|
40
|
+
export declare function resolveBookDirName(client: WebdavClient, project: ProjectContext, cachedDirName?: string): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* 清理书目录里遗留的 `.tmp-*`(我们自己的前缀;可能来自中断的上传)。
|
|
43
|
+
* 只删前缀匹配的**文件**;失败仅记日志(垃圾清理不该阻塞推送)。
|
|
44
|
+
*/
|
|
45
|
+
export declare function cleanCloudTempFiles(client: WebdavClient, dirName: string, entries: readonly DavEntry[]): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* 保留策略:把「能解析出时间戳且**没有用户标签**」的份按时间倒序保留最近 `MAX_CLOUD_BACKUPS` 份,
|
|
48
|
+
* 其余删除(带标签的份、目录、非本程序命名的文件一律不动)。返回本次删除的文件名。
|
|
49
|
+
*/
|
|
50
|
+
export declare function pruneCloudBackups(client: WebdavClient, dirName: string, entries: readonly DavEntry[]): Promise<string[]>;
|
|
51
|
+
/**
|
|
52
|
+
* 书名改名后同步云端目录名(WebDAV `MOVE`,卡 4)。
|
|
53
|
+
*
|
|
54
|
+
* 调用方(`POST /project/rename`)**不阻塞本地改名**:云端慢/不可达时改名照常成功,失败只记日志——
|
|
55
|
+
* 下一轮推送/状态检查会按 `-<projectId>` 后缀回退扫描重新定位(只是云端目录名暂时落后)。
|
|
56
|
+
* 云端还没有这本书的目录(从未推送过)→ 直接返回,不创建。
|
|
57
|
+
*/
|
|
58
|
+
export declare function renameCloudDir(project: ProjectContext): Promise<void>;
|
|
59
|
+
/** 云端书目录内的**文件集合**(排序;目录条目剔除)——「云端有更新」的判定基准(与时间戳无关) */
|
|
60
|
+
export declare function cloudFileSet(entries: readonly DavEntry[]): string[];
|
|
61
|
+
/** `GET /cloud/status` 的本机段 + 三态(UI 据此决定提示与可用动作) */
|
|
62
|
+
export interface CloudSyncComputation {
|
|
63
|
+
local: CloudLocalState | null;
|
|
64
|
+
state: CloudSyncState;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 计算本机侧状态与三态(卡 5 定稿口径):
|
|
68
|
+
* - **「云端有更新」= 云端文件集合 ≠ `lastSeenCloudFiles`**(不看时间戳:跨机器时钟偏差会让 head 比较漏报,见设计文档 §3)
|
|
69
|
+
* - **「本机有改动」= 创作数据 mtime 晚于 `lastSyncAt`**(`hasLocalEditsSince`,三文件 + `data.db-wal` +
|
|
70
|
+
* `references/`/`sessions/`;**不含 `.backups/`**——force 会把云端旧份写进那里)。
|
|
71
|
+
* 注意口径差异:`data.db`/`-wal` 比较带 1s 容差(checkpoint 会刷新其 mtime),其余**严格比较**
|
|
72
|
+
* - 自动推送的失败标记(`lastAutoPushError`)原样透出(面板显示一行,成功即消失;卡 7)
|
|
73
|
+
* - 无同步记录(`lastSyncAt`/`lastSeenCloudFiles` 缺失)→ 云端有份即视为「有更新」、本机按「有改动」处理(保守)
|
|
74
|
+
*/
|
|
75
|
+
export declare function computeCloudSync(project: ProjectContext | null, webdavConfigured: boolean, remote: {
|
|
76
|
+
files: readonly string[];
|
|
77
|
+
} | null): CloudSyncComputation;
|
|
78
|
+
export interface PullOptions {
|
|
79
|
+
/** 要拉取的云端备份文件名(缺省 = 云端 head);须通过 parseBackupFileName 白名单 */
|
|
80
|
+
fileName?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 从云端拉取一份备份应用到当前项目(`POST /cloud/pull` 的实现)。
|
|
84
|
+
*
|
|
85
|
+
* 流程:定位书目录 → 列目录取目标(缺省 head)→ 下载 → 原样落进本地 `.backups/`(校验失败则回收)→
|
|
86
|
+
* 走 **restore 管道**(覆盖前自动快照 + 校验 + 原子替换三文件 + db 重连 + 重启备份定时器)+
|
|
87
|
+
* **两个打包目录并集合并**(基线 = `cloud.json` 的 `baseEntries`,删除优先)→ 更新同步状态
|
|
88
|
+
*(`lastPushedFileName` = 拉到的这份,既是新的冲突判定基准,也是「本机已基于该版本」的标记;
|
|
89
|
+
* `lastSeenCloudFiles` = 当前云端集合 → 立刻复查不会判「云端有更新」)。
|
|
90
|
+
*
|
|
91
|
+
* @throws HttpError 409 CLOUD_NOT_CONFIGURED、404 CLOUD_FILE_NOT_FOUND、400 VALIDATION_ERROR(坏包/文件名非法)、
|
|
92
|
+
* 409 SCHEMA_VERSION_MISMATCH(备份来自更高版本)、502 三码
|
|
93
|
+
*/
|
|
94
|
+
export declare function pullBackup(project: ProjectContext, options?: PullOptions): Promise<CloudPullResult>;
|
|
95
|
+
/**
|
|
96
|
+
* 推送一份本地备份到云端(`POST /cloud/push` 的实现)。
|
|
97
|
+
*
|
|
98
|
+
* @throws HttpError 409 CLOUD_NOT_CONFIGURED / CLOUD_CONFLICT、400 VALIDATION_ERROR / CLOUD_BACKUP_TOO_LARGE、
|
|
99
|
+
* 404 VALIDATION_ERROR(本地备份不存在)、502 CLOUD_AUTH_FAILED / CLOUD_UNREACHABLE / CLOUD_QUOTA_EXCEEDED
|
|
100
|
+
*/
|
|
101
|
+
export declare function pushBackup(project: ProjectContext, options?: PushOptions): Promise<PushResult>;
|
|
102
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/cloud/sync.ts"],"names":[],"mappings":"AAcA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,OAAO,EAAsB,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEnF,kDAAkD;AAClD,eAAO,MAAM,oBAAoB,QAAoB,CAAC;AAEtD,kCAAkC;AAClC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,UAAU,CAAC;AAExC,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,eAAe,CAAC;AAEzC,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAyCD,+CAA+C;AAC/C,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,GAAG,gBAAgB,EAAE,CAkB/E;AAED,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAO5D;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,cAAc,EACvB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYxB;AAED,wCAAwC;AACxC;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,cAAc,EACvB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAOD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAS5H;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,QAAQ,EAAE,GAC3B,OAAO,CAAC,MAAM,EAAE,CAAC,CAiBnB;AASD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAW3E;AAED,uDAAuD;AACvD,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,EAAE,CAKnE;AAED,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,cAAc,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,GAAG,IAAI,EAC9B,gBAAgB,EAAE,OAAO,EACzB,MAAM,EAAE;IAAE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,GAC1C,oBAAoB,CA6BtB;AAED,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAsE7G;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA2GxG"}
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// 云端推送(卡 4):把一份本地备份上传到云盘书目录
|
|
2
|
+
//
|
|
3
|
+
// 语义(`docs/design/40-cloud-sync.md` §3 整包 + 乐观并发 / §6 云端布局、保留、临时名 + MOVE)与
|
|
4
|
+
// 端点契约(`docs/api/100-api-cloud.md` 的 `POST /cloud/push`)逐条对应。不变式:
|
|
5
|
+
// - **推送 = 上传本地备份文件本身**(逐字节拷贝、文件名原样),不重新打包、不转换格式
|
|
6
|
+
// - 云端正式文件名下**永远是完整包**(`PUT` 临时名 → `MOVE` 提交;中断只留 `.tmp-` 垃圾,下次推送前清理)
|
|
7
|
+
// - **冲突判定 = 云端 head ≠ 本机 `lastPushedFileName`**(不看文件是否存在——旧份被清理也不误判)
|
|
8
|
+
// - 保留策略:只删「能解析出时间戳」的份、**带用户标签的永不删**;只在推送成功后执行,失败不阻塞
|
|
9
|
+
// - 云端书目录按 `project.id` 定位(缓存 `dirName` 快路径 → 404 回退扫描根目录按 `-<id>` 后缀匹配)
|
|
10
|
+
// - 推送前本地体积检查(云盘单文件上限),不等服务器回 413
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { Unzip } from "fflate";
|
|
14
|
+
import { parseBackupFileName, } from "@whispering233/ai-editor-shared";
|
|
15
|
+
import { HttpError } from "../middleware/error.js";
|
|
16
|
+
import { BACKUPS_DIR_NAME, PACKED_DIR_NAMES, hasLocalEditsSince, hasUnbackedChanges, restoreBackup } from "../backup.js";
|
|
17
|
+
import { readBookState, readWebdavConfig, writeBookState } from "./state.js";
|
|
18
|
+
import { createWebdavClient } from "./webdav.js";
|
|
19
|
+
/** 云盘单文件上限(实测口径:坚果云 WebDAV 500MB,见设计文档 §2 限制表) */
|
|
20
|
+
export const MAX_CLOUD_FILE_BYTES = 500 * 1024 * 1024;
|
|
21
|
+
/** 云端保留份数(**不带标签**的份;带标签的永不清理) */
|
|
22
|
+
export const MAX_CLOUD_BACKUPS = 5;
|
|
23
|
+
/** 上传中/中断遗留的临时文件名前缀(正式名与保留策略都不认它们;推送前清理) */
|
|
24
|
+
export const CLOUD_TMP_PREFIX = ".tmp-";
|
|
25
|
+
/** 本地 `.backups/` 下最新一份(按文件名时间戳;无合法备份 → null) */
|
|
26
|
+
function latestLocalBackupName(backupsDir) {
|
|
27
|
+
let files;
|
|
28
|
+
try {
|
|
29
|
+
files = readdirSync(backupsDir);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
let latest = null;
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const parsed = parseBackupFileName(file);
|
|
37
|
+
if (parsed === null)
|
|
38
|
+
continue;
|
|
39
|
+
const time = parsed.time.getTime();
|
|
40
|
+
if (latest === null || time > latest.time)
|
|
41
|
+
latest = { name: file, time };
|
|
42
|
+
}
|
|
43
|
+
return latest?.name ?? null;
|
|
44
|
+
}
|
|
45
|
+
/** 解析并读取待推送的本地备份(白名单校验防路径穿越;缺文件 404) */
|
|
46
|
+
function resolveLocalBackup(project, fileName) {
|
|
47
|
+
const backupsDir = join(project.root, BACKUPS_DIR_NAME);
|
|
48
|
+
const name = fileName ?? latestLocalBackupName(backupsDir);
|
|
49
|
+
if (name === null) {
|
|
50
|
+
throw new HttpError(404, "VALIDATION_ERROR", "本机没有可推送的备份(先「立即备份」生成一份)");
|
|
51
|
+
}
|
|
52
|
+
if (parseBackupFileName(name) === null) {
|
|
53
|
+
throw new HttpError(400, "VALIDATION_ERROR", `备份文件名不在白名单内: ${name}`);
|
|
54
|
+
}
|
|
55
|
+
const path = join(backupsDir, name);
|
|
56
|
+
if (!existsSync(path)) {
|
|
57
|
+
throw new HttpError(404, "VALIDATION_ERROR", `备份不存在: ${name}`);
|
|
58
|
+
}
|
|
59
|
+
const bytes = readFileSync(path);
|
|
60
|
+
return { fileName: name, bytes, size: bytes.length };
|
|
61
|
+
}
|
|
62
|
+
/** 云端条目 → 备份投影(只认能解析出时间戳的文件;目录与非本程序命名的文件跳过) */
|
|
63
|
+
export function toCloudBackups(entries) {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.isCollection)
|
|
67
|
+
continue;
|
|
68
|
+
const parsed = parseBackupFileName(entry.name);
|
|
69
|
+
if (parsed === null)
|
|
70
|
+
continue;
|
|
71
|
+
out.push({
|
|
72
|
+
fileName: entry.name,
|
|
73
|
+
createdAt: parsed.time.toISOString(),
|
|
74
|
+
kind: parsed.kind,
|
|
75
|
+
...(parsed.name !== undefined ? { name: parsed.name } : {}),
|
|
76
|
+
device: parsed.device,
|
|
77
|
+
stats: parsed.stats,
|
|
78
|
+
size: entry.size ?? 0,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// 时间倒序(最新在前;head = [0])——与本地备份列表同口径
|
|
82
|
+
return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
83
|
+
}
|
|
84
|
+
/** zip 内两个打包目录的条目名(`baseEntries` 基线;**只读名字不碰数据**,零解压开销) */
|
|
85
|
+
export function packedEntriesOfZip(zip) {
|
|
86
|
+
const names = [];
|
|
87
|
+
const unzipper = new Unzip((file) => {
|
|
88
|
+
names.push(file.name); // 不 start():只收名,不做任何解压
|
|
89
|
+
});
|
|
90
|
+
unzipper.push(zip, true);
|
|
91
|
+
return names.filter((name) => PACKED_DIR_NAMES.some((dir) => name.startsWith(`${dir}/`))).sort();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 定位云端书目录名:
|
|
95
|
+
* ① 缓存 `dirName` 且目录存在 → 用它;
|
|
96
|
+
* ② 否则回退扫描云盘根目录,取名字以 `-<projectId>` 结尾的集合(改名后仍能认出来);
|
|
97
|
+
* ③ 都没有 → 用预期名 `<书名>-<完整 id>`(后续 `MKCOL` 创建;旧目录留存不影响正确性)。
|
|
98
|
+
*/
|
|
99
|
+
export async function findExistingCloudDir(client, project, cachedDirName) {
|
|
100
|
+
const id = project.config.id;
|
|
101
|
+
if (typeof cachedDirName === "string" && cachedDirName !== "" && (await client.list(cachedDirName)) !== null) {
|
|
102
|
+
return cachedDirName;
|
|
103
|
+
}
|
|
104
|
+
const root = await client.list("");
|
|
105
|
+
// 匹配三种命名(都含完整 id):`<书名>-<id>`(默认)、`ai-editor-<id>`(名字被拒后的回退)、
|
|
106
|
+
// `<id>`(回退链最后一档,某些云盘对名字长度限制极严时用)
|
|
107
|
+
const match = (root ?? []).find((entry) => entry.isCollection && (entry.name === id || entry.name.endsWith(`-${id}`)));
|
|
108
|
+
return match?.name ?? null;
|
|
109
|
+
}
|
|
110
|
+
/** 推送用:已存在则用它,否则回落到预期名(由 `MKCOL` 创建) */
|
|
111
|
+
/**
|
|
112
|
+
* 幂等建书目录;**名字被云盘拒绝时回退短名**(坚果云实测:`<书名>-<id>` 可能触发
|
|
113
|
+
* `400 IllegalArgument / sandbox name is too long`,其单段名字限制比文档里的 255 字符路径上限严得多)。
|
|
114
|
+
*
|
|
115
|
+
* 回退名务必**仍以 `-<id>` 结尾**——`findExistingCloudDir` 的回退扫描就是按这个后缀找目录的;
|
|
116
|
+
* 长度 = `ai-editor-` (10) + `-` + id(26)≈ 37 字符纯 ASCII,比任意中文书名短且稳定。
|
|
117
|
+
*
|
|
118
|
+
* @returns 实际生效的目录名(正常 = 传入名;回退 = 短名)
|
|
119
|
+
*/
|
|
120
|
+
export async function ensureBookDir(client, dirName, project) {
|
|
121
|
+
// 候选链:漂亮名 → 短前缀名 → 纯 id(后者一定最短,且仍被回退扫描匹配)
|
|
122
|
+
const candidates = [dirName, `ai-editor-${project.config.id}`, project.config.id].filter((name, i, all) => all.indexOf(name) === i);
|
|
123
|
+
let lastErr;
|
|
124
|
+
for (const name of candidates) {
|
|
125
|
+
try {
|
|
126
|
+
await client.mkcol(name);
|
|
127
|
+
if (name !== dirName) {
|
|
128
|
+
console.warn(`[cloud] 云盘拒绝较长的目录名,改用「${name}」(原候选「${dirName}」)`);
|
|
129
|
+
}
|
|
130
|
+
return name;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
lastErr = err;
|
|
134
|
+
// 只在「名字不被接受」这类 4xx 上换名字重试;网络/认证/配额等照旧抛。
|
|
135
|
+
// 注意用 `upstreamStatus`(上游原始码)——对外的 `status` 一律是映射后的 502。
|
|
136
|
+
const upstream = err instanceof HttpError ? err.upstreamStatus : undefined;
|
|
137
|
+
if (upstream === undefined || upstream < 400 || upstream >= 500)
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw lastErr instanceof Error ? lastErr : new Error("云盘拒绝创建书目录(名字长度限制?)");
|
|
142
|
+
}
|
|
143
|
+
export async function resolveBookDirName(client, project, cachedDirName) {
|
|
144
|
+
return ((await findExistingCloudDir(client, project, cachedDirName)) ?? `${project.config.name}-${project.config.id}`);
|
|
145
|
+
}
|
|
146
|
+
/** 云端 head = 能解析出时间戳的备份里时间最新的一份(目录内没有合法备份 → null) */
|
|
147
|
+
function headOf(backups) {
|
|
148
|
+
return backups.length === 0 ? null : backups[0];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 清理书目录里遗留的 `.tmp-*`(我们自己的前缀;可能来自中断的上传)。
|
|
152
|
+
* 只删前缀匹配的**文件**;失败仅记日志(垃圾清理不该阻塞推送)。
|
|
153
|
+
*/
|
|
154
|
+
export async function cleanCloudTempFiles(client, dirName, entries) {
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
if (entry.isCollection || !entry.name.startsWith(CLOUD_TMP_PREFIX))
|
|
157
|
+
continue;
|
|
158
|
+
try {
|
|
159
|
+
await client.remove(`${dirName}/${entry.name}`);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.error(`[cloud] 清理云端临时文件失败(跳过): ${entry.name}`, err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 保留策略:把「能解析出时间戳且**没有用户标签**」的份按时间倒序保留最近 `MAX_CLOUD_BACKUPS` 份,
|
|
168
|
+
* 其余删除(带标签的份、目录、非本程序命名的文件一律不动)。返回本次删除的文件名。
|
|
169
|
+
*/
|
|
170
|
+
export async function pruneCloudBackups(client, dirName, entries) {
|
|
171
|
+
const deletable = entries
|
|
172
|
+
.filter((entry) => !entry.isCollection)
|
|
173
|
+
.map((entry) => ({ entry, parsed: parseBackupFileName(entry.name) }))
|
|
174
|
+
.filter((x) => x.parsed !== null)
|
|
175
|
+
.filter((x) => x.parsed.name === undefined) // 带标签的永不清理
|
|
176
|
+
.sort((a, b) => b.parsed.time.getTime() - a.parsed.time.getTime());
|
|
177
|
+
const pruned = [];
|
|
178
|
+
for (const { entry } of deletable.slice(MAX_CLOUD_BACKUPS)) {
|
|
179
|
+
try {
|
|
180
|
+
await client.remove(`${dirName}/${entry.name}`);
|
|
181
|
+
pruned.push(entry.name);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.error(`[cloud] 清理云端旧备份失败(不阻塞推送): ${entry.name}`, err);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return pruned;
|
|
188
|
+
}
|
|
189
|
+
/** 把云端 head 原样下载存进本地 `.backups/`(强推前的「两边都留档」;本地已有同名则保留本地那份) */
|
|
190
|
+
function archiveCloudHead(project, fileName, bytes) {
|
|
191
|
+
const path = join(project.root, BACKUPS_DIR_NAME, fileName);
|
|
192
|
+
if (!existsSync(path))
|
|
193
|
+
writeFileSync(path, bytes);
|
|
194
|
+
return { fileName };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 书名改名后同步云端目录名(WebDAV `MOVE`,卡 4)。
|
|
198
|
+
*
|
|
199
|
+
* 调用方(`POST /project/rename`)**不阻塞本地改名**:云端慢/不可达时改名照常成功,失败只记日志——
|
|
200
|
+
* 下一轮推送/状态检查会按 `-<projectId>` 后缀回退扫描重新定位(只是云端目录名暂时落后)。
|
|
201
|
+
* 云端还没有这本书的目录(从未推送过)→ 直接返回,不创建。
|
|
202
|
+
*/
|
|
203
|
+
export async function renameCloudDir(project) {
|
|
204
|
+
const webdav = readWebdavConfig();
|
|
205
|
+
if (webdav === null)
|
|
206
|
+
return;
|
|
207
|
+
const client = createWebdavClient(webdav);
|
|
208
|
+
const state = readBookState(project.config.id);
|
|
209
|
+
const current = await findExistingCloudDir(client, project, state?.dirName);
|
|
210
|
+
if (current === null)
|
|
211
|
+
return;
|
|
212
|
+
const next = `${project.config.name}-${project.config.id}`;
|
|
213
|
+
if (current === next)
|
|
214
|
+
return;
|
|
215
|
+
await client.move(current, next);
|
|
216
|
+
writeBookState(project.config.id, { dirName: next });
|
|
217
|
+
}
|
|
218
|
+
/** 云端书目录内的**文件集合**(排序;目录条目剔除)——「云端有更新」的判定基准(与时间戳无关) */
|
|
219
|
+
export function cloudFileSet(entries) {
|
|
220
|
+
return entries
|
|
221
|
+
.filter((entry) => !entry.isCollection)
|
|
222
|
+
.map((entry) => entry.name)
|
|
223
|
+
.sort();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* 计算本机侧状态与三态(卡 5 定稿口径):
|
|
227
|
+
* - **「云端有更新」= 云端文件集合 ≠ `lastSeenCloudFiles`**(不看时间戳:跨机器时钟偏差会让 head 比较漏报,见设计文档 §3)
|
|
228
|
+
* - **「本机有改动」= 创作数据 mtime 晚于 `lastSyncAt`**(`hasLocalEditsSince`,三文件 + `data.db-wal` +
|
|
229
|
+
* `references/`/`sessions/`;**不含 `.backups/`**——force 会把云端旧份写进那里)。
|
|
230
|
+
* 注意口径差异:`data.db`/`-wal` 比较带 1s 容差(checkpoint 会刷新其 mtime),其余**严格比较**
|
|
231
|
+
* - 自动推送的失败标记(`lastAutoPushError`)原样透出(面板显示一行,成功即消失;卡 7)
|
|
232
|
+
* - 无同步记录(`lastSyncAt`/`lastSeenCloudFiles` 缺失)→ 云端有份即视为「有更新」、本机按「有改动」处理(保守)
|
|
233
|
+
*/
|
|
234
|
+
export function computeCloudSync(project, webdavConfigured, remote) {
|
|
235
|
+
if (project === null) {
|
|
236
|
+
return { local: null, state: webdavConfigured ? "no-project" : "unconfigured" };
|
|
237
|
+
}
|
|
238
|
+
const state = readBookState(project.config.id);
|
|
239
|
+
const lastSyncAt = state?.lastSyncAt ?? null;
|
|
240
|
+
const dirty = lastSyncAt === null ? true : hasLocalEditsSince(project, new Date(lastSyncAt));
|
|
241
|
+
const local = {
|
|
242
|
+
lastPushedFileName: state?.lastPushedFileName ?? null,
|
|
243
|
+
lastSyncAt,
|
|
244
|
+
dirty,
|
|
245
|
+
latestBackupFileName: latestLocalBackupName(join(project.root, BACKUPS_DIR_NAME)),
|
|
246
|
+
// 「有改动未进最新备份」**且**「确有未同步改动」:后者把「管道自己刚写下的文件」排掉——
|
|
247
|
+
// 拉取/恢复会用刚生成的快照(时间戳早于写入)当最新备份,若只看 mtime 会立刻误报「有改动未进备份」
|
|
248
|
+
//(用户实测)。自动路径的守卫用的是**纯** `hasUnbackedChanges`(那边必须严格:最新备份确实落后于本机状态
|
|
249
|
+
// 就不能推,否则会把旧内容推上去),两者口径不同是有意的。
|
|
250
|
+
backupStale: hasUnbackedChanges(project) && dirty,
|
|
251
|
+
// 自动推送最近一次失败(缺省不出现——成功即清,见 cloud/auto-push.ts)
|
|
252
|
+
...(state?.lastAutoPushError !== undefined ? { lastAutoPushError: state.lastAutoPushError } : {}),
|
|
253
|
+
};
|
|
254
|
+
if (!webdavConfigured)
|
|
255
|
+
return { local, state: "unconfigured" };
|
|
256
|
+
if (remote === null)
|
|
257
|
+
return { local, state: "unreachable" };
|
|
258
|
+
const seen = state?.lastSeenCloudFiles;
|
|
259
|
+
const remoteChanged = seen === undefined ? remote.files.length > 0 : seen.join("\n") !== [...remote.files].sort().join("\n");
|
|
260
|
+
if (remoteChanged && dirty)
|
|
261
|
+
return { local, state: "conflict" };
|
|
262
|
+
if (remoteChanged)
|
|
263
|
+
return { local, state: "remote-ahead" };
|
|
264
|
+
if (dirty)
|
|
265
|
+
return { local, state: "local-ahead" };
|
|
266
|
+
return { local, state: "synced" };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 从云端拉取一份备份应用到当前项目(`POST /cloud/pull` 的实现)。
|
|
270
|
+
*
|
|
271
|
+
* 流程:定位书目录 → 列目录取目标(缺省 head)→ 下载 → 原样落进本地 `.backups/`(校验失败则回收)→
|
|
272
|
+
* 走 **restore 管道**(覆盖前自动快照 + 校验 + 原子替换三文件 + db 重连 + 重启备份定时器)+
|
|
273
|
+
* **两个打包目录并集合并**(基线 = `cloud.json` 的 `baseEntries`,删除优先)→ 更新同步状态
|
|
274
|
+
*(`lastPushedFileName` = 拉到的这份,既是新的冲突判定基准,也是「本机已基于该版本」的标记;
|
|
275
|
+
* `lastSeenCloudFiles` = 当前云端集合 → 立刻复查不会判「云端有更新」)。
|
|
276
|
+
*
|
|
277
|
+
* @throws HttpError 409 CLOUD_NOT_CONFIGURED、404 CLOUD_FILE_NOT_FOUND、400 VALIDATION_ERROR(坏包/文件名非法)、
|
|
278
|
+
* 409 SCHEMA_VERSION_MISMATCH(备份来自更高版本)、502 三码
|
|
279
|
+
*/
|
|
280
|
+
export async function pullBackup(project, options = {}) {
|
|
281
|
+
const webdav = readWebdavConfig();
|
|
282
|
+
if (webdav === null) {
|
|
283
|
+
throw new HttpError(409, "CLOUD_NOT_CONFIGURED", "云盘未配置:请先在设置页填写 WebDAV 地址与用户名/应用密码");
|
|
284
|
+
}
|
|
285
|
+
if (options.fileName !== undefined && parseBackupFileName(options.fileName) === null) {
|
|
286
|
+
throw new HttpError(400, "VALIDATION_ERROR", `备份文件名不在白名单内: ${options.fileName}`);
|
|
287
|
+
}
|
|
288
|
+
const projectId = project.config.id;
|
|
289
|
+
const client = createWebdavClient(webdav);
|
|
290
|
+
const stateBefore = readBookState(projectId);
|
|
291
|
+
const dirName = await findExistingCloudDir(client, project, stateBefore?.dirName);
|
|
292
|
+
const entries = dirName === null ? [] : ((await client.list(dirName)) ?? []);
|
|
293
|
+
const backups = toCloudBackups(entries);
|
|
294
|
+
const head = backups[0] ?? null;
|
|
295
|
+
const target = options.fileName === undefined ? head : (backups.find((entry) => entry.fileName === options.fileName) ?? null);
|
|
296
|
+
if (target === null || dirName === null) {
|
|
297
|
+
throw new HttpError(404, "CLOUD_FILE_NOT_FOUND", options.fileName === undefined
|
|
298
|
+
? "云端还没有可拉取的备份(先在本机推送一份)"
|
|
299
|
+
: `云端那份备份已不存在: ${options.fileName}(可能已被保留策略清理或手动删除)`);
|
|
300
|
+
}
|
|
301
|
+
const bytes = await client.get(`${dirName}/${target.fileName}`);
|
|
302
|
+
if (bytes === null) {
|
|
303
|
+
throw new HttpError(404, "CLOUD_FILE_NOT_FOUND", `云端那份备份已不存在: ${target.fileName}`);
|
|
304
|
+
}
|
|
305
|
+
// 原样落进本地 .backups/(成为一份普通本地备份,参与保留策略;校验失败则回收,不留坏包)
|
|
306
|
+
const backupsDir = join(project.root, BACKUPS_DIR_NAME);
|
|
307
|
+
mkdirSync(backupsDir, { recursive: true });
|
|
308
|
+
const localPath = join(backupsDir, target.fileName);
|
|
309
|
+
const createdLocally = !existsSync(localPath);
|
|
310
|
+
if (createdLocally)
|
|
311
|
+
writeFileSync(localPath, bytes);
|
|
312
|
+
let restored;
|
|
313
|
+
try {
|
|
314
|
+
// 走 restore 管道(覆盖前自动快照 / 校验 / 原子替换 / 重连 / 定时器),但两目录用并集合并
|
|
315
|
+
restored = restoreBackup(project, target.fileName, {
|
|
316
|
+
mergePackedDirs: { baseEntries: stateBefore?.baseEntries ?? [] },
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
if (createdLocally) {
|
|
321
|
+
try {
|
|
322
|
+
unlinkSync(localPath); // 坏包/版本不兼容:不留一份无法使用的“备份”
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// 回收失败不掩盖原始错误
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
330
|
+
writeBookState(projectId, {
|
|
331
|
+
dirName,
|
|
332
|
+
lastPushedFileName: target.fileName,
|
|
333
|
+
lastSeenHeadFileName: head.fileName,
|
|
334
|
+
lastSyncAt: new Date().toISOString(),
|
|
335
|
+
lastSeenCloudFiles: cloudFileSet(entries),
|
|
336
|
+
baseEntries: packedEntriesOfZip(bytes),
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
pulled: target,
|
|
340
|
+
snapshot: restored.snapshot,
|
|
341
|
+
merged: restored.merged ?? { kept: 0, written: 0, removed: 0 },
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* 推送一份本地备份到云端(`POST /cloud/push` 的实现)。
|
|
346
|
+
*
|
|
347
|
+
* @throws HttpError 409 CLOUD_NOT_CONFIGURED / CLOUD_CONFLICT、400 VALIDATION_ERROR / CLOUD_BACKUP_TOO_LARGE、
|
|
348
|
+
* 404 VALIDATION_ERROR(本地备份不存在)、502 CLOUD_AUTH_FAILED / CLOUD_UNREACHABLE / CLOUD_QUOTA_EXCEEDED
|
|
349
|
+
*/
|
|
350
|
+
export async function pushBackup(project, options = {}) {
|
|
351
|
+
const webdav = readWebdavConfig();
|
|
352
|
+
if (webdav === null) {
|
|
353
|
+
throw new HttpError(409, "CLOUD_NOT_CONFIGURED", "云盘未配置:请先在设置页填写 WebDAV 地址与用户名/应用密码");
|
|
354
|
+
}
|
|
355
|
+
const local = resolveLocalBackup(project, options.fileName);
|
|
356
|
+
const projectId = project.config.id;
|
|
357
|
+
const client = createWebdavClient(webdav);
|
|
358
|
+
// ① 定位云端书目录(缓存 → 回退扫描 → 预期名);目录不存在 → 幂等 MKCOL
|
|
359
|
+
const stateBefore = readBookState(projectId);
|
|
360
|
+
const resolved = await resolveBookDirName(client, project, stateBefore?.dirName);
|
|
361
|
+
let dirName = resolved;
|
|
362
|
+
let entries = await client.list(dirName);
|
|
363
|
+
if (entries === null) {
|
|
364
|
+
// 工作区可能还不存在(从未跑过「测试连接」)——幂等建「云盘根 + 工作根」,
|
|
365
|
+
// 否则 MKCOL 书目录会因父目录缺失报 409(被映射成「云盘不可达」,文案误导;见 backlog)
|
|
366
|
+
if ((await client.list("")) === null)
|
|
367
|
+
await client.ensureWorkingRoot();
|
|
368
|
+
// 名字过长被拒时回退短名(坚果云实测),回退结果就是这次真正使用的目录名
|
|
369
|
+
dirName = await ensureBookDir(client, dirName, project);
|
|
370
|
+
entries = (await client.list(dirName)) ?? [];
|
|
371
|
+
}
|
|
372
|
+
// ② 清理遗留临时文件后重新列目录(残留来自中断的上传);冲突判定基于 head
|
|
373
|
+
await cleanCloudTempFiles(client, dirName, entries);
|
|
374
|
+
entries = (await client.list(dirName)) ?? [];
|
|
375
|
+
const backups = toCloudBackups(entries);
|
|
376
|
+
const head = headOf(backups);
|
|
377
|
+
const lastPushed = stateBefore?.lastPushedFileName;
|
|
378
|
+
const conflicted = head !== null && head.fileName !== lastPushed;
|
|
379
|
+
// ③ 冲突:默认拒绝(人裁决在 UI);force = 先把云端那份存进本地 .backups/(两边都留档)
|
|
380
|
+
let snapshot;
|
|
381
|
+
if (conflicted && options.force === true) {
|
|
382
|
+
const bytes = await client.get(`${dirName}/${head.fileName}`);
|
|
383
|
+
if (bytes !== null)
|
|
384
|
+
snapshot = archiveCloudHead(project, head.fileName, bytes);
|
|
385
|
+
}
|
|
386
|
+
else if (conflicted) {
|
|
387
|
+
throw new HttpError(409, "CLOUD_CONFLICT", `云端已有更新的备份(${head.fileName}),本机上次推送的是 ${lastPushed ?? "(无记录)"}——请先拉取查看,或选择用本机覆盖云端`);
|
|
388
|
+
}
|
|
389
|
+
// ④ 体积检查(本地判定,不等服务器回 413)
|
|
390
|
+
if (local.size > MAX_CLOUD_FILE_BYTES) {
|
|
391
|
+
throw new HttpError(400, "CLOUD_BACKUP_TOO_LARGE", `备份包 ${(local.size / 1024 / 1024).toFixed(1)}MB 超过云盘单文件上限 ${MAX_CLOUD_FILE_BYTES / 1024 / 1024}MB,未推送`);
|
|
392
|
+
}
|
|
393
|
+
// ⑤ 上传:临时名 → MOVE(正式名下永远是完整包;MOVE 失败清理临时名后抛出)
|
|
394
|
+
// 已存在同名同大小的份 → **跳过上传**(用户连点、上次推完又推、自动推送重试):既省云盘配额,
|
|
395
|
+
// 也避开「目标已存在」在部分云盘上的 MOVE 语义差异(坚果云即使带 `Overwrite: T` 也回 409 DuplicateName)
|
|
396
|
+
const alreadyThere = entries.some((entry) => !entry.isCollection && entry.name === local.fileName && entry.size === local.size);
|
|
397
|
+
if (!alreadyThere) {
|
|
398
|
+
const tmpName = `${CLOUD_TMP_PREFIX}${local.fileName}`;
|
|
399
|
+
await client.put(`${dirName}/${tmpName}`, local.bytes);
|
|
400
|
+
try {
|
|
401
|
+
await client.move(`${dirName}/${tmpName}`, `${dirName}/${local.fileName}`);
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
try {
|
|
405
|
+
await client.remove(`${dirName}/${tmpName}`);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// 临时名残留由下次推送前清理;原始错误优先
|
|
409
|
+
}
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ⑥ 保留清理(只在推送成功后;失败不阻塞——pruneCloudBackups 内部吞错记日志)
|
|
414
|
+
// 注意:状态里的 lastSeenCloudFiles 必须记**清理后**的集合(否则下次检查会把被清理的份当成变化)
|
|
415
|
+
const pruned = await pruneCloudBackups(client, dirName, await client.list(dirName).then((list) => list ?? []));
|
|
416
|
+
const after = (await client.list(dirName)) ?? [];
|
|
417
|
+
// ⑦ 更新本机同步状态(推送成功后;baseEntries = zip 内两个打包目录的条目名,供拉取三方比较)
|
|
418
|
+
// `lastAutoPushError` 的**唯一清除点**在这里(`undefined` 在合并写下即删除该键):口径是
|
|
419
|
+
// 「最近一次自动推送失败」——任何一次推送成功都证明它已过期,不能只由自动路径清
|
|
420
|
+
//(否则用户手动推送成功后,面板仍常驻一行过期的失败提示;见卡 7 oracle 反例 4)
|
|
421
|
+
writeBookState(projectId, {
|
|
422
|
+
dirName,
|
|
423
|
+
lastPushedFileName: local.fileName,
|
|
424
|
+
lastSeenHeadFileName: toCloudBackups(after)[0]?.fileName ?? local.fileName,
|
|
425
|
+
lastSyncAt: new Date().toISOString(),
|
|
426
|
+
lastSeenCloudFiles: cloudFileSet(after),
|
|
427
|
+
baseEntries: packedEntriesOfZip(local.bytes),
|
|
428
|
+
lastAutoPushError: undefined,
|
|
429
|
+
});
|
|
430
|
+
const parsedLocal = parseBackupFileName(local.fileName);
|
|
431
|
+
const pushed = {
|
|
432
|
+
fileName: local.fileName,
|
|
433
|
+
createdAt: parsedLocal.time.toISOString(),
|
|
434
|
+
kind: parsedLocal.kind,
|
|
435
|
+
...(parsedLocal.name !== undefined ? { name: parsedLocal.name } : {}),
|
|
436
|
+
device: parsedLocal.device,
|
|
437
|
+
stats: parsedLocal.stats,
|
|
438
|
+
size: local.size,
|
|
439
|
+
};
|
|
440
|
+
return {
|
|
441
|
+
pushed,
|
|
442
|
+
remote: { dirName, headFileName: local.fileName },
|
|
443
|
+
pruned,
|
|
444
|
+
...(snapshot !== undefined ? { snapshot } : {}),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/cloud/sync.ts"],"names":[],"mappings":"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"}
|