@narumitw/pi-sync 0.50.3 → 0.51.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 +4 -1
- package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
- package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
- package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
- package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
- package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
- package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
- package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
- package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
- package/dist/chunks/chunk-6546TS54.ts.map +7 -0
- package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
- package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
- package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
- package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
- package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
- package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
- package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
- package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
- package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
- package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
- package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
- package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
- package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
- package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
- package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
- package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
- package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
- package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
- package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
- package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
- package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
- package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
- package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
- package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
- package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
- package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
- package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
- package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
- package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
- package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
- package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
- package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
- package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
- package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
- package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
- package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
- package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
- package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
- package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
- package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
- package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
- package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
- package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
- package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
- package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
- package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
- package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
- package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
- package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
- package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
- package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
- package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
- package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
- package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
- package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
- package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
- package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
- package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
- package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
- package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
- package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
- package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
- package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
- package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
- package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
- package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
- package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
- package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
- package/dist/index.ts +49 -74
- package/dist/index.ts.map +3 -3
- package/docs/settings.md +9 -0
- package/package.json +61 -62
- package/src/backends/backend-factory.ts +15 -15
- package/src/backends/backend-identity.ts +31 -31
- package/src/backends/backend-types.ts +42 -42
- package/src/backends/git/git-backend.ts +714 -788
- package/src/backends/git/git-config.ts +104 -111
- package/src/backends/git/git-runner.ts +230 -247
- package/src/backends/git/git-storage.ts +228 -245
- package/src/backends/s3/s3-backend.ts +408 -441
- package/src/backends/s3/s3-client.ts +288 -333
- package/src/backends/sync-backend.ts +65 -65
- package/src/backends/webdav/webdav-backend.ts +542 -589
- package/src/backends/webdav/webdav-client.ts +292 -316
- package/src/backends/webdav/webdav-config.ts +57 -68
- package/src/commands/command-execution.ts +270 -289
- package/src/commands/command-handler.ts +160 -163
- package/src/commands/command-types.ts +13 -13
- package/src/commands/command.ts +190 -196
- package/src/commands/setup-completions.ts +1 -1
- package/src/paths.ts +47 -47
- package/src/settings/config-errors.ts +4 -5
- package/src/settings/config-file.ts +473 -506
- package/src/settings/config.ts +171 -174
- package/src/settings/settings-management.ts +191 -216
- package/src/settings/settings-store.ts +63 -77
- package/src/settings/settings-types.ts +78 -75
- package/src/settings/settings-validation.ts +328 -364
- package/src/snapshot/session-paths.ts +41 -55
- package/src/snapshot/snapshot-apply.ts +197 -229
- package/src/snapshot/snapshot-codec.ts +29 -31
- package/src/snapshot/snapshot-paths.ts +4 -6
- package/src/snapshot/snapshot-transaction.ts +161 -170
- package/src/snapshot/snapshot-types.ts +24 -24
- package/src/snapshot/snapshot.ts +310 -343
- package/src/state/json-file.ts +11 -11
- package/src/state/lock.ts +226 -242
- package/src/state/lockfile-fs.ts +20 -20
- package/src/state/operation-availability.ts +36 -41
- package/src/state/state-directory.ts +171 -173
- package/src/state/state-types.ts +15 -15
- package/src/state/sync-state-store.ts +23 -23
- package/src/sync/automatic-sync.ts +75 -80
- package/src/sync/remote-snapshot.ts +52 -60
- package/src/sync/setup-switch.ts +99 -114
- package/src/sync/signals.ts +3 -5
- package/src/sync/startup-check.ts +97 -111
- package/src/sync/sync-decision.ts +76 -80
- package/src/sync/sync-error-guidance.ts +41 -48
- package/src/sync/sync-errors.ts +10 -10
- package/src/sync/sync-inspection.ts +29 -33
- package/src/sync/sync-loaders.ts +28 -36
- package/src/sync/sync-mutations.ts +510 -622
- package/src/sync/sync-operations.ts +7 -7
- package/src/sync/sync-policy.ts +296 -326
- package/src/sync/sync-queries.ts +175 -194
- package/src/sync/sync-state.ts +74 -100
- package/src/sync-extension.ts +89 -101
- package/src/sync.ts +26 -26
- package/src/types.ts +37 -37
- package/src/ui/automatic-sync-summary.ts +1 -3
- package/src/ui/cancellable-operation.ts +89 -101
- package/src/ui/file-selection.ts +250 -274
- package/src/ui/manager-attention.ts +51 -63
- package/src/ui/manager-recovery.ts +48 -57
- package/src/ui/manager-result-dispatcher.ts +62 -76
- package/src/ui/manager-state.ts +200 -232
- package/src/ui/manager-ui.ts +264 -362
- package/src/ui/remote-selection-ui.ts +547 -588
- package/src/ui/secret-input.ts +197 -205
- package/src/ui/settings-ui.ts +175 -172
- package/src/ui/setup/git-ui.ts +232 -261
- package/src/ui/setup/s3-credentials-ui.ts +58 -65
- package/src/ui/setup/s3-ui.ts +171 -195
- package/src/ui/setup/setup-actions.ts +91 -101
- package/src/ui/setup/setup-location-ui.ts +121 -137
- package/src/ui/setup/setup-name-ui.ts +27 -27
- package/src/ui/setup/setup-prompts.ts +65 -65
- package/src/ui/setup/setup-review.ts +58 -59
- package/src/ui/setup/setup-switcher.ts +156 -160
- package/src/ui/setup/setup-wizard.ts +28 -28
- package/src/ui/setup/text-input.ts +54 -58
- package/src/ui/setup/webdav-ui.ts +246 -293
- package/src/ui/storage-connections-ui.ts +317 -369
- package/src/ui/sync-attention.ts +148 -164
- package/src/ui/sync-format.ts +110 -121
- package/src/ui/sync-resolution-ui.ts +147 -153
- package/src/ui/sync-setups-ui.ts +218 -238
- package/src/ui/sync-status.ts +17 -0
- package/src/ui/terminal-text.ts +2 -2
- package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
- package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
- package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
- package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
- package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
- package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
- package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
- package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
- package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
- package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
- package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
- package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
- package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
- package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
- package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
- package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
- package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
- package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
- package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
- package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
- package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
- package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
- package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
- package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
- package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
- package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
- package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
- package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
- package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
- package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
- package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
- package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
- package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
- package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
- package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
- package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
- package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
- /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
- /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
- /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
|
@@ -10,42 +10,40 @@ const MAX_DECOMPRESSED_SNAPSHOT_BYTES = 512 * 1024 * 1024;
|
|
|
10
10
|
const gzipAsync = promisify(gzip);
|
|
11
11
|
|
|
12
12
|
export async function encodeSnapshot(snapshot: Snapshot) {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
snapshotSelectionInclude(snapshot);
|
|
14
|
+
return gzipAsync(Buffer.from(JSON.stringify(snapshot), "utf8"));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export async function decodeSnapshot(
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
buffer: Buffer,
|
|
19
|
+
options: { signal?: AbortSignal; maxOutputLength?: number } = {},
|
|
20
20
|
): Promise<Snapshot> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
21
|
+
throwIfAborted(options.signal);
|
|
22
|
+
const limit = options.maxOutputLength ?? MAX_DECOMPRESSED_SNAPSHOT_BYTES;
|
|
23
|
+
const chunks: Buffer[] = [];
|
|
24
|
+
let total = 0;
|
|
25
|
+
const sink = new Writable({
|
|
26
|
+
write(chunk: Buffer, _encoding, callback) {
|
|
27
|
+
total += chunk.byteLength;
|
|
28
|
+
if (total > limit) {
|
|
29
|
+
callback(new Error(`Decompressed snapshot exceeds the ${limit}-byte limit.`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
chunks.push(Buffer.from(chunk));
|
|
33
|
+
callback();
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
await pipeline(Readable.from([buffer]), createGunzip(), sink, { signal: options.signal });
|
|
37
|
+
throwIfAborted(options.signal);
|
|
38
|
+
const parsed = JSON.parse(Buffer.concat(chunks, total).toString("utf8")) as Snapshot;
|
|
39
|
+
if (parsed.version !== VERSION || !Array.isArray(parsed.files)) {
|
|
40
|
+
throw new Error("Unsupported snapshot format.");
|
|
41
|
+
}
|
|
42
|
+
snapshotSelectionInclude(parsed);
|
|
43
|
+
return parsed;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function throwIfAborted(signal?: AbortSignal) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? signal.reason
|
|
50
|
-
: new DOMException("The operation was aborted", "AbortError");
|
|
47
|
+
if (!signal?.aborted) return;
|
|
48
|
+
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
51
49
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
function expandHome(value: string) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
if (value === "~") return process.env.HOME ?? value;
|
|
5
|
+
if (value.startsWith("~/")) return path.join(process.env.HOME ?? "~", value.slice(2));
|
|
6
|
+
return value;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function sessionStorageRoot(root: string, configuredSessionDir?: string) {
|
|
10
|
-
|
|
11
|
-
? path.resolve(expandHome(configuredSessionDir))
|
|
12
|
-
: path.resolve(root, "sessions");
|
|
10
|
+
return configuredSessionDir ? path.resolve(expandHome(configuredSessionDir)) : path.resolve(root, "sessions");
|
|
13
11
|
}
|
|
@@ -11,203 +11,194 @@ import type { SnapshotApplyPlan } from "./snapshot-types.js";
|
|
|
11
11
|
const JOURNAL_VERSION = 1;
|
|
12
12
|
|
|
13
13
|
interface TransactionEntry {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
target: string;
|
|
15
|
+
backupName: string;
|
|
16
|
+
kind: "missing" | "file" | "directory" | "symlink";
|
|
17
|
+
linkTarget?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface TransactionJournal {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
version: number;
|
|
22
|
+
root: string;
|
|
23
|
+
sessionRoot?: string;
|
|
24
|
+
entries: TransactionEntry[];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export async function applySnapshotTransaction(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
throw error;
|
|
52
|
-
}
|
|
27
|
+
export async function applySnapshotTransaction(plan: SnapshotApplyPlan, options: { sessionDir?: string } = {}) {
|
|
28
|
+
await recoverPendingSnapshotTransactions();
|
|
29
|
+
const transaction = await prepareTransaction(plan, options.sessionDir);
|
|
30
|
+
try {
|
|
31
|
+
for (const target of plan.deletes) {
|
|
32
|
+
await fs.rm(target, { force: true, recursive: true });
|
|
33
|
+
}
|
|
34
|
+
for (const item of plan.writes) {
|
|
35
|
+
await fs.mkdir(path.dirname(item.target), { recursive: true });
|
|
36
|
+
await fs.writeFile(item.target, item.content);
|
|
37
|
+
}
|
|
38
|
+
await fs.rm(transaction.directory, { recursive: true, force: true });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
try {
|
|
41
|
+
await restoreTransaction(transaction.directory, transaction.journal);
|
|
42
|
+
} catch (recoveryError) {
|
|
43
|
+
throw new AggregateError(
|
|
44
|
+
[error, recoveryError],
|
|
45
|
+
`Snapshot apply failed and automatic recovery also failed. Transaction retained at ${transaction.directory}.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
export async function recoverSnapshotTransactionsOnStartup() {
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
if (!(await pendingTransactionEntries()).some((entry) => entry.isDirectory())) return;
|
|
54
|
+
await withLock("recovery", recoverPendingSnapshotTransactions, { reclaimStale: true });
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
export async function recoverPendingSnapshotTransactions() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
58
|
+
const directory = transactionRoot();
|
|
59
|
+
const entries = await pendingTransactionEntries();
|
|
60
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
61
|
+
if (!entry.isDirectory()) continue;
|
|
62
|
+
const transactionDirectory = path.join(directory, entry.name);
|
|
63
|
+
const journalPath = path.join(transactionDirectory, "journal.json");
|
|
64
|
+
let journal: TransactionJournal;
|
|
65
|
+
try {
|
|
66
|
+
journal = JSON.parse(await fs.readFile(journalPath, "utf8")) as TransactionJournal;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
69
|
+
await fs.rm(transactionDirectory, { recursive: true, force: true });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Cannot recover malformed pi-sync transaction: ${journalPath}`, {
|
|
73
|
+
cause: error,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
await restoreTransaction(transactionDirectory, journal);
|
|
77
|
+
}
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
async function pendingTransactionEntries() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
try {
|
|
82
|
+
return await fs.readdir(transactionRoot(), { withFileTypes: true });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
async function prepareTransaction(plan: SnapshotApplyPlan, sessionDir?: string) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
mode: 0o600,
|
|
146
|
-
},
|
|
147
|
-
);
|
|
148
|
-
return { directory, journal };
|
|
90
|
+
const root = path.resolve(agentDir());
|
|
91
|
+
const sessionRoot = sessionDir ? path.resolve(sessionStorageRoot(root, sessionDir)) : undefined;
|
|
92
|
+
const directory = path.join(transactionRoot(), randomUUID());
|
|
93
|
+
const backupDirectory = path.join(directory, "before");
|
|
94
|
+
await fs.mkdir(backupDirectory, { recursive: true, mode: 0o700 });
|
|
95
|
+
const targets = [...new Set([...plan.deletes, ...plan.writes.map((item) => item.target)])].sort();
|
|
96
|
+
const entries: TransactionEntry[] = [];
|
|
97
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
98
|
+
const target = targets[index];
|
|
99
|
+
if (!target) continue;
|
|
100
|
+
assertAllowedTarget(root, sessionRoot, target);
|
|
101
|
+
const backupName = `${index}`;
|
|
102
|
+
const backupPath = path.join(backupDirectory, backupName);
|
|
103
|
+
try {
|
|
104
|
+
const stat = await fs.lstat(target);
|
|
105
|
+
if (stat.isSymbolicLink()) {
|
|
106
|
+
entries.push({
|
|
107
|
+
target,
|
|
108
|
+
backupName,
|
|
109
|
+
kind: "symlink",
|
|
110
|
+
linkTarget: await fs.readlink(target),
|
|
111
|
+
});
|
|
112
|
+
} else if (stat.isDirectory()) {
|
|
113
|
+
await fs.cp(target, backupPath, {
|
|
114
|
+
recursive: true,
|
|
115
|
+
dereference: false,
|
|
116
|
+
preserveTimestamps: true,
|
|
117
|
+
});
|
|
118
|
+
entries.push({ target, backupName, kind: "directory" });
|
|
119
|
+
} else if (stat.isFile()) {
|
|
120
|
+
await fs.copyFile(target, backupPath);
|
|
121
|
+
await fs.chmod(backupPath, stat.mode);
|
|
122
|
+
entries.push({ target, backupName, kind: "file" });
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(`Unsupported existing snapshot target: ${target}`);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
128
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
129
|
+
entries.push({ target, backupName, kind: "missing" });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const journal: TransactionJournal = {
|
|
133
|
+
version: JOURNAL_VERSION,
|
|
134
|
+
root,
|
|
135
|
+
sessionRoot,
|
|
136
|
+
entries,
|
|
137
|
+
};
|
|
138
|
+
await fs.writeFile(path.join(directory, "journal.json"), `${JSON.stringify(journal, null, "\t")}\n`, {
|
|
139
|
+
mode: 0o600,
|
|
140
|
+
});
|
|
141
|
+
return { directory, journal };
|
|
149
142
|
}
|
|
150
143
|
|
|
151
144
|
async function restoreTransaction(directory: string, journal: TransactionJournal) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
145
|
+
validateJournal(directory, journal);
|
|
146
|
+
for (const entry of [...journal.entries].sort((left, right) => right.target.length - left.target.length)) {
|
|
147
|
+
await fs.rm(entry.target, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
for (const entry of journal.entries) {
|
|
150
|
+
if (entry.kind === "missing") continue;
|
|
151
|
+
await fs.mkdir(path.dirname(entry.target), { recursive: true });
|
|
152
|
+
const backupPath = path.join(directory, "before", entry.backupName);
|
|
153
|
+
assertWithinRoot(directory, backupPath);
|
|
154
|
+
if (entry.kind === "file") await fs.copyFile(backupPath, entry.target);
|
|
155
|
+
else if (entry.kind === "directory") {
|
|
156
|
+
await fs.cp(backupPath, entry.target, {
|
|
157
|
+
recursive: true,
|
|
158
|
+
dereference: false,
|
|
159
|
+
preserveTimestamps: true,
|
|
160
|
+
});
|
|
161
|
+
} else if (entry.kind === "symlink" && entry.linkTarget !== undefined) {
|
|
162
|
+
await fs.symlink(entry.linkTarget, entry.target);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
175
166
|
}
|
|
176
167
|
|
|
177
168
|
function validateJournal(directory: string, journal: TransactionJournal) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
169
|
+
if (journal.version !== JOURNAL_VERSION || !Array.isArray(journal.entries)) {
|
|
170
|
+
throw new Error(`Unsupported pi-sync transaction journal: ${directory}`);
|
|
171
|
+
}
|
|
172
|
+
const expectedRoot = path.resolve(agentDir());
|
|
173
|
+
if (path.resolve(journal.root) !== expectedRoot) {
|
|
174
|
+
throw new Error(`Transaction root no longer matches the Pi agent directory: ${directory}`);
|
|
175
|
+
}
|
|
176
|
+
for (const entry of journal.entries) {
|
|
177
|
+
if (
|
|
178
|
+
!entry ||
|
|
179
|
+
typeof entry.target !== "string" ||
|
|
180
|
+
typeof entry.backupName !== "string" ||
|
|
181
|
+
!/^\d+$/u.test(entry.backupName)
|
|
182
|
+
) {
|
|
183
|
+
throw new Error(`Invalid pi-sync transaction entry: ${directory}`);
|
|
184
|
+
}
|
|
185
|
+
assertAllowedTarget(journal.root, journal.sessionRoot, entry.target);
|
|
186
|
+
}
|
|
196
187
|
}
|
|
197
188
|
|
|
198
189
|
function assertAllowedTarget(root: string, sessionRoot: string | undefined, target: string) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
190
|
+
const resolved = path.resolve(target);
|
|
191
|
+
if (isPathInside(root, resolved)) {
|
|
192
|
+
assertWithinRoot(root, resolved);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (sessionRoot && isPathInside(sessionRoot, resolved)) {
|
|
196
|
+
assertWithinRoot(sessionRoot, resolved);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Transaction target is outside configured roots: ${target}`);
|
|
209
200
|
}
|
|
210
201
|
|
|
211
202
|
function transactionRoot() {
|
|
212
|
-
|
|
203
|
+
return path.join(stateDir(), "transactions");
|
|
213
204
|
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
export interface SnapshotFile {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
path: string;
|
|
3
|
+
contentBase64: string;
|
|
4
|
+
sha256: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export interface SnapshotSelection {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
version: 1;
|
|
9
|
+
include: string[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface Snapshot {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
version: number;
|
|
14
|
+
id: string;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
machine: string;
|
|
17
|
+
/** Backend-scoped remote identity retained in the snapshot wire format. */
|
|
18
|
+
profile: string;
|
|
19
|
+
syncSessions?: boolean;
|
|
20
|
+
/** Portable, credential-free included-content intent. Absent on legacy snapshots. */
|
|
21
|
+
selection?: SnapshotSelection;
|
|
22
|
+
files: SnapshotFile[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface SnapshotOptions {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
include?: string[];
|
|
28
|
+
sessionDir?: string;
|
|
29
|
+
/** Temporary internal projections while snapshot storage remains wire-compatible. */
|
|
30
|
+
syncFiles?: string[];
|
|
31
|
+
syncSessions?: boolean;
|
|
32
|
+
extraFiles?: string[];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface SnapshotApplyPlan {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
writes: Array<{ target: string; content: Buffer }>;
|
|
37
|
+
deletes: string[];
|
|
38
38
|
}
|