@narumitw/pi-sync 0.31.0 → 0.35.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 +159 -236
- package/package.json +11 -3
- package/src/backend-factory.ts +20 -0
- package/src/command.ts +39 -39
- package/src/config-errors.ts +7 -0
- package/src/config-file.ts +156 -62
- package/src/config.ts +605 -476
- package/src/file-selection.ts +121 -140
- package/src/git-backend.ts +832 -0
- package/src/git-config.ts +136 -0
- package/src/git-runner.ts +276 -0
- package/src/git-storage.ts +288 -0
- package/src/git-ui.ts +307 -0
- package/src/manager-helpers.ts +64 -0
- package/src/manager-ui.ts +537 -542
- package/src/s3-backend.ts +498 -0
- package/src/s3-client.ts +147 -64
- package/src/s3-credentials-ui.ts +92 -0
- package/src/secret-input.ts +199 -0
- package/src/settings-management.ts +191 -277
- package/src/settings-ui.ts +94 -115
- package/src/setup-switch.ts +150 -0
- package/src/snapshot-apply.ts +13 -7
- package/src/snapshot-codec.ts +48 -0
- package/src/snapshot-transaction.ts +3 -1
- package/src/snapshot.ts +63 -32
- package/src/storage-connections-ui.ts +308 -0
- package/src/sync-backend.ts +97 -0
- package/src/sync-format.ts +31 -33
- package/src/sync-operations.ts +912 -0
- package/src/sync-policy.ts +137 -61
- package/src/sync-setups-ui.ts +177 -0
- package/src/sync-state.ts +48 -48
- package/src/sync.ts +180 -739
- package/src/types.ts +177 -53
- package/src/webdav-backend.ts +678 -0
- package/src/webdav-client.ts +390 -0
- package/src/webdav-config.ts +92 -0
- package/src/webdav-ui.ts +421 -0
- package/src/target-switch.ts +0 -119
|
@@ -1,317 +1,231 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
1
|
import {
|
|
5
|
-
|
|
6
|
-
effectiveTargetRemoteIdentity,
|
|
2
|
+
effectiveSyncSetupRemoteIdentity,
|
|
7
3
|
localConfigPath,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
stateDir,
|
|
14
|
-
statePathForPartialConfig,
|
|
15
|
-
writeLocalConfigObject,
|
|
4
|
+
normalizeSyncInclude,
|
|
5
|
+
type SyncSetupStorageReview,
|
|
6
|
+
syncSetupStorageReview,
|
|
7
|
+
updateLocalConfig,
|
|
8
|
+
validateConfigName,
|
|
16
9
|
} from "./config.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
afterLegacySettingsReadHook = hook;
|
|
28
|
-
try {
|
|
29
|
-
return await run();
|
|
30
|
-
} finally {
|
|
31
|
-
afterLegacySettingsReadHook = previous;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const LEGACY_FIELDS = new Set([
|
|
36
|
-
"endpoint",
|
|
37
|
-
"bucket",
|
|
38
|
-
"region",
|
|
39
|
-
"accessKeyId",
|
|
40
|
-
"secretAccessKey",
|
|
41
|
-
"sessionToken",
|
|
42
|
-
"profile",
|
|
43
|
-
"prefix",
|
|
44
|
-
"autoSync",
|
|
45
|
-
"syncFiles",
|
|
46
|
-
"syncSessions",
|
|
47
|
-
"extraFiles",
|
|
48
|
-
]);
|
|
49
|
-
|
|
50
|
-
export interface MigrationResult {
|
|
51
|
-
settings: Record<string, unknown>;
|
|
52
|
-
backupPath: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function migrateLegacySettings(
|
|
56
|
-
targetName: string,
|
|
57
|
-
storageProfileName: string,
|
|
58
|
-
): Promise<MigrationResult> {
|
|
59
|
-
validateName(targetName, "target");
|
|
60
|
-
validateName(storageProfileName, "storage profile");
|
|
61
|
-
return withLock("settings-migration", async () => {
|
|
62
|
-
const document = await readLocalConfigDocument();
|
|
63
|
-
if (!document) throw new Error("No legacy pi-sync settings are available to migrate.");
|
|
64
|
-
const { bytes: originalBytes, parsed: current } = document;
|
|
65
|
-
if (current.version === 2) {
|
|
66
|
-
throw new Error("pi-sync settings already use profiles and targets.");
|
|
67
|
-
}
|
|
68
|
-
await afterLegacySettingsReadHook();
|
|
69
|
-
const next = legacySettingsAsV2(current, targetName, storageProfileName);
|
|
70
|
-
const backupPath = await writeMigrationBackup(originalBytes);
|
|
71
|
-
await adoptLegacyState(current, next, targetName);
|
|
72
|
-
await replaceLocalConfigDocument(document, next);
|
|
73
|
-
return { settings: next, backupPath };
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function legacySettingsAsV2(
|
|
78
|
-
legacy: Record<string, unknown>,
|
|
79
|
-
targetName: string,
|
|
80
|
-
storageProfileName: string,
|
|
10
|
+
import type { PiSyncSettingsV3, StorageConnectionSettings, SyncSetupSettings } from "./types.js";
|
|
11
|
+
|
|
12
|
+
export async function saveNewV3Settings(
|
|
13
|
+
input: {
|
|
14
|
+
setupName: string;
|
|
15
|
+
connectionName: string;
|
|
16
|
+
connection: StorageConnectionSettings;
|
|
17
|
+
setup: SyncSetupSettings;
|
|
18
|
+
},
|
|
19
|
+
signal?: AbortSignal,
|
|
81
20
|
) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
const target = compactObject({
|
|
96
|
-
profile: storageProfileName,
|
|
97
|
-
bucket: legacy.bucket,
|
|
98
|
-
prefix: legacy.prefix,
|
|
99
|
-
namespace: legacy.profile ?? targetName,
|
|
100
|
-
autoSync: legacy.autoSync,
|
|
101
|
-
syncFiles: legacy.syncFiles,
|
|
102
|
-
syncSessions: legacy.syncSessions,
|
|
103
|
-
extraFiles: legacy.extraFiles,
|
|
104
|
-
legacyStateProfile: legacy.profile ?? "default",
|
|
105
|
-
});
|
|
106
|
-
return {
|
|
107
|
-
...unknown,
|
|
108
|
-
version: 2,
|
|
109
|
-
activeTarget: targetName,
|
|
110
|
-
profiles: { [storageProfileName]: profile },
|
|
111
|
-
targets: { [targetName]: target },
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function saveNewV2Settings(input: {
|
|
116
|
-
targetName: string;
|
|
117
|
-
storageProfileName: string;
|
|
118
|
-
profile: StorageProfileSettings;
|
|
119
|
-
target: SyncTargetSettings;
|
|
120
|
-
}) {
|
|
121
|
-
validateName(input.targetName, "target");
|
|
122
|
-
validateName(input.storageProfileName, "storage profile");
|
|
123
|
-
const current = (await readLocalConfigObject()) ?? {};
|
|
124
|
-
if (Object.keys(current).length > 0)
|
|
125
|
-
throw new Error(`Settings already exist: ${localConfigPath()}`);
|
|
126
|
-
const settings = {
|
|
127
|
-
version: 2,
|
|
128
|
-
activeTarget: input.targetName,
|
|
129
|
-
targetSwitchAction: DEFAULT_TARGET_SWITCH_ACTION,
|
|
130
|
-
profiles: { [input.storageProfileName]: { ...input.profile } },
|
|
131
|
-
targets: {
|
|
132
|
-
[input.targetName]: { ...input.target, profile: input.storageProfileName },
|
|
21
|
+
validateConfigName(input.setupName, "sync setup");
|
|
22
|
+
validateConfigName(input.connectionName, "storage connection");
|
|
23
|
+
const settings: PiSyncSettingsV3 = {
|
|
24
|
+
version: 3,
|
|
25
|
+
activeSyncSetup: input.setupName,
|
|
26
|
+
onSwitch: "ask-before-pull",
|
|
27
|
+
storageConnections: { [input.connectionName]: structuredClone(input.connection) },
|
|
28
|
+
syncSetups: {
|
|
29
|
+
[input.setupName]: {
|
|
30
|
+
...structuredClone(input.setup),
|
|
31
|
+
storage: { ...input.setup.storage, connection: input.connectionName },
|
|
32
|
+
},
|
|
133
33
|
},
|
|
134
34
|
};
|
|
135
|
-
await
|
|
35
|
+
await updateLocalConfig((current) => {
|
|
36
|
+
if (Object.keys(current.storageConnections).length || Object.keys(current.syncSetups).length) {
|
|
37
|
+
throw new Error(`Settings already exist: ${localConfigPath()}`);
|
|
38
|
+
}
|
|
39
|
+
return settings;
|
|
40
|
+
}, signal);
|
|
136
41
|
return settings;
|
|
137
42
|
}
|
|
138
43
|
|
|
139
|
-
export async function
|
|
140
|
-
validateName(name, "storage profile");
|
|
141
|
-
await updateV2Settings((settings) => {
|
|
142
|
-
const profiles = requireObject(settings.profiles, "profiles");
|
|
143
|
-
if (Object.hasOwn(profiles, name)) throw new Error(`Storage profile already exists: ${name}`);
|
|
144
|
-
return { ...settings, profiles: { ...profiles, [name]: { ...profile } } };
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export async function updateStorageProfile(
|
|
44
|
+
export async function addStorageConnection(
|
|
149
45
|
name: string,
|
|
150
|
-
|
|
46
|
+
connection: StorageConnectionSettings,
|
|
47
|
+
signal?: AbortSignal,
|
|
151
48
|
) {
|
|
152
|
-
|
|
153
|
-
await
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return { ...settings, profiles: { ...profiles, [name]: update(profile) } };
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export async function addSyncTarget(name: string, target: SyncTargetSettings) {
|
|
161
|
-
validateName(name, "target");
|
|
162
|
-
await updateV2Settings((settings) => {
|
|
163
|
-
const targets = requireObject(settings.targets, "targets");
|
|
164
|
-
const profiles = requireObject(settings.profiles, "profiles");
|
|
165
|
-
if (Object.hasOwn(targets, name)) throw new Error(`Sync target already exists: ${name}`);
|
|
166
|
-
if (!target.profile || !Object.hasOwn(profiles, target.profile)) {
|
|
167
|
-
throw new Error(`Storage profile not found: ${target.profile ?? "missing"}`);
|
|
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}`);
|
|
168
53
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
54
|
+
return {
|
|
55
|
+
...settings,
|
|
56
|
+
storageConnections: {
|
|
57
|
+
...settings.storageConnections,
|
|
58
|
+
[name]: structuredClone(connection),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}, signal);
|
|
172
62
|
}
|
|
173
63
|
|
|
174
|
-
export async function
|
|
64
|
+
export async function updateStorageConnection(
|
|
175
65
|
name: string,
|
|
176
|
-
update: (
|
|
66
|
+
update: (connection: StorageConnectionSettings) => StorageConnectionSettings,
|
|
67
|
+
expectedSetups?: readonly string[],
|
|
68
|
+
signal?: AbortSignal,
|
|
177
69
|
) {
|
|
178
|
-
|
|
179
|
-
await
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function addSyncSetup(name: string, setup: SyncSetupSettings, signal?: AbortSignal) {
|
|
88
|
+
validateConfigName(name, "sync setup");
|
|
89
|
+
await updateSettings((settings) => {
|
|
90
|
+
if (Object.hasOwn(settings.syncSetups, name))
|
|
91
|
+
throw new Error(`Sync setup already exists: ${name}`);
|
|
92
|
+
if (!Object.hasOwn(settings.storageConnections, setup.storage.connection)) {
|
|
93
|
+
throw new Error(`Storage connection not found: ${setup.storage.connection}`);
|
|
195
94
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
95
|
+
const nextSetups = { ...settings.syncSetups, [name]: structuredClone(setup) };
|
|
96
|
+
assertUniqueLocations(nextSetups, settings.storageConnections);
|
|
198
97
|
return {
|
|
199
98
|
...settings,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
settings.activeTarget === name ? Object.keys(nextTargets)[0] : settings.activeTarget,
|
|
99
|
+
syncSetups: nextSetups,
|
|
100
|
+
...(settings.activeSyncSetup ? {} : { activeSyncSetup: name }),
|
|
203
101
|
};
|
|
204
|
-
});
|
|
102
|
+
}, signal);
|
|
205
103
|
}
|
|
206
104
|
|
|
207
|
-
export async function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (referenced) throw new Error(`Storage profile “${name}” is used by target “${referenced}”.`);
|
|
216
|
-
if (!Object.hasOwn(profiles, name)) throw new Error(`Storage profile not found: ${name}`);
|
|
217
|
-
const nextProfiles = { ...profiles };
|
|
218
|
-
delete nextProfiles[name];
|
|
219
|
-
return { ...settings, profiles: nextProfiles };
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function updateV2Settings(
|
|
224
|
-
update: (settings: Record<string, unknown>) => Record<string, unknown>,
|
|
105
|
+
export async function updateSyncSetup(
|
|
106
|
+
name: string,
|
|
107
|
+
update: (setup: SyncSetupSettings) => SyncSetupSettings,
|
|
108
|
+
options: {
|
|
109
|
+
expectedStorage?: SyncSetupStorageReview;
|
|
110
|
+
expectedInclude?: readonly string[];
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
} = {},
|
|
225
113
|
) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
114
|
+
validateConfigName(name, "sync setup");
|
|
115
|
+
await updateSettings((settings) => {
|
|
116
|
+
const setup = settings.syncSetups[name];
|
|
117
|
+
if (!setup) throw new Error(`Sync setup not found: ${name}`);
|
|
118
|
+
if (
|
|
119
|
+
options.expectedInclude &&
|
|
120
|
+
!sameNames(normalizeSyncInclude(setup.sync.include), options.expectedInclude)
|
|
121
|
+
) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Sync setup “${name}” included content changed while it was open; reopen it and review the current selection.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (options.expectedStorage) {
|
|
127
|
+
const connectionName = setup.storage.connection;
|
|
128
|
+
const connection = settings.storageConnections[connectionName];
|
|
129
|
+
if (
|
|
130
|
+
!connection ||
|
|
131
|
+
!sameStorageReview(
|
|
132
|
+
syncSetupStorageReview(name, setup, connectionName, connection),
|
|
133
|
+
options.expectedStorage,
|
|
134
|
+
)
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Sync setup “${name}” storage changed while it was open; reopen it and review the current storage location.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const nextSetup = update(structuredClone(setup));
|
|
142
|
+
if (!Object.hasOwn(settings.storageConnections, nextSetup.storage.connection)) {
|
|
143
|
+
throw new Error(`Storage connection not found: ${nextSetup.storage.connection}`);
|
|
230
144
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
145
|
+
const nextSetups = { ...settings.syncSetups, [name]: nextSetup };
|
|
146
|
+
assertUniqueLocations(nextSetups, settings.storageConnections);
|
|
147
|
+
return { ...settings, syncSetups: nextSetups };
|
|
148
|
+
}, options.signal);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function removeSyncSetup(name: string, signal?: AbortSignal) {
|
|
152
|
+
validateConfigName(name, "sync setup");
|
|
153
|
+
await updateSettings((settings) => {
|
|
154
|
+
if (!Object.hasOwn(settings.syncSetups, name)) throw new Error(`Sync setup not found: ${name}`);
|
|
155
|
+
const isCurrent = settings.activeSyncSetup === name;
|
|
156
|
+
if (isCurrent && Object.keys(settings.syncSetups).length > 1) {
|
|
157
|
+
throw new Error("Switch to another sync setup before removing the current setup.");
|
|
158
|
+
}
|
|
159
|
+
const syncSetups = { ...settings.syncSetups };
|
|
160
|
+
delete syncSetups[name];
|
|
161
|
+
const next = { ...settings, syncSetups };
|
|
162
|
+
if (isCurrent) delete next.activeSyncSetup;
|
|
233
163
|
return next;
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function writeMigrationBackup(bytes: Buffer) {
|
|
238
|
-
const directory = path.join(stateDir(), "backups", "config");
|
|
239
|
-
await fs.mkdir(directory, { recursive: true });
|
|
240
|
-
const backupPath = path.join(
|
|
241
|
-
directory,
|
|
242
|
-
`pi-sync.local.${new Date().toISOString().replace(/[:.]/gu, "-")}.${randomUUID().slice(0, 8)}.json`,
|
|
243
|
-
);
|
|
244
|
-
await fs.writeFile(backupPath, bytes, { flag: "wx", mode: 0o600 });
|
|
245
|
-
if (process.platform !== "win32") await fs.chmod(backupPath, 0o600);
|
|
246
|
-
return backupPath;
|
|
164
|
+
}, signal);
|
|
247
165
|
}
|
|
248
166
|
|
|
249
|
-
async function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
const destination = statePathForPartialConfig(resolveV2PartialConfig(next, targetName));
|
|
264
|
-
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
265
|
-
try {
|
|
266
|
-
await fs.writeFile(destination, bytes, { flag: "wx", mode: 0o600 });
|
|
267
|
-
} catch (error) {
|
|
268
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
269
|
-
}
|
|
167
|
+
export async function removeStorageConnection(name: string, signal?: AbortSignal) {
|
|
168
|
+
validateConfigName(name, "storage connection");
|
|
169
|
+
await updateSettings((settings) => {
|
|
170
|
+
const referenced = referencingSetupNames(settings.syncSetups, name)[0];
|
|
171
|
+
if (referenced) {
|
|
172
|
+
throw new Error(`Storage connection “${name}” is used by sync setup “${referenced}”.`);
|
|
173
|
+
}
|
|
174
|
+
if (!Object.hasOwn(settings.storageConnections, name)) {
|
|
175
|
+
throw new Error(`Storage connection not found: ${name}`);
|
|
176
|
+
}
|
|
177
|
+
const storageConnections = { ...settings.storageConnections };
|
|
178
|
+
delete storageConnections[name];
|
|
179
|
+
return { ...settings, storageConnections };
|
|
180
|
+
}, signal);
|
|
270
181
|
}
|
|
271
182
|
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
target: SyncTargetSettings,
|
|
183
|
+
async function updateSettings(
|
|
184
|
+
update: (settings: PiSyncSettingsV3) => PiSyncSettingsV3,
|
|
185
|
+
signal?: AbortSignal,
|
|
276
186
|
) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
otherName !== name &&
|
|
281
|
-
effectiveTargetRemoteIdentity(requireObject(value, "target"), otherName) === identity
|
|
282
|
-
) {
|
|
283
|
-
throw new Error(`Target “${name}” duplicates the remote destination of “${otherName}”.`);
|
|
187
|
+
return updateLocalConfig((settings) => {
|
|
188
|
+
if (settings.version !== 3) {
|
|
189
|
+
throw new Error("Storage connections and sync setups require version 3 pi-sync settings.");
|
|
284
190
|
}
|
|
285
|
-
|
|
191
|
+
return update(settings);
|
|
192
|
+
}, signal);
|
|
286
193
|
}
|
|
287
194
|
|
|
288
|
-
function
|
|
289
|
-
return Object.
|
|
195
|
+
function referencingSetupNames(setups: Record<string, SyncSetupSettings>, connection: string) {
|
|
196
|
+
return Object.entries(setups)
|
|
197
|
+
.filter(([, setup]) => setup.storage.connection === connection)
|
|
198
|
+
.map(([name]) => name)
|
|
199
|
+
.sort((left, right) => left.localeCompare(right));
|
|
290
200
|
}
|
|
291
201
|
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
202
|
+
function assertUniqueLocations(
|
|
203
|
+
setups: Record<string, SyncSetupSettings>,
|
|
204
|
+
connections: Record<string, StorageConnectionSettings>,
|
|
205
|
+
) {
|
|
206
|
+
const identities = new Map<string, string>();
|
|
207
|
+
for (const [name, setup] of Object.entries(setups)) {
|
|
208
|
+
const connection = connections[setup.storage.connection];
|
|
209
|
+
if (!connection) throw new Error(`Storage connection not found: ${setup.storage.connection}`);
|
|
210
|
+
const identity = effectiveSyncSetupRemoteIdentity(setup, connection);
|
|
211
|
+
const previous = identities.get(identity);
|
|
212
|
+
if (previous) {
|
|
213
|
+
throw new Error(`Sync setup “${name}” duplicates the storage location of “${previous}”.`);
|
|
214
|
+
}
|
|
215
|
+
identities.set(identity, name);
|
|
216
|
+
}
|
|
296
217
|
}
|
|
297
218
|
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
throw new Error(`Invalid pi-sync settings: ${name} must be an object.`);
|
|
301
|
-
}
|
|
302
|
-
return value as Record<string, unknown>;
|
|
219
|
+
function sameNames(left: readonly string[], right: readonly string[]) {
|
|
220
|
+
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
303
221
|
}
|
|
304
222
|
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
/[\u0000-\u001f\u007f-\u009f]/u.test(value)
|
|
314
|
-
) {
|
|
315
|
-
throw new Error(`Invalid ${label} name.`);
|
|
316
|
-
}
|
|
223
|
+
function sameStorageReview(left: SyncSetupStorageReview, right: SyncSetupStorageReview) {
|
|
224
|
+
return (
|
|
225
|
+
left.connectionName === right.connectionName &&
|
|
226
|
+
left.storageKind === right.storageKind &&
|
|
227
|
+
left.storagePath === right.storagePath &&
|
|
228
|
+
left.bucket === right.bucket &&
|
|
229
|
+
left.branch === right.branch
|
|
230
|
+
);
|
|
317
231
|
}
|