@narumitw/pi-sync 0.50.3 → 0.51.1
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
|
@@ -3,262 +3,237 @@ import { normalizeSyncInclude } from "../sync/sync-policy.js";
|
|
|
3
3
|
import { type SyncSetupStorageReview, syncSetupStorageReview } from "./config.js";
|
|
4
4
|
import { localConfigPath } from "./config-file.js";
|
|
5
5
|
import { updateLocalConfig } from "./settings-store.js";
|
|
6
|
-
import type {
|
|
7
|
-
PiSyncSettingsV3,
|
|
8
|
-
StorageConnectionSettings,
|
|
9
|
-
SyncSetupSettings,
|
|
10
|
-
} from "./settings-types.js";
|
|
6
|
+
import type { PiSyncSettingsV3, StorageConnectionSettings, SyncSetupSettings } from "./settings-types.js";
|
|
11
7
|
import { effectiveSyncSetupRemoteIdentity, validateConfigName } from "./settings-validation.js";
|
|
12
8
|
|
|
13
9
|
export class SyncSetupReviewChangedError extends Error {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "SyncSetupReviewChangedError";
|
|
13
|
+
}
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
export async function saveNewV3Settings(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
input: {
|
|
18
|
+
setupName: string;
|
|
19
|
+
connectionName: string;
|
|
20
|
+
connection: StorageConnectionSettings;
|
|
21
|
+
setup: SyncSetupSettings;
|
|
22
|
+
},
|
|
23
|
+
signal?: AbortSignal,
|
|
28
24
|
) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
25
|
+
validateConfigName(input.setupName, "sync setup");
|
|
26
|
+
validateConfigName(input.connectionName, "storage connection");
|
|
27
|
+
const settings: PiSyncSettingsV3 = {
|
|
28
|
+
version: 3,
|
|
29
|
+
activeSyncSetup: input.setupName,
|
|
30
|
+
onSwitch: "ask-before-pull",
|
|
31
|
+
skipSecretScan: false,
|
|
32
|
+
storageConnections: { [input.connectionName]: structuredClone(input.connection) },
|
|
33
|
+
syncSetups: {
|
|
34
|
+
[input.setupName]: {
|
|
35
|
+
...structuredClone(input.setup),
|
|
36
|
+
storage: { ...input.setup.storage, connection: input.connectionName },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return updateLocalConfig((current) => {
|
|
41
|
+
if (Object.keys(current.storageConnections).length || Object.keys(current.syncSetups).length) {
|
|
42
|
+
throw new Error(`Settings already exist: ${localConfigPath()}`);
|
|
43
|
+
}
|
|
44
|
+
return { ...current, ...settings };
|
|
45
|
+
}, signal);
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
export async function addStorageConnection(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
[name]: structuredClone(connection),
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}, signal);
|
|
48
|
+
export async function addStorageConnection(name: string, connection: StorageConnectionSettings, signal?: AbortSignal) {
|
|
49
|
+
validateConfigName(name, "storage connection");
|
|
50
|
+
await updateSettings((settings) => {
|
|
51
|
+
if (Object.hasOwn(settings.storageConnections, name)) {
|
|
52
|
+
throw new Error(`Storage connection already exists: ${name}`);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
...settings,
|
|
56
|
+
storageConnections: {
|
|
57
|
+
...settings.storageConnections,
|
|
58
|
+
[name]: structuredClone(connection),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}, signal);
|
|
70
62
|
}
|
|
71
63
|
|
|
72
64
|
export async function updateStorageConnection(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
name: string,
|
|
66
|
+
update: (connection: StorageConnectionSettings) => StorageConnectionSettings,
|
|
67
|
+
expectedSetups?: readonly string[],
|
|
68
|
+
signal?: AbortSignal,
|
|
77
69
|
) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
70
|
+
validateConfigName(name, "storage connection");
|
|
71
|
+
await updateSettings((settings) => {
|
|
72
|
+
const connection = settings.storageConnections[name];
|
|
73
|
+
if (!connection) throw new Error(`Storage connection not found: ${name}`);
|
|
74
|
+
const currentSetups = referencingSetupNames(settings.syncSetups, name);
|
|
75
|
+
if (expectedSetups && !sameNames(currentSetups, expectedSetups)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Storage connection “${name}” usage changed while it was open; reopen it and review the affected sync setups.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const nextConnection = update(structuredClone(connection));
|
|
81
|
+
const nextConnections = { ...settings.storageConnections, [name]: nextConnection };
|
|
82
|
+
assertUniqueLocations(settings.syncSetups, nextConnections);
|
|
83
|
+
return { ...settings, storageConnections: nextConnections };
|
|
84
|
+
}, signal);
|
|
93
85
|
}
|
|
94
86
|
|
|
95
87
|
export async function addSyncSetup(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
name: string,
|
|
89
|
+
setup: SyncSetupSettings,
|
|
90
|
+
signal?: AbortSignal,
|
|
91
|
+
expectedConnection?: StorageConnectionSettings,
|
|
100
92
|
) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
...(settings.activeSyncSetup ? {} : { activeSyncSetup: name }),
|
|
122
|
-
};
|
|
123
|
-
}, signal);
|
|
93
|
+
validateConfigName(name, "sync setup");
|
|
94
|
+
await updateSettings((settings) => {
|
|
95
|
+
if (Object.hasOwn(settings.syncSetups, name)) throw new Error(`Sync setup already exists: ${name}`);
|
|
96
|
+
if (!Object.hasOwn(settings.storageConnections, setup.storage.connection)) {
|
|
97
|
+
throw new Error(`Storage connection not found: ${setup.storage.connection}`);
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
expectedConnection &&
|
|
101
|
+
!isDeepStrictEqual(settings.storageConnections[setup.storage.connection], expectedConnection)
|
|
102
|
+
) {
|
|
103
|
+
throw new SyncSetupReviewChangedError("Storage connection changed while the setup review was open; reopen it.");
|
|
104
|
+
}
|
|
105
|
+
const nextSetups = { ...settings.syncSetups, [name]: structuredClone(setup) };
|
|
106
|
+
assertUniqueLocations(nextSetups, settings.storageConnections);
|
|
107
|
+
return {
|
|
108
|
+
...settings,
|
|
109
|
+
syncSetups: nextSetups,
|
|
110
|
+
...(settings.activeSyncSetup ? {} : { activeSyncSetup: name }),
|
|
111
|
+
};
|
|
112
|
+
}, signal);
|
|
124
113
|
}
|
|
125
114
|
|
|
126
115
|
export async function updateSyncSetup(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
116
|
+
name: string,
|
|
117
|
+
update: (setup: SyncSetupSettings) => SyncSetupSettings,
|
|
118
|
+
options: {
|
|
119
|
+
expectedStorage?: SyncSetupStorageReview;
|
|
120
|
+
expectedInclude?: readonly string[];
|
|
121
|
+
expectedConnection?: StorageConnectionSettings;
|
|
122
|
+
signal?: AbortSignal;
|
|
123
|
+
} = {},
|
|
135
124
|
) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"Storage connection changed while the setup review was open; reopen it.",
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
const nextSetup = update(structuredClone(setup));
|
|
175
|
-
if (!Object.hasOwn(settings.storageConnections, nextSetup.storage.connection)) {
|
|
176
|
-
throw new Error(`Storage connection not found: ${nextSetup.storage.connection}`);
|
|
177
|
-
}
|
|
178
|
-
const nextSetups = { ...settings.syncSetups, [name]: nextSetup };
|
|
179
|
-
assertUniqueLocations(nextSetups, settings.storageConnections);
|
|
180
|
-
return { ...settings, syncSetups: nextSetups };
|
|
181
|
-
}, options.signal);
|
|
125
|
+
validateConfigName(name, "sync setup");
|
|
126
|
+
await updateSettings((settings) => {
|
|
127
|
+
const setup = settings.syncSetups[name];
|
|
128
|
+
if (!setup) throw new Error(`Sync setup not found: ${name}`);
|
|
129
|
+
if (options.expectedInclude && !sameNames(normalizeSyncInclude(setup.sync.include), options.expectedInclude)) {
|
|
130
|
+
throw new SyncSetupReviewChangedError(
|
|
131
|
+
`Sync setup “${name}” included content changed while it was open; reopen it and review the current selection.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (options.expectedStorage) {
|
|
135
|
+
const connectionName = setup.storage.connection;
|
|
136
|
+
const connection = settings.storageConnections[connectionName];
|
|
137
|
+
if (
|
|
138
|
+
!connection ||
|
|
139
|
+
!sameStorageReview(syncSetupStorageReview(name, setup, connectionName, connection), options.expectedStorage)
|
|
140
|
+
) {
|
|
141
|
+
throw new SyncSetupReviewChangedError(
|
|
142
|
+
`Sync setup “${name}” storage changed while it was open; reopen it and review the current storage location.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
options.expectedConnection &&
|
|
148
|
+
!isDeepStrictEqual(settings.storageConnections[setup.storage.connection], options.expectedConnection)
|
|
149
|
+
) {
|
|
150
|
+
throw new SyncSetupReviewChangedError("Storage connection changed while the setup review was open; reopen it.");
|
|
151
|
+
}
|
|
152
|
+
const nextSetup = update(structuredClone(setup));
|
|
153
|
+
if (!Object.hasOwn(settings.storageConnections, nextSetup.storage.connection)) {
|
|
154
|
+
throw new Error(`Storage connection not found: ${nextSetup.storage.connection}`);
|
|
155
|
+
}
|
|
156
|
+
const nextSetups = { ...settings.syncSetups, [name]: nextSetup };
|
|
157
|
+
assertUniqueLocations(nextSetups, settings.storageConnections);
|
|
158
|
+
return { ...settings, syncSetups: nextSetups };
|
|
159
|
+
}, options.signal);
|
|
182
160
|
}
|
|
183
161
|
|
|
184
162
|
export async function removeSyncSetup(name: string, signal?: AbortSignal) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
163
|
+
validateConfigName(name, "sync setup");
|
|
164
|
+
await updateSettings((settings) => {
|
|
165
|
+
if (!Object.hasOwn(settings.syncSetups, name)) throw new Error(`Sync setup not found: ${name}`);
|
|
166
|
+
const isCurrent = settings.activeSyncSetup === name;
|
|
167
|
+
if (isCurrent && Object.keys(settings.syncSetups).length > 1) {
|
|
168
|
+
throw new Error("Switch to another sync setup before removing the current setup.");
|
|
169
|
+
}
|
|
170
|
+
const syncSetups = { ...settings.syncSetups };
|
|
171
|
+
delete syncSetups[name];
|
|
172
|
+
const next = { ...settings, syncSetups };
|
|
173
|
+
if (isCurrent) delete next.activeSyncSetup;
|
|
174
|
+
return next;
|
|
175
|
+
}, signal);
|
|
198
176
|
}
|
|
199
177
|
|
|
200
178
|
export async function removeStorageConnection(name: string, signal?: AbortSignal) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
179
|
+
validateConfigName(name, "storage connection");
|
|
180
|
+
await updateSettings((settings) => {
|
|
181
|
+
const referenced = referencingSetupNames(settings.syncSetups, name)[0];
|
|
182
|
+
if (referenced) {
|
|
183
|
+
throw new Error(`Storage connection “${name}” is used by sync setup “${referenced}”.`);
|
|
184
|
+
}
|
|
185
|
+
if (!Object.hasOwn(settings.storageConnections, name)) {
|
|
186
|
+
throw new Error(`Storage connection not found: ${name}`);
|
|
187
|
+
}
|
|
188
|
+
const storageConnections = { ...settings.storageConnections };
|
|
189
|
+
delete storageConnections[name];
|
|
190
|
+
return { ...settings, storageConnections };
|
|
191
|
+
}, signal);
|
|
214
192
|
}
|
|
215
193
|
|
|
216
|
-
async function updateSettings(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
return update(settings);
|
|
225
|
-
}, signal);
|
|
194
|
+
async function updateSettings(update: (settings: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
|
|
195
|
+
return updateLocalConfig((settings) => {
|
|
196
|
+
if (settings.version !== 3) {
|
|
197
|
+
throw new Error("Storage connections and sync setups require version 3 pi-sync settings.");
|
|
198
|
+
}
|
|
199
|
+
return update(settings);
|
|
200
|
+
}, signal);
|
|
226
201
|
}
|
|
227
202
|
|
|
228
203
|
function referencingSetupNames(setups: Record<string, SyncSetupSettings>, connection: string) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
204
|
+
return Object.entries(setups)
|
|
205
|
+
.filter(([, setup]) => setup.storage.connection === connection)
|
|
206
|
+
.map(([name]) => name)
|
|
207
|
+
.sort((left, right) => left.localeCompare(right));
|
|
233
208
|
}
|
|
234
209
|
|
|
235
210
|
function assertUniqueLocations(
|
|
236
|
-
|
|
237
|
-
|
|
211
|
+
setups: Record<string, SyncSetupSettings>,
|
|
212
|
+
connections: Record<string, StorageConnectionSettings>,
|
|
238
213
|
) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
214
|
+
const identities = new Map<string, string>();
|
|
215
|
+
for (const [name, setup] of Object.entries(setups)) {
|
|
216
|
+
const connection = connections[setup.storage.connection];
|
|
217
|
+
if (!connection) throw new Error(`Storage connection not found: ${setup.storage.connection}`);
|
|
218
|
+
const identity = effectiveSyncSetupRemoteIdentity(setup, connection);
|
|
219
|
+
const previous = identities.get(identity);
|
|
220
|
+
if (previous) {
|
|
221
|
+
throw new Error(`Sync setup “${name}” duplicates the storage location of “${previous}”.`);
|
|
222
|
+
}
|
|
223
|
+
identities.set(identity, name);
|
|
224
|
+
}
|
|
250
225
|
}
|
|
251
226
|
|
|
252
227
|
function sameNames(left: readonly string[], right: readonly string[]) {
|
|
253
|
-
|
|
228
|
+
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
254
229
|
}
|
|
255
230
|
|
|
256
231
|
function sameStorageReview(left: SyncSetupStorageReview, right: SyncSetupStorageReview) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
232
|
+
return (
|
|
233
|
+
left.connectionName === right.connectionName &&
|
|
234
|
+
left.storageKind === right.storageKind &&
|
|
235
|
+
left.storagePath === right.storagePath &&
|
|
236
|
+
left.bucket === right.bucket &&
|
|
237
|
+
left.branch === right.branch
|
|
238
|
+
);
|
|
264
239
|
}
|
|
@@ -1,109 +1,95 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
localConfigPath,
|
|
6
|
-
readMigratingLocalConfigDocument,
|
|
7
|
-
updateLocalConfigDocument,
|
|
8
|
-
} from "./config-file.js";
|
|
4
|
+
import { localConfigPath, readMigratingLocalConfigDocument, updateLocalConfigDocument } from "./config-file.js";
|
|
9
5
|
import type { OnSwitchAction, PiSyncSettingsV3 } from "./settings-types.js";
|
|
10
6
|
import { DEFAULT_ON_SWITCH, validateSettingsDocument } from "./settings-validation.js";
|
|
11
7
|
|
|
12
8
|
let configUpdateQueue: Promise<void> = Promise.resolve();
|
|
13
9
|
|
|
14
10
|
export async function requireSettings() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
return settings;
|
|
11
|
+
const settings = await readLocalConfigObject();
|
|
12
|
+
if (!settings) {
|
|
13
|
+
throw new Error(`Missing pi-sync settings. Open /sync and choose Set up sync, or create ${localConfigPath()}.`);
|
|
14
|
+
}
|
|
15
|
+
return settings;
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
export async function configuredSyncSetupNames() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right))
|
|
28
|
-
: [];
|
|
19
|
+
const settings = await readLocalConfigObject();
|
|
20
|
+
return settings ? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right)) : [];
|
|
29
21
|
}
|
|
30
22
|
|
|
31
23
|
export async function loadOnSwitch(): Promise<OnSwitchAction> {
|
|
32
|
-
|
|
24
|
+
return (await requireSettings()).onSwitch;
|
|
33
25
|
}
|
|
34
26
|
|
|
35
27
|
export function localConfigTemplate(): PiSyncSettingsV3 {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
return {
|
|
29
|
+
version: 3,
|
|
30
|
+
onSwitch: DEFAULT_ON_SWITCH,
|
|
31
|
+
skipSecretScan: false,
|
|
32
|
+
showStatus: true,
|
|
33
|
+
storageConnections: {},
|
|
34
|
+
syncSetups: {},
|
|
35
|
+
};
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
export async function readLocalConfigDocument() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
const document = await readMigratingLocalConfigDocument((settings) => {
|
|
40
|
+
validateSettingsDocument(settings);
|
|
41
|
+
});
|
|
42
|
+
if (document) validateSettingsDocument(document.parsed);
|
|
43
|
+
return document;
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
export async function readLocalConfigObject(): Promise<PiSyncSettingsV3 | undefined> {
|
|
54
|
-
|
|
47
|
+
return (await readLocalConfigDocument())?.parsed as PiSyncSettingsV3 | undefined;
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
export function updateLocalConfig(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
() => undefined,
|
|
68
|
-
);
|
|
69
|
-
return operation;
|
|
50
|
+
export function updateLocalConfig(update: (current: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
|
|
51
|
+
const operation = configUpdateQueue.then(() => {
|
|
52
|
+
signal?.throwIfAborted();
|
|
53
|
+
return performLocalConfigUpdate(update, signal);
|
|
54
|
+
});
|
|
55
|
+
configUpdateQueue = operation.then(
|
|
56
|
+
() => undefined,
|
|
57
|
+
() => undefined,
|
|
58
|
+
);
|
|
59
|
+
return operation;
|
|
70
60
|
}
|
|
71
61
|
|
|
72
|
-
async function performLocalConfigUpdate(
|
|
73
|
-
|
|
74
|
-
signal?: AbortSignal,
|
|
75
|
-
) {
|
|
76
|
-
return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
|
|
62
|
+
async function performLocalConfigUpdate(update: (current: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
|
|
63
|
+
return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
|
|
77
64
|
}
|
|
78
65
|
|
|
79
66
|
export async function writeLocalConfigObject(value: PiSyncSettingsV3 | Record<string, unknown>) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
67
|
+
validateSettingsDocument(value as Record<string, unknown>);
|
|
68
|
+
const configPath = localConfigPath();
|
|
69
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.lstat(configPath);
|
|
72
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing to overwrite symlinked pi-sync settings: ${configPath}`);
|
|
73
|
+
if (!stat.isFile()) throw new Error(`pi-sync settings are not a regular file: ${configPath}`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
76
|
+
}
|
|
77
|
+
const temporaryPath = path.join(
|
|
78
|
+
path.dirname(configPath),
|
|
79
|
+
`.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`,
|
|
80
|
+
);
|
|
81
|
+
let handle: fs.FileHandle | undefined;
|
|
82
|
+
try {
|
|
83
|
+
handle = await fs.open(temporaryPath, "wx", 0o600);
|
|
84
|
+
await handle.writeFile(`${JSON.stringify(value, null, "\t")}\n`, "utf8");
|
|
85
|
+
if (process.platform !== "win32") await handle.chmod(0o600);
|
|
86
|
+
await handle.sync();
|
|
87
|
+
await handle.close();
|
|
88
|
+
handle = undefined;
|
|
89
|
+
await fs.rename(temporaryPath, configPath);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
await handle?.close().catch(() => undefined);
|
|
92
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
109
95
|
}
|