@lemoncat7/dsh-partner 1.16.2 → 2.0.0
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/README.md +15 -0
- package/cordis.patch.yml +1 -0
- package/docs/releases/2.0.0.md +34 -0
- package/docs/storage-migration.md +72 -0
- package/lib/api.d.ts +2 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +49 -1
- package/lib/api.js.map +1 -1
- package/lib/attachments/service.d.ts +10 -0
- package/lib/attachments/service.d.ts.map +1 -1
- package/lib/attachments/service.js +68 -9
- package/lib/attachments/service.js.map +1 -1
- package/lib/client-controller.d.ts +1 -1
- package/lib/client-controller.d.ts.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +460 -239
- package/lib/client.js.map +4 -4
- package/lib/companions/removal.d.ts.map +1 -1
- package/lib/companions/removal.js +3 -0
- package/lib/companions/removal.js.map +1 -1
- package/lib/concern-store.d.ts +5 -1
- package/lib/concern-store.d.ts.map +1 -1
- package/lib/concern-store.js +13 -4
- package/lib/concern-store.js.map +1 -1
- package/lib/config.d.ts +1 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +6 -0
- package/lib/config.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +162 -132
- package/lib/index.js.map +1 -1
- package/lib/memory-store.d.ts +4 -1
- package/lib/memory-store.d.ts.map +1 -1
- package/lib/memory-store.js +10 -2
- package/lib/memory-store.js.map +1 -1
- package/lib/notifications/domain.d.ts +2 -1
- package/lib/notifications/domain.d.ts.map +1 -1
- package/lib/notifications/store.d.ts +8 -0
- package/lib/notifications/store.d.ts.map +1 -1
- package/lib/notifications/store.js +81 -17
- package/lib/notifications/store.js.map +1 -1
- package/lib/pendant/reader.js +1 -1
- package/lib/pendant/reader.js.map +1 -1
- package/lib/pendant/widget.d.ts.map +1 -1
- package/lib/pendant/widget.js +5 -1
- package/lib/pendant/widget.js.map +1 -1
- package/lib/storage/atomic.d.ts +3 -0
- package/lib/storage/atomic.d.ts.map +1 -0
- package/lib/storage/atomic.js +35 -0
- package/lib/storage/atomic.js.map +1 -0
- package/lib/storage/bootstrap.d.ts +8 -0
- package/lib/storage/bootstrap.d.ts.map +1 -0
- package/lib/storage/bootstrap.js +31 -0
- package/lib/storage/bootstrap.js.map +1 -0
- package/lib/storage/coordinator.d.ts +36 -0
- package/lib/storage/coordinator.d.ts.map +1 -0
- package/lib/storage/coordinator.js +103 -0
- package/lib/storage/coordinator.js.map +1 -0
- package/lib/storage/layout.d.ts +13 -0
- package/lib/storage/layout.d.ts.map +1 -0
- package/lib/storage/layout.js +30 -0
- package/lib/storage/layout.js.map +1 -0
- package/lib/storage/lease.d.ts +5 -0
- package/lib/storage/lease.d.ts.map +1 -0
- package/lib/storage/lease.js +25 -0
- package/lib/storage/lease.js.map +1 -0
- package/lib/storage/legacy-backups.d.ts +3 -0
- package/lib/storage/legacy-backups.d.ts.map +1 -0
- package/lib/storage/legacy-backups.js +14 -0
- package/lib/storage/legacy-backups.js.map +1 -0
- package/lib/storage/migrate-layout.d.ts +18 -0
- package/lib/storage/migrate-layout.d.ts.map +1 -0
- package/lib/storage/migrate-layout.js +204 -0
- package/lib/storage/migrate-layout.js.map +1 -0
- package/lib/storage/migrations/index.d.ts +3 -0
- package/lib/storage/migrations/index.d.ts.map +1 -0
- package/lib/storage/migrations/index.js +4 -0
- package/lib/storage/migrations/index.js.map +1 -0
- package/lib/storage/migrations/registry.d.ts +19 -0
- package/lib/storage/migrations/registry.d.ts.map +1 -0
- package/lib/storage/migrations/registry.js +40 -0
- package/lib/storage/migrations/registry.js.map +1 -0
- package/lib/storage/migrations/v0-to-v1.d.ts +5 -0
- package/lib/storage/migrations/v0-to-v1.d.ts.map +1 -0
- package/lib/storage/migrations/v0-to-v1.js +11 -0
- package/lib/storage/migrations/v0-to-v1.js.map +1 -0
- package/lib/storage/notice.d.ts +4 -0
- package/lib/storage/notice.d.ts.map +1 -0
- package/lib/storage/notice.js +13 -0
- package/lib/storage/notice.js.map +1 -0
- package/lib/storage/preflight.d.ts +40 -0
- package/lib/storage/preflight.d.ts.map +1 -0
- package/lib/storage/preflight.js +208 -0
- package/lib/storage/preflight.js.map +1 -0
- package/lib/storage/safe-files.d.ts +6 -0
- package/lib/storage/safe-files.d.ts.map +1 -0
- package/lib/storage/safe-files.js +125 -0
- package/lib/storage/safe-files.js.map +1 -0
- package/lib/storage/source-archive.d.ts +14 -0
- package/lib/storage/source-archive.d.ts.map +1 -0
- package/lib/storage/source-archive.js +98 -0
- package/lib/storage/source-archive.js.map +1 -0
- package/lib/storage/split-state.d.ts +14 -0
- package/lib/storage/split-state.d.ts.map +1 -0
- package/lib/storage/split-state.js +107 -0
- package/lib/storage/split-state.js.map +1 -0
- package/lib/store.d.ts +5 -0
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +12 -1
- package/lib/store.js.map +1 -1
- package/lib/ui/storage-recovery-marker.d.ts +4 -0
- package/lib/ui/storage-recovery-marker.d.ts.map +1 -0
- package/lib/ui/storage-recovery-marker.js +19 -0
- package/lib/ui/storage-recovery-marker.js.map +1 -0
- package/lib/ui/storage-recovery.d.ts +20 -0
- package/lib/ui/storage-recovery.d.ts.map +1 -0
- package/lib/ui/storage-recovery.js +39 -0
- package/lib/ui/storage-recovery.js.map +1 -0
- package/lib/ui/storage-settings.d.ts +2 -0
- package/lib/ui/storage-settings.d.ts.map +1 -0
- package/lib/ui/storage-settings.js +90 -0
- package/lib/ui/storage-settings.js.map +1 -0
- package/lib/ui/workspace-navigation.d.ts +78 -0
- package/lib/ui/workspace-navigation.d.ts.map +1 -0
- package/lib/ui/workspace-navigation.js +28 -0
- package/lib/ui/workspace-navigation.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function migrationPath(steps, from, to) {
|
|
2
|
+
if (![from, to].every(version => Number.isSafeInteger(version) && version >= 0))
|
|
3
|
+
throw Error('迁移版本必须是非负整数');
|
|
4
|
+
if (from > to)
|
|
5
|
+
throw Error('不支持自动降级数据目录');
|
|
6
|
+
const bySource = new Map();
|
|
7
|
+
const ids = new Set();
|
|
8
|
+
for (const step of steps) {
|
|
9
|
+
if (!step.id || ids.has(step.id) || bySource.has(step.from))
|
|
10
|
+
throw Error('迁移版本注册重复');
|
|
11
|
+
if (!Number.isSafeInteger(step.from) || step.from < 0 || step.to !== step.from + 1)
|
|
12
|
+
throw Error('迁移必须逐版本递增');
|
|
13
|
+
ids.add(step.id);
|
|
14
|
+
bySource.set(step.from, step);
|
|
15
|
+
}
|
|
16
|
+
const result = [];
|
|
17
|
+
for (let version = from; version < to; version++) {
|
|
18
|
+
const step = bySource.get(version);
|
|
19
|
+
if (!step)
|
|
20
|
+
throw Error(`缺少 v${version} → v${version + 1} 迁移脚本`);
|
|
21
|
+
result.push(step);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
/** The caller must hold its runtime write gate throughout. On any error stop:
|
|
26
|
+
* reopen the persisted version/journal before retrying (including commit errors).
|
|
27
|
+
* This runner does not guess whether a failed commit reached durable storage.
|
|
28
|
+
*/
|
|
29
|
+
export async function runMigrationPath(steps, from, to, context, commitVersion) {
|
|
30
|
+
const path = migrationPath(steps, from, to);
|
|
31
|
+
// Refuse incomplete chains before touching even the first step.
|
|
32
|
+
if (path.some(step => !step.execute || !step.verify))
|
|
33
|
+
throw Error('迁移链尚未全部实现,拒绝执行');
|
|
34
|
+
for (const step of path) {
|
|
35
|
+
await step.execute(context);
|
|
36
|
+
await step.verify(context);
|
|
37
|
+
await commitVersion(step.from, step.to, step.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/storage/migrations/registry.ts"],"names":[],"mappings":"AAaA,MAAM,UAAU,aAAa,CAAU,KAA2C,EAAE,IAAY,EAAE,EAAU;IAC1G,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,CAAC;QAAE,MAAM,KAAK,CAAC,aAAa,CAAC,CAAA;IAC3G,IAAI,IAAI,GAAG,EAAE;QAAE,MAAM,KAAK,CAAC,aAAa,CAAC,CAAA;IACzC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAqC,CAAA;IAC7D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAA;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,MAAM,KAAK,CAAC,UAAU,CAAC,CAAA;QACpF,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC;YAAE,MAAM,KAAK,CAAC,WAAW,CAAC,CAAA;QAC5G,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IACjD,CAAC;IACD,MAAM,MAAM,GAAgC,EAAE,CAAA;IAC9C,KAAK,IAAI,OAAO,GAAG,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC;QACjD,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAClC,IAAI,CAAC,IAAI;YAAE,MAAM,KAAK,CAAC,OAAO,OAAO,OAAO,OAAO,GAAG,CAAC,OAAO,CAAC,CAAA;QAC/D,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACnB,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAU,KAA2C,EAAE,IAAY,EAAE,EAAU,EAAE,OAAgB,EAAE,aAAqF;IAC5N,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;IAC3C,gEAAgE;IAChE,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,KAAK,CAAC,gBAAgB,CAAC,CAAA;IACnF,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,IAAI,CAAC,OAAQ,CAAC,OAAO,CAAC,CAAA;QAC5B,MAAM,IAAI,CAAC,MAAO,CAAC,OAAO,CAAC,CAAA;QAC3B,MAAM,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IAClD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StorageMigration } from './registry.js';
|
|
2
|
+
import { type LayoutMigrationContext } from '../migrate-layout.js';
|
|
3
|
+
/** Owns only the v0 -> v1 transition. Later layouts get separate modules. */
|
|
4
|
+
export declare const v0ToV1: StorageMigration<LayoutMigrationContext>;
|
|
5
|
+
//# sourceMappingURL=v0-to-v1.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v0-to-v1.d.ts","sourceRoot":"","sources":["../../../src/storage/migrations/v0-to-v1.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAwC,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAExG,6EAA6E;AAC7E,eAAO,MAAM,MAAM,EAAE,gBAAgB,CAAC,sBAAsB,CAO3D,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { migrateLayout, verifyInstalledLayout } from '../migrate-layout.js';
|
|
2
|
+
/** Owns only the v0 -> v1 transition. Later layouts get separate modules. */
|
|
3
|
+
export const v0ToV1 = {
|
|
4
|
+
id: 'v0-to-v1-private-public-layout',
|
|
5
|
+
from: 0,
|
|
6
|
+
to: 1,
|
|
7
|
+
title: '公共数据归档与伙伴 .partner 私有目录',
|
|
8
|
+
execute: async (context) => { context.result = await migrateLayout(context.statePath, context.root, context.state); },
|
|
9
|
+
verify: verifyInstalledLayout,
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=v0-to-v1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v0-to-v1.js","sourceRoot":"","sources":["../../../src/storage/migrations/v0-to-v1.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAA+B,MAAM,sBAAsB,CAAA;AAExG,6EAA6E;AAC7E,MAAM,CAAC,MAAM,MAAM,GAA6C;IAC9D,EAAE,EAAE,gCAAgC;IACpC,IAAI,EAAE,CAAC;IACP,EAAE,EAAE,CAAC;IACL,KAAK,EAAE,yBAAyB;IAChC,OAAO,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE,GAAG,OAAO,CAAC,MAAM,GAAC,MAAM,aAAa,CAAC,OAAO,CAAC,SAAS,EAAC,OAAO,CAAC,IAAI,EAAC,OAAO,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC;IAC9G,MAAM,EAAE,qBAAqB;CAC9B,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PartnerInboxStore } from '../notifications/store.js';
|
|
2
|
+
/** Startup-only, local system notice. No model, channel delivery or directory scan. */
|
|
3
|
+
export declare function syncStorageNotice(inbox: PartnerInboxStore, current: number, target: number): void;
|
|
4
|
+
//# sourceMappingURL=notice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notice.d.ts","sourceRoot":"","sources":["../../src/storage/notice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAElE,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CASjG"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Startup-only, local system notice. No model, channel delivery or directory scan. */
|
|
2
|
+
export function syncStorageNotice(inbox, current, target) {
|
|
3
|
+
const id = `system:storage-migration:${target}`;
|
|
4
|
+
const pending = current < target;
|
|
5
|
+
const previous = inbox.snapshot().items.filter(item => item.kind === 'system' && item.action === 'storage-migration');
|
|
6
|
+
inbox.remove(previous.filter(item => !pending || item.id !== id).map(item => item.id));
|
|
7
|
+
if (!pending)
|
|
8
|
+
return;
|
|
9
|
+
inbox.append({ id, kind: 'system', action: 'storage-migration', companionId: '', companionName: '伙伴插件',
|
|
10
|
+
title: '需要升级数据目录',
|
|
11
|
+
summary: `当前存储版本 **${current}**,可升级到 **${target}**。\n\n升级会整理公共数据及各伙伴的私有目录。请先备份,再进入「设置 → 基本设置 → 升级迁移」检查并确认。\n\n**不会自动迁移**,点击下方「前往升级迁移」打开设置。`, createdAt: Date.now() });
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=notice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notice.js","sourceRoot":"","sources":["../../src/storage/notice.ts"],"names":[],"mappings":"AAEA,uFAAuF;AACvF,MAAM,UAAU,iBAAiB,CAAC,KAAwB,EAAE,OAAe,EAAE,MAAc;IACzF,MAAM,EAAE,GAAG,4BAA4B,MAAM,EAAE,CAAA;IAC/C,MAAM,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;IAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,mBAAmB,CAAC,CAAA;IACrH,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;IACtF,IAAI,CAAC,OAAO;QAAE,OAAM;IACpB,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,mBAAmB,EAAE,WAAW,EAAE,EAAE,EAAE,aAAa,EAAE,MAAM;QACpG,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,YAAY,OAAO,aAAa,MAAM,4FAA4F,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;AACzK,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PartnerState } from '../domain.js';
|
|
2
|
+
export interface StorageCheckItem {
|
|
3
|
+
label: string;
|
|
4
|
+
source: string;
|
|
5
|
+
target: string;
|
|
6
|
+
files: number;
|
|
7
|
+
bytes: number;
|
|
8
|
+
exists: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface StorageInspection {
|
|
11
|
+
currentVersion: number;
|
|
12
|
+
targetVersion: number;
|
|
13
|
+
migrationAvailable: boolean;
|
|
14
|
+
consistentSnapshot: false;
|
|
15
|
+
items: StorageCheckItem[];
|
|
16
|
+
blockers: string[];
|
|
17
|
+
notices: string[];
|
|
18
|
+
checkedAt: number;
|
|
19
|
+
steps: {
|
|
20
|
+
id: string;
|
|
21
|
+
from: number;
|
|
22
|
+
to: number;
|
|
23
|
+
title: string;
|
|
24
|
+
available: boolean;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
/** Read-only inventory. Never opens SQLite or reads config/credential payloads.
|
|
28
|
+
* Live sizes are estimates; the eventual migration must recheck under a gate.
|
|
29
|
+
*/
|
|
30
|
+
export declare class StoragePreflight {
|
|
31
|
+
private readonly statePath;
|
|
32
|
+
private readonly root;
|
|
33
|
+
private readonly version;
|
|
34
|
+
private readonly snapshot;
|
|
35
|
+
private pending;
|
|
36
|
+
constructor(statePath: string, root: string, version: number, snapshot: () => PartnerState);
|
|
37
|
+
inspect(): Promise<StorageInspection>;
|
|
38
|
+
private scan;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../../src/storage/preflight.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAKhD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;CAChB;AACD,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,kBAAkB,EAAE,KAAK,CAAA;IACzB,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CACrF;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAEf,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAU,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAU,OAAO,CAAC,QAAQ,CAAC,OAAO;IAAU,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAD1I,OAAO,CAAC,OAAO,CAAwC;gBAC1B,SAAS,EAAE,MAAM,EAAmB,IAAI,EAAE,MAAM,EAAmB,OAAO,EAAE,MAAM,EAAmB,QAAQ,EAAE,MAAM,YAAY;IAE9J,OAAO,IAAI,OAAO,CAAC,iBAAiB,CAAC;YAOvB,IAAI;CAmFnB"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { access, lstat, readdir, readFile, statfs } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { storageLayout, storageVersion, TARGET_STORAGE_VERSION } from './layout.js';
|
|
5
|
+
import { migrationPath, storageMigrations } from './migrations/index.js';
|
|
6
|
+
import { legacyMemoryBackups } from './legacy-backups.js';
|
|
7
|
+
/** Read-only inventory. Never opens SQLite or reads config/credential payloads.
|
|
8
|
+
* Live sizes are estimates; the eventual migration must recheck under a gate.
|
|
9
|
+
*/
|
|
10
|
+
export class StoragePreflight {
|
|
11
|
+
statePath;
|
|
12
|
+
root;
|
|
13
|
+
version;
|
|
14
|
+
snapshot;
|
|
15
|
+
pending;
|
|
16
|
+
constructor(statePath, root, version, snapshot) {
|
|
17
|
+
this.statePath = statePath;
|
|
18
|
+
this.root = root;
|
|
19
|
+
this.version = version;
|
|
20
|
+
this.snapshot = snapshot;
|
|
21
|
+
}
|
|
22
|
+
inspect() {
|
|
23
|
+
if (this.pending)
|
|
24
|
+
return this.pending;
|
|
25
|
+
const work = this.scan().finally(() => { if (this.pending === work)
|
|
26
|
+
this.pending = undefined; });
|
|
27
|
+
this.pending = work;
|
|
28
|
+
return work;
|
|
29
|
+
}
|
|
30
|
+
async scan() {
|
|
31
|
+
const state = this.snapshot();
|
|
32
|
+
const layout = storageLayout(resolve(this.statePath), resolve(this.root));
|
|
33
|
+
const result = {
|
|
34
|
+
currentVersion: storageVersion(this.version), targetVersion: TARGET_STORAGE_VERSION,
|
|
35
|
+
migrationAvailable: this.version < TARGET_STORAGE_VERSION, consistentSnapshot: false, items: [], blockers: [],
|
|
36
|
+
steps: migrationPath(storageMigrations, this.version, TARGET_STORAGE_VERSION).map(step => ({ id: step.id, from: step.from, to: step.to, title: step.title, available: !!step.execute && !!step.verify })),
|
|
37
|
+
notices: ['检查不会修改配置或数据库;运行中的文件数量与大小仅作预估。', '版本由迁移流程维护,请勿手动修改;公共和私有数据全部校验后才提交版本。', '凭据、宿主会话与知识库正文不搬动;工作文件和记录文档保持原位。'], checkedAt: Date.now(),
|
|
38
|
+
};
|
|
39
|
+
if (this.version === TARGET_STORAGE_VERSION) {
|
|
40
|
+
result.notices.push(`已使用新版公共目录:${layout.publicRoot}`);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
let recoveryId;
|
|
44
|
+
try {
|
|
45
|
+
const journal = JSON.parse(await readFile(join(layout.legacyPublic, 'storage-migration.json'), 'utf8'));
|
|
46
|
+
if (typeof journal.id === 'string')
|
|
47
|
+
recoveryId = journal.id;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (!missing(error))
|
|
51
|
+
result.blockers.push('迁移恢复记录无法读取');
|
|
52
|
+
}
|
|
53
|
+
let budget = 50_000;
|
|
54
|
+
const inspectTree = async (path, device) => {
|
|
55
|
+
if (--budget < 0)
|
|
56
|
+
throw Error('文件数量超过检查上限,请先核对目录范围');
|
|
57
|
+
let entry;
|
|
58
|
+
try {
|
|
59
|
+
entry = await lstat(path);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (missing(error))
|
|
63
|
+
return { exists: false, files: 0, bytes: 0 };
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
if (entry.isSymbolicLink())
|
|
67
|
+
throw Error('包含符号链接,需人工确认');
|
|
68
|
+
if (device !== undefined && entry.dev !== device)
|
|
69
|
+
throw Error('跨文件系统目录,需人工确认');
|
|
70
|
+
if (entry.isFile()) {
|
|
71
|
+
await access(path, constants.R_OK);
|
|
72
|
+
return { exists: true, files: 1, bytes: entry.size };
|
|
73
|
+
}
|
|
74
|
+
if (!entry.isDirectory())
|
|
75
|
+
throw Error('包含特殊文件,需人工确认');
|
|
76
|
+
let files = 0, bytes = 0;
|
|
77
|
+
for (const child of await readdir(path)) {
|
|
78
|
+
const value = await inspectTree(join(path, child), entry.dev);
|
|
79
|
+
files += value.files;
|
|
80
|
+
bytes += value.bytes;
|
|
81
|
+
}
|
|
82
|
+
return { exists: true, files, bytes };
|
|
83
|
+
};
|
|
84
|
+
const inspect = async (label, source, target) => {
|
|
85
|
+
try {
|
|
86
|
+
await assertAncestors(source);
|
|
87
|
+
const value = await inspectTree(source);
|
|
88
|
+
result.items.push({ label, source, target, ...value });
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
result.blockers.push(`${label}:${safeError(error)}`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const targets = [layout.publicRoot];
|
|
95
|
+
await inspect('公共配置(待拆分私有字段)', resolve(this.statePath), join(layout.publicRoot, 'state'));
|
|
96
|
+
await inspect('Skill 安装库', layout.legacySkills, join(layout.publicRoot, 'skills'));
|
|
97
|
+
await inspect('消息索引(待按伙伴拆分)', join(layout.legacyPublic, 'partner-inbox.sqlite'), join(layout.publicRoot, 'indexes'));
|
|
98
|
+
await inspect('附件交付(待按伙伴拆分)', join(layout.legacyPublic, 'attachment-deliveries'), join(layout.publicRoot, 'indexes'));
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
for (const companion of state.companions) {
|
|
101
|
+
if (seen.has(companion.id)) {
|
|
102
|
+
result.blockers.push('伙伴 ID 重复,不能迁移');
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
seen.add(companion.id);
|
|
106
|
+
let source, target;
|
|
107
|
+
try {
|
|
108
|
+
source = layout.legacyCompanion(companion.id);
|
|
109
|
+
target = layout.privateRoot(companion.id);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
result.blockers.push('伙伴 ID 无效,不能迁移');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
targets.push(target);
|
|
116
|
+
await inspect(`${companion.name} · 记忆`, join(source, 'memory'), join(target, 'memory'));
|
|
117
|
+
await inspect(`${companion.name} · 关注`, join(source, 'concerns'), join(target, 'concerns'));
|
|
118
|
+
try {
|
|
119
|
+
for (const name of await legacyMemoryBackups(source)) {
|
|
120
|
+
await inspect(`${companion.name} · ${name}`, join(source, name), join(target, 'backups', 'legacy-memory', name));
|
|
121
|
+
result.notices.push(`${companion.name} · ${name} 原样归档,不合并到当前记忆`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (!missing(error))
|
|
126
|
+
result.blockers.push(`${companion.name} · 备份检查:${safeError(error)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const total = result.items.reduce((sum, item) => sum + item.bytes, 0);
|
|
130
|
+
for (const target of targets) {
|
|
131
|
+
try {
|
|
132
|
+
await assertAncestors(target);
|
|
133
|
+
let ancestor = target;
|
|
134
|
+
try {
|
|
135
|
+
await lstat(target);
|
|
136
|
+
let owned = false;
|
|
137
|
+
if (recoveryId)
|
|
138
|
+
try {
|
|
139
|
+
const marker = JSON.parse(await readFile(join(target, 'migration-owner.json'), 'utf8'));
|
|
140
|
+
owned = marker.transactionId === recoveryId;
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
if (!owned)
|
|
144
|
+
result.blockers.push(`目标目录已存在,不允许覆盖:${target}`);
|
|
145
|
+
else
|
|
146
|
+
result.notices.push(`发现未提交的迁移,可安全重试:${target}`);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!missing(error))
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
while (true) {
|
|
153
|
+
try {
|
|
154
|
+
await lstat(ancestor);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!missing(error))
|
|
159
|
+
throw error;
|
|
160
|
+
ancestor = dirname(ancestor);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await access(ancestor, constants.W_OK | constants.X_OK);
|
|
164
|
+
const fs = await statfs(ancestor);
|
|
165
|
+
// Deliberately conservative until an exact per-volume copy plan exists.
|
|
166
|
+
if (fs.bavail * fs.bsize < total * 3)
|
|
167
|
+
result.blockers.push(`目标磁盘可用空间不足(含完整备份):${target}`);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
result.blockers.push(`目标目录 ${target}:${safeError(error)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
result.notices.push(`已识别 ${state.companions.length} 位伙伴;私有配置还需从公共 state.json 按归属拆分,不是整份复制到每个伙伴。`);
|
|
174
|
+
result.migrationAvailable = result.blockers.length === 0;
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function assertAncestors(path) {
|
|
179
|
+
const paths = [];
|
|
180
|
+
for (let current = resolve(path);;) {
|
|
181
|
+
paths.push(current);
|
|
182
|
+
const parent = dirname(current);
|
|
183
|
+
if (parent === current)
|
|
184
|
+
break;
|
|
185
|
+
current = parent;
|
|
186
|
+
}
|
|
187
|
+
for (const current of paths.reverse()) {
|
|
188
|
+
try {
|
|
189
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
190
|
+
throw Error('路径含符号链接,需人工确认');
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (missing(error))
|
|
194
|
+
return;
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function missing(error) { return error?.code === 'ENOENT'; }
|
|
200
|
+
function safeError(error) {
|
|
201
|
+
const code = error?.code;
|
|
202
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
203
|
+
return '目录读写权限不足';
|
|
204
|
+
if (code)
|
|
205
|
+
return `文件检查失败(${code})`;
|
|
206
|
+
return error instanceof Error ? error.message : '检查失败';
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.js","sourceRoot":"","sources":["../../src/storage/preflight.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function exists(path: string): Promise<boolean>;
|
|
2
|
+
export declare function safeTree(path: string): Promise<string[]>;
|
|
3
|
+
export declare function fileHash(path: string): Promise<string>;
|
|
4
|
+
export declare function copyVerified(source: string, target: string): Promise<void>;
|
|
5
|
+
export declare function verifyDatabases(root: string): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=safe-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-files.d.ts","sourceRoot":"","sources":["../../src/storage/safe-files.ts"],"names":[],"mappings":"AAOA,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE3D;AACD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAyB9D;AACD,wBAAsB,QAAQ,CAAC,IAAI,EAAC,MAAM,GAAE,OAAO,CAAC,MAAM,CAAC,CAG1D;AACD,wBAAsB,YAAY,CAAC,MAAM,EAAC,MAAM,EAAC,MAAM,EAAC,MAAM,GAAE,OAAO,CAAC,IAAI,CAAC,CAkB5E;AACD,wBAAsB,eAAe,CAAC,IAAI,EAAC,MAAM,GAAE,OAAO,CAAC,IAAI,CAAC,CAU/D"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, mkdir, open, readdir, readFile, copyFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { syncDirectory } from './atomic.js';
|
|
7
|
+
export async function exists(path) {
|
|
8
|
+
try {
|
|
9
|
+
await lstat(path);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
if (e.code === 'ENOENT')
|
|
14
|
+
return false;
|
|
15
|
+
throw e;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function safeTree(path) {
|
|
19
|
+
const absolute = resolve(path), ancestors = [];
|
|
20
|
+
for (let p = absolute;;) {
|
|
21
|
+
ancestors.push(p);
|
|
22
|
+
const parent = dirname(p);
|
|
23
|
+
if (p === parent)
|
|
24
|
+
break;
|
|
25
|
+
p = parent;
|
|
26
|
+
}
|
|
27
|
+
for (const p of ancestors.reverse()) {
|
|
28
|
+
if (!await exists(p))
|
|
29
|
+
break;
|
|
30
|
+
if ((await lstat(p)).isSymbolicLink())
|
|
31
|
+
throw Error('迁移路径不允许符号链接');
|
|
32
|
+
}
|
|
33
|
+
if (!await exists(absolute))
|
|
34
|
+
return [];
|
|
35
|
+
if (process.platform === 'linux') {
|
|
36
|
+
const mounts = await readFile('/proc/self/mountinfo', 'utf8');
|
|
37
|
+
for (const line of mounts.split('\n')) {
|
|
38
|
+
const mount = line.split(' ')[4]?.replace(/\\([0-7]{3})/g, (_, n) => String.fromCharCode(parseInt(n, 8)));
|
|
39
|
+
if (mount === absolute || mount?.startsWith(absolute + sep))
|
|
40
|
+
throw Error('迁移目录包含挂载点,需人工处理');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const files = [], pending = [absolute];
|
|
44
|
+
let count = 0;
|
|
45
|
+
while (pending.length) {
|
|
46
|
+
if (++count > 100_000)
|
|
47
|
+
throw Error('迁移目录文件过多');
|
|
48
|
+
const p = pending.pop(), stat = await lstat(p);
|
|
49
|
+
if (stat.isSymbolicLink())
|
|
50
|
+
throw Error('迁移目录包含符号链接');
|
|
51
|
+
if (stat.isDirectory())
|
|
52
|
+
for (const child of await readdir(p))
|
|
53
|
+
pending.push(join(p, child));
|
|
54
|
+
else if (stat.isFile())
|
|
55
|
+
files.push(p);
|
|
56
|
+
else
|
|
57
|
+
throw Error('迁移目录包含特殊文件');
|
|
58
|
+
}
|
|
59
|
+
return files.sort();
|
|
60
|
+
}
|
|
61
|
+
export async function fileHash(path) {
|
|
62
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
63
|
+
try {
|
|
64
|
+
const hash = createHash('sha256');
|
|
65
|
+
for await (const chunk of file.createReadStream({ autoClose: false }))
|
|
66
|
+
hash.update(chunk);
|
|
67
|
+
return hash.digest('hex');
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await file.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function copyVerified(source, target) {
|
|
74
|
+
const files = await safeTree(source);
|
|
75
|
+
if (!await exists(source))
|
|
76
|
+
return;
|
|
77
|
+
const directory = (await lstat(source)).isDirectory();
|
|
78
|
+
if (directory) {
|
|
79
|
+
const directories = [source];
|
|
80
|
+
while (directories.length) {
|
|
81
|
+
const current = directories.pop();
|
|
82
|
+
await mkdir(join(target, current.slice(source.length)), { recursive: true, mode: 0o700 });
|
|
83
|
+
for (const item of await readdir(current, { withFileTypes: true }))
|
|
84
|
+
if (item.isDirectory())
|
|
85
|
+
directories.push(join(current, item.name));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const destination = directory ? join(target, file.slice(source.length + 1)) : target;
|
|
90
|
+
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
|
|
91
|
+
await copyFile(file, destination, constants.COPYFILE_EXCL);
|
|
92
|
+
const output = await open(destination, 'r+');
|
|
93
|
+
try {
|
|
94
|
+
await output.chmod(0o600);
|
|
95
|
+
await output.sync();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await output.close();
|
|
99
|
+
}
|
|
100
|
+
if (await fileHash(file) !== await fileHash(destination))
|
|
101
|
+
throw Error('文件复制校验失败');
|
|
102
|
+
await syncDirectory(dirname(destination));
|
|
103
|
+
}
|
|
104
|
+
if (directory)
|
|
105
|
+
await syncDirectory(target);
|
|
106
|
+
await syncDirectory(dirname(target));
|
|
107
|
+
}
|
|
108
|
+
export async function verifyDatabases(root) {
|
|
109
|
+
for (const path of await safeTree(root))
|
|
110
|
+
if (path.endsWith('.sqlite')) {
|
|
111
|
+
const db = new DatabaseSync(path);
|
|
112
|
+
try {
|
|
113
|
+
const rows = db.prepare('PRAGMA integrity_check').all();
|
|
114
|
+
if (rows.length !== 1 || Object.values(rows[0])[0] !== 'ok')
|
|
115
|
+
throw Error('SQLite 完整性检查失败');
|
|
116
|
+
const checkpoint = db.prepare('PRAGMA wal_checkpoint(TRUNCATE)').get();
|
|
117
|
+
if (checkpoint && Number(checkpoint.busy) !== 0)
|
|
118
|
+
throw Error('SQLite 仍在使用中');
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
db.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=safe-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-files.js","sourceRoot":"","sources":["../../src/storage/safe-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CleanupResult {
|
|
2
|
+
pending: number;
|
|
3
|
+
removed: number;
|
|
4
|
+
warnings: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function archiveRoot(statePath: string, id: string): string;
|
|
7
|
+
/** Called under the migration write gate. Complete backups precede activation. */
|
|
8
|
+
export declare function prepareSourceArchive(statePath: string, root: string, id: string, owners: string[]): Promise<void>;
|
|
9
|
+
/** No source is removed until both the active version and its backup verify.
|
|
10
|
+
* A changed source or incomplete backup is left intact and reported, not guessed.
|
|
11
|
+
* Only called while services are stopped, never as an automatic startup sweep.
|
|
12
|
+
*/
|
|
13
|
+
export declare function cleanupArchivedSources(statePath: string, root: string, id: string): Promise<CleanupResult>;
|
|
14
|
+
//# sourceMappingURL=source-archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-archive.d.ts","sourceRoot":"","sources":["../../src/storage/source-archive.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAAG,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE;AAyBvF,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAGjE;AACD,kFAAkF;AAClF,wBAAsB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBvH;AACD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqBhH"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFile, rm, lstat, readdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { atomicJson } from './atomic.js';
|
|
5
|
+
import { copyVerified, exists, fileHash, safeTree } from './safe-files.js';
|
|
6
|
+
import { storageLayout } from './layout.js';
|
|
7
|
+
import { isLegacyMemoryBackup, legacyMemoryBackups } from './legacy-backups.js';
|
|
8
|
+
import { SplitStatePersistence } from './split-state.js';
|
|
9
|
+
const validId = (id) => { if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(id))
|
|
10
|
+
throw Error('备份事务标识无效'); };
|
|
11
|
+
function sourcePath(statePath, root, source) {
|
|
12
|
+
const layout = storageLayout(resolve(statePath), resolve(root));
|
|
13
|
+
switch (source.kind) {
|
|
14
|
+
case 'state': return resolve(statePath);
|
|
15
|
+
case 'inbox': return join(layout.legacyPublic, 'partner-inbox.sqlite');
|
|
16
|
+
case 'inbox-wal': return join(layout.legacyPublic, 'partner-inbox.sqlite-wal');
|
|
17
|
+
case 'inbox-shm': return join(layout.legacyPublic, 'partner-inbox.sqlite-shm');
|
|
18
|
+
case 'deliveries': return join(layout.legacyPublic, 'attachment-deliveries');
|
|
19
|
+
case 'skills': return layout.legacySkills;
|
|
20
|
+
case 'private':
|
|
21
|
+
if (!['memory', 'concerns'].includes(source.name) && !isLegacyMemoryBackup(source.name))
|
|
22
|
+
throw Error('非迁移范围目录,拒绝清理');
|
|
23
|
+
return join(layout.legacyCompanion(source.owner), source.name);
|
|
24
|
+
default: throw Error('未知迁移源,拒绝清理');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function digest(path) {
|
|
28
|
+
const hash = createHash('sha256');
|
|
29
|
+
const dirs = async (p) => { const entry = await lstat(p); hash.update(JSON.stringify([relative(path, p), entry.isDirectory() ? 'directory' : 'file'])); if (entry.isDirectory())
|
|
30
|
+
for (const name of (await readdir(p)).sort())
|
|
31
|
+
await dirs(join(p, name)); };
|
|
32
|
+
await safeTree(path);
|
|
33
|
+
await dirs(path);
|
|
34
|
+
for (const file of await safeTree(path))
|
|
35
|
+
hash.update(JSON.stringify([relative(path, file), await fileHash(file)]));
|
|
36
|
+
return hash.digest('hex');
|
|
37
|
+
}
|
|
38
|
+
export function archiveRoot(statePath, id) {
|
|
39
|
+
validId(id);
|
|
40
|
+
return join(dirname(resolve(statePath)), 'migration-backups', id);
|
|
41
|
+
}
|
|
42
|
+
/** Called under the migration write gate. Complete backups precede activation. */
|
|
43
|
+
export async function prepareSourceArchive(statePath, root, id, owners) {
|
|
44
|
+
const backup = archiveRoot(statePath, id);
|
|
45
|
+
const sources = [{ kind: 'state' }, { kind: 'inbox' }, { kind: 'inbox-wal' }, { kind: 'inbox-shm' }, { kind: 'deliveries' }, { kind: 'skills' }];
|
|
46
|
+
const layout = storageLayout(resolve(statePath), resolve(root));
|
|
47
|
+
for (const owner of owners)
|
|
48
|
+
for (const name of ['memory', 'concerns', ...await legacyMemoryBackups(layout.legacyCompanion(owner))])
|
|
49
|
+
sources.push({ kind: 'private', owner, name });
|
|
50
|
+
const entries = [];
|
|
51
|
+
for (const source of sources) {
|
|
52
|
+
const path = sourcePath(statePath, root, source);
|
|
53
|
+
if (!await exists(path))
|
|
54
|
+
continue;
|
|
55
|
+
const target = join(backup, 'originals', String(entries.length));
|
|
56
|
+
await copyVerified(path, target);
|
|
57
|
+
const fingerprint = await digest(path);
|
|
58
|
+
if (fingerprint !== await digest(target))
|
|
59
|
+
throw Error('完整备份校验失败');
|
|
60
|
+
entries.push({ source, digest: fingerprint });
|
|
61
|
+
}
|
|
62
|
+
await atomicJson(join(backup, 'archive.json'), { version: 1, transactionId: id, entries });
|
|
63
|
+
}
|
|
64
|
+
/** No source is removed until both the active version and its backup verify.
|
|
65
|
+
* A changed source or incomplete backup is left intact and reported, not guessed.
|
|
66
|
+
* Only called while services are stopped, never as an automatic startup sweep.
|
|
67
|
+
*/
|
|
68
|
+
export async function cleanupArchivedSources(statePath, root, id) {
|
|
69
|
+
const backup = archiveRoot(statePath, id);
|
|
70
|
+
const config = JSON.parse(await readFile(join(storageLayout(resolve(statePath), resolve(root)).legacyPublic, 'storage-config.json'), 'utf8'));
|
|
71
|
+
if (config.storageVersion !== 1 || config.transactionId !== id || config.workspaceRoot !== resolve(root))
|
|
72
|
+
throw Error('迁移版本未提交,禁止清理原文件');
|
|
73
|
+
const layout = storageLayout(resolve(statePath), resolve(root));
|
|
74
|
+
await new SplitStatePersistence(layout.publicRoot, layout.privateRoot).read();
|
|
75
|
+
const archive = JSON.parse(await readFile(join(backup, 'archive.json'), 'utf8'));
|
|
76
|
+
if (archive.version !== 1 || archive.transactionId !== id || !Array.isArray(archive.entries))
|
|
77
|
+
throw Error('备份清单无效');
|
|
78
|
+
const result = { pending: 0, removed: 0, warnings: [] };
|
|
79
|
+
for (const [index, entry] of archive.entries.entries()) {
|
|
80
|
+
try {
|
|
81
|
+
const path = sourcePath(statePath, root, entry.source);
|
|
82
|
+
if (!await exists(path))
|
|
83
|
+
continue;
|
|
84
|
+
const saved = join(backup, 'originals', String(index));
|
|
85
|
+
if (!await exists(saved) || await digest(saved) !== entry.digest || await digest(path) !== entry.digest)
|
|
86
|
+
throw Error('源文件或备份已变化');
|
|
87
|
+
await rm(path, { recursive: true, force: false });
|
|
88
|
+
result.removed++;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
result.pending++;
|
|
92
|
+
result.warnings.push(`第 ${index + 1} 项原路径未清理,已保留,请核查备份或文件占用。`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
await atomicJson(join(backup, 'cleanup.json'), result);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=source-archive.js.map
|