zen-fs-config 0.5.39 → 0.5.41
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.
|
@@ -122,6 +122,9 @@ async function wrapZenFSFileSystem(config) {
|
|
|
122
122
|
if (typeof isolatedFS.writeFileWithMtime === "function") {
|
|
123
123
|
backend.writeFileWithMtime = (path, data, mtimeMs) => isolatedFS.writeFileWithMtime(path, data, mtimeMs);
|
|
124
124
|
}
|
|
125
|
+
if (typeof isolatedFS.shouldSync === "function") {
|
|
126
|
+
backend.shouldSync = (...args) => isolatedFS.shouldSync(...args);
|
|
127
|
+
}
|
|
125
128
|
return backend;
|
|
126
129
|
}
|
|
127
130
|
var inMemoryCounter = 0;
|
|
@@ -185,7 +188,8 @@ var OPTIONAL_BACKEND_GLOBALS = {
|
|
|
185
188
|
// third-party Gitee widget) would shadow the real backend and break mounting
|
|
186
189
|
// with "Invalid mount configuration".
|
|
187
190
|
"zen-fs-gitee": ["ZenFSGitee"],
|
|
188
|
-
"zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
|
|
191
|
+
"zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"],
|
|
192
|
+
"zen-fs-webdav": ["ZenFSWebDAV"]
|
|
189
193
|
};
|
|
190
194
|
async function importOptionalBackend(pkg) {
|
|
191
195
|
const candidates = OPTIONAL_BACKEND_GLOBALS[pkg];
|
|
@@ -273,9 +277,9 @@ registerBackend("GitHub", async (options) => {
|
|
|
273
277
|
registerBackend("Gitee", async (options) => {
|
|
274
278
|
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
275
279
|
const Gitee = _gt?.Gitee ?? _gt?.default;
|
|
276
|
-
if (typeof Gitee !== "function") {
|
|
280
|
+
if (!Gitee || typeof Gitee !== "function" && typeof Gitee.create !== "function") {
|
|
277
281
|
throw new Error(
|
|
278
|
-
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a
|
|
282
|
+
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a usable backend (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
|
|
279
283
|
);
|
|
280
284
|
}
|
|
281
285
|
return wrapZenFSFileSystem({
|
|
@@ -302,6 +306,39 @@ registerBackend("Gitee", async (options) => {
|
|
|
302
306
|
// identity = the Git repo location
|
|
303
307
|
identityFields: ["owner", "repo", "branch", "baseUrl"]
|
|
304
308
|
});
|
|
309
|
+
async function resolveRemoteStorageHref(href) {
|
|
310
|
+
const raw = String(href ?? "").trim();
|
|
311
|
+
if (!raw) throw new Error("RemoteStorage href is required.");
|
|
312
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw.replace(/\/+$/, "");
|
|
313
|
+
const match = raw.match(/^(?:acct:)?([^@/]+)@([^@/]+)$/);
|
|
314
|
+
if (!match) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`RemoteStorage href "${raw}" is neither an absolute URL nor a user address (user@domain).`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const user = match[1];
|
|
320
|
+
const domain = match[2];
|
|
321
|
+
const webfingerUrl = `https://${domain}/.well-known/webfinger?resource=acct:${encodeURIComponent(user)}@${encodeURIComponent(domain)}`;
|
|
322
|
+
let response;
|
|
323
|
+
try {
|
|
324
|
+
response = await globalThis.fetch(webfingerUrl, {
|
|
325
|
+
headers: { Accept: "application/jrd+json, application/json" }
|
|
326
|
+
});
|
|
327
|
+
} catch (err) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`RemoteStorage WebFinger request failed (${webfingerUrl}): ${err instanceof Error ? err.message : String(err)}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
throw new Error(`RemoteStorage WebFinger lookup failed (HTTP ${response.status}): ${webfingerUrl}`);
|
|
334
|
+
}
|
|
335
|
+
const jrd = await response.json();
|
|
336
|
+
const link = (jrd?.links ?? []).find(
|
|
337
|
+
(item) => item && item.rel === "http://tools.ietf.org/id/draft-dejong-remotestorage" && item.href
|
|
338
|
+
);
|
|
339
|
+
if (!link) throw new Error(`WebFinger response has no RemoteStorage link: ${webfingerUrl}`);
|
|
340
|
+
return String(link.href).replace(/\/+$/, "");
|
|
341
|
+
}
|
|
305
342
|
registerBackend("RemoteStorage", async (options) => {
|
|
306
343
|
const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
|
|
307
344
|
const RemoteStorageFileSystem = _rs?.RemoteStorageFileSystem ?? _rs?.default ?? (typeof _rs === "function" ? _rs : void 0);
|
|
@@ -311,7 +348,7 @@ registerBackend("RemoteStorage", async (options) => {
|
|
|
311
348
|
);
|
|
312
349
|
}
|
|
313
350
|
return new RemoteStorageFileSystem({
|
|
314
|
-
href: options.href,
|
|
351
|
+
href: await resolveRemoteStorageHref(options.href),
|
|
315
352
|
token: options.token,
|
|
316
353
|
basePath: options.basePath || void 0,
|
|
317
354
|
preciseMtime: true,
|
|
@@ -322,7 +359,7 @@ registerBackend("RemoteStorage", async (options) => {
|
|
|
322
359
|
label: "RemoteStorage",
|
|
323
360
|
icon: "\u{1F4E1}",
|
|
324
361
|
fields: [
|
|
325
|
-
{ key: "href", label: "User Address (href)", type: "text", placeholder: "user@5apps.com", required: true },
|
|
362
|
+
{ key: "href", label: "User Address / Storage URL (href)", type: "text", placeholder: "user@5apps.com \u6216 https://storage.5apps.com/user", required: true },
|
|
326
363
|
{ key: "token", label: "Bearer Token", type: "password", placeholder: "rs-xxxxxxxx", required: true },
|
|
327
364
|
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
|
|
328
365
|
],
|
|
@@ -333,6 +370,53 @@ registerBackend("RemoteStorage", async (options) => {
|
|
|
333
370
|
// connections to the same endpoint with different tuning are deduplicated.
|
|
334
371
|
identityFields: ["href", "token", "basePath"]
|
|
335
372
|
});
|
|
373
|
+
registerBackend("WebDAV", async (options) => {
|
|
374
|
+
const _dav = await importOptionalBackend("zen-fs-webdav");
|
|
375
|
+
const SyncableWebDAVFS = _dav?.SyncableWebDAVFS ?? _dav?.default?.SyncableWebDAVFS ?? globalThis.ZenFSWebDAV?.SyncableWebDAVFS;
|
|
376
|
+
if (typeof SyncableWebDAVFS !== "function") {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`Backend package "zen-fs-webdav" loaded, but its SyncableWebDAVFS export is not a constructor (typeof=${typeof SyncableWebDAVFS}). Run: npm install zen-fs-webdav, or load its UMD bundle via <script src=".../zen-fs-webdav.global.js"> so it exposes the global "ZenFSWebDAV".`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const instance = new SyncableWebDAVFS({
|
|
382
|
+
baseUrl: options.baseUrl,
|
|
383
|
+
username: options.username,
|
|
384
|
+
password: options.password,
|
|
385
|
+
token: options.token,
|
|
386
|
+
headers: options.headers,
|
|
387
|
+
timeout: options.timeout,
|
|
388
|
+
preciseMtime: true
|
|
389
|
+
});
|
|
390
|
+
if (options.purgeLegacyMtime === true) {
|
|
391
|
+
try {
|
|
392
|
+
await instance.purgeLegacyMtimeSidecars({ root: options.basePath ?? "/" });
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return instance;
|
|
397
|
+
}, {
|
|
398
|
+
type: "WebDAV",
|
|
399
|
+
label: "WebDAV",
|
|
400
|
+
icon: "\u2601\uFE0F",
|
|
401
|
+
fields: [
|
|
402
|
+
{ key: "baseUrl", label: "Base URL", type: "text", placeholder: "https://dav.example.com/remote.php/dav/files/user/", required: true },
|
|
403
|
+
{ key: "username", label: "Username", type: "text", placeholder: "admin" },
|
|
404
|
+
{ key: "password", label: "Password", type: "password" },
|
|
405
|
+
{ key: "token", label: "Token", type: "password", placeholder: "Bearer token (optional)" },
|
|
406
|
+
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" },
|
|
407
|
+
{ key: "purgeLegacyMtime", label: "Purge legacy .mtime sidecars", type: "select", options: [
|
|
408
|
+
{ value: "false", label: "no (default)" },
|
|
409
|
+
{ value: "true", label: "yes (one-time migration)" }
|
|
410
|
+
] }
|
|
411
|
+
],
|
|
412
|
+
defaultOptions: { baseUrl: "", username: "", password: "", token: "", basePath: "/", purgeLegacyMtime: "false" },
|
|
413
|
+
// accountFields: credentials live on the endpoint, so a data-sync group can
|
|
414
|
+
// reuse them from an already-connected config-sync WebDAV backend.
|
|
415
|
+
accountFields: ["username", "password", "token"],
|
|
416
|
+
// identity = the WebDAV endpoint. Credentials are auth, not location, so two
|
|
417
|
+
// connections to the same URL are deduplicated as one replica.
|
|
418
|
+
identityFields: ["baseUrl"]
|
|
419
|
+
});
|
|
336
420
|
|
|
337
421
|
export {
|
|
338
422
|
registerBackend,
|
package/dist/index.js
CHANGED
|
@@ -165,6 +165,9 @@ async function wrapZenFSFileSystem(config) {
|
|
|
165
165
|
if (typeof isolatedFS.writeFileWithMtime === "function") {
|
|
166
166
|
backend.writeFileWithMtime = (path, data, mtimeMs) => isolatedFS.writeFileWithMtime(path, data, mtimeMs);
|
|
167
167
|
}
|
|
168
|
+
if (typeof isolatedFS.shouldSync === "function") {
|
|
169
|
+
backend.shouldSync = (...args) => isolatedFS.shouldSync(...args);
|
|
170
|
+
}
|
|
168
171
|
return backend;
|
|
169
172
|
}
|
|
170
173
|
function _cacheGet(key) {
|
|
@@ -212,6 +215,39 @@ async function importOptionalBackend(pkg) {
|
|
|
212
215
|
);
|
|
213
216
|
}
|
|
214
217
|
}
|
|
218
|
+
async function resolveRemoteStorageHref(href) {
|
|
219
|
+
const raw = String(href ?? "").trim();
|
|
220
|
+
if (!raw) throw new Error("RemoteStorage href is required.");
|
|
221
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw.replace(/\/+$/, "");
|
|
222
|
+
const match = raw.match(/^(?:acct:)?([^@/]+)@([^@/]+)$/);
|
|
223
|
+
if (!match) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`RemoteStorage href "${raw}" is neither an absolute URL nor a user address (user@domain).`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const user = match[1];
|
|
229
|
+
const domain = match[2];
|
|
230
|
+
const webfingerUrl = `https://${domain}/.well-known/webfinger?resource=acct:${encodeURIComponent(user)}@${encodeURIComponent(domain)}`;
|
|
231
|
+
let response;
|
|
232
|
+
try {
|
|
233
|
+
response = await globalThis.fetch(webfingerUrl, {
|
|
234
|
+
headers: { Accept: "application/jrd+json, application/json" }
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`RemoteStorage WebFinger request failed (${webfingerUrl}): ${err instanceof Error ? err.message : String(err)}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
throw new Error(`RemoteStorage WebFinger lookup failed (HTTP ${response.status}): ${webfingerUrl}`);
|
|
243
|
+
}
|
|
244
|
+
const jrd = await response.json();
|
|
245
|
+
const link = (jrd?.links ?? []).find(
|
|
246
|
+
(item) => item && item.rel === "http://tools.ietf.org/id/draft-dejong-remotestorage" && item.href
|
|
247
|
+
);
|
|
248
|
+
if (!link) throw new Error(`WebFinger response has no RemoteStorage link: ${webfingerUrl}`);
|
|
249
|
+
return String(link.href).replace(/\/+$/, "");
|
|
250
|
+
}
|
|
215
251
|
var registry, metadataRegistry, inMemoryCounter, _memCache, OPTIONAL_BACKEND_GLOBALS;
|
|
216
252
|
var init_backend_registry = __esm({
|
|
217
253
|
"src/backend-registry.ts"() {
|
|
@@ -260,7 +296,8 @@ var init_backend_registry = __esm({
|
|
|
260
296
|
// third-party Gitee widget) would shadow the real backend and break mounting
|
|
261
297
|
// with "Invalid mount configuration".
|
|
262
298
|
"zen-fs-gitee": ["ZenFSGitee"],
|
|
263
|
-
"zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
|
|
299
|
+
"zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"],
|
|
300
|
+
"zen-fs-webdav": ["ZenFSWebDAV"]
|
|
264
301
|
};
|
|
265
302
|
registerBackend("GitHub", async (options) => {
|
|
266
303
|
const _gh = await importOptionalBackend("zen-fs-github");
|
|
@@ -322,9 +359,9 @@ var init_backend_registry = __esm({
|
|
|
322
359
|
registerBackend("Gitee", async (options) => {
|
|
323
360
|
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
324
361
|
const Gitee = _gt?.Gitee ?? _gt?.default;
|
|
325
|
-
if (typeof Gitee !== "function") {
|
|
362
|
+
if (!Gitee || typeof Gitee !== "function" && typeof Gitee.create !== "function") {
|
|
326
363
|
throw new Error(
|
|
327
|
-
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a
|
|
364
|
+
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a usable backend (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
|
|
328
365
|
);
|
|
329
366
|
}
|
|
330
367
|
return wrapZenFSFileSystem({
|
|
@@ -360,7 +397,7 @@ var init_backend_registry = __esm({
|
|
|
360
397
|
);
|
|
361
398
|
}
|
|
362
399
|
return new RemoteStorageFileSystem({
|
|
363
|
-
href: options.href,
|
|
400
|
+
href: await resolveRemoteStorageHref(options.href),
|
|
364
401
|
token: options.token,
|
|
365
402
|
basePath: options.basePath || void 0,
|
|
366
403
|
preciseMtime: true,
|
|
@@ -371,7 +408,7 @@ var init_backend_registry = __esm({
|
|
|
371
408
|
label: "RemoteStorage",
|
|
372
409
|
icon: "\u{1F4E1}",
|
|
373
410
|
fields: [
|
|
374
|
-
{ key: "href", label: "User Address (href)", type: "text", placeholder: "user@5apps.com", required: true },
|
|
411
|
+
{ key: "href", label: "User Address / Storage URL (href)", type: "text", placeholder: "user@5apps.com \u6216 https://storage.5apps.com/user", required: true },
|
|
375
412
|
{ key: "token", label: "Bearer Token", type: "password", placeholder: "rs-xxxxxxxx", required: true },
|
|
376
413
|
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
|
|
377
414
|
],
|
|
@@ -382,6 +419,53 @@ var init_backend_registry = __esm({
|
|
|
382
419
|
// connections to the same endpoint with different tuning are deduplicated.
|
|
383
420
|
identityFields: ["href", "token", "basePath"]
|
|
384
421
|
});
|
|
422
|
+
registerBackend("WebDAV", async (options) => {
|
|
423
|
+
const _dav = await importOptionalBackend("zen-fs-webdav");
|
|
424
|
+
const SyncableWebDAVFS = _dav?.SyncableWebDAVFS ?? _dav?.default?.SyncableWebDAVFS ?? globalThis.ZenFSWebDAV?.SyncableWebDAVFS;
|
|
425
|
+
if (typeof SyncableWebDAVFS !== "function") {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Backend package "zen-fs-webdav" loaded, but its SyncableWebDAVFS export is not a constructor (typeof=${typeof SyncableWebDAVFS}). Run: npm install zen-fs-webdav, or load its UMD bundle via <script src=".../zen-fs-webdav.global.js"> so it exposes the global "ZenFSWebDAV".`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const instance = new SyncableWebDAVFS({
|
|
431
|
+
baseUrl: options.baseUrl,
|
|
432
|
+
username: options.username,
|
|
433
|
+
password: options.password,
|
|
434
|
+
token: options.token,
|
|
435
|
+
headers: options.headers,
|
|
436
|
+
timeout: options.timeout,
|
|
437
|
+
preciseMtime: true
|
|
438
|
+
});
|
|
439
|
+
if (options.purgeLegacyMtime === true) {
|
|
440
|
+
try {
|
|
441
|
+
await instance.purgeLegacyMtimeSidecars({ root: options.basePath ?? "/" });
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return instance;
|
|
446
|
+
}, {
|
|
447
|
+
type: "WebDAV",
|
|
448
|
+
label: "WebDAV",
|
|
449
|
+
icon: "\u2601\uFE0F",
|
|
450
|
+
fields: [
|
|
451
|
+
{ key: "baseUrl", label: "Base URL", type: "text", placeholder: "https://dav.example.com/remote.php/dav/files/user/", required: true },
|
|
452
|
+
{ key: "username", label: "Username", type: "text", placeholder: "admin" },
|
|
453
|
+
{ key: "password", label: "Password", type: "password" },
|
|
454
|
+
{ key: "token", label: "Token", type: "password", placeholder: "Bearer token (optional)" },
|
|
455
|
+
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" },
|
|
456
|
+
{ key: "purgeLegacyMtime", label: "Purge legacy .mtime sidecars", type: "select", options: [
|
|
457
|
+
{ value: "false", label: "no (default)" },
|
|
458
|
+
{ value: "true", label: "yes (one-time migration)" }
|
|
459
|
+
] }
|
|
460
|
+
],
|
|
461
|
+
defaultOptions: { baseUrl: "", username: "", password: "", token: "", basePath: "/", purgeLegacyMtime: "false" },
|
|
462
|
+
// accountFields: credentials live on the endpoint, so a data-sync group can
|
|
463
|
+
// reuse them from an already-connected config-sync WebDAV backend.
|
|
464
|
+
accountFields: ["username", "password", "token"],
|
|
465
|
+
// identity = the WebDAV endpoint. Credentials are auth, not location, so two
|
|
466
|
+
// connections to the same URL are deduplicated as one replica.
|
|
467
|
+
identityFields: ["baseUrl"]
|
|
468
|
+
});
|
|
385
469
|
}
|
|
386
470
|
});
|
|
387
471
|
|
|
@@ -1367,6 +1451,12 @@ function identityOptions(type, options) {
|
|
|
1367
1451
|
for (const f of fields) {
|
|
1368
1452
|
if (options && options[f] !== void 0) subset[f] = options[f];
|
|
1369
1453
|
}
|
|
1454
|
+
if (type === "Gitee" || type === "GitHub") {
|
|
1455
|
+
const defaultEndpoint = type === "Gitee" ? "https://gitee.com/api/v5" : "https://api.github.com";
|
|
1456
|
+
const raw = options && options.baseUrl;
|
|
1457
|
+
const value = typeof raw === "string" && raw.trim() !== "" ? raw : defaultEndpoint;
|
|
1458
|
+
subset["baseUrl"] = value.replace(/\/+$/, "");
|
|
1459
|
+
}
|
|
1370
1460
|
return subset;
|
|
1371
1461
|
}
|
|
1372
1462
|
function backendDedupKey(desc) {
|
|
@@ -2757,10 +2847,15 @@ var ConfigRepo = class {
|
|
|
2757
2847
|
}
|
|
2758
2848
|
const descPath = this.appDataGroupFilePath(id);
|
|
2759
2849
|
try {
|
|
2760
|
-
await this.
|
|
2761
|
-
} catch {
|
|
2850
|
+
await this.deleteFile(descPath);
|
|
2851
|
+
} catch (err) {
|
|
2852
|
+
log2.log(`[ConfigRepo] removeAppDataGroup: deleteFile failed for "${id}":`, err);
|
|
2853
|
+
try {
|
|
2854
|
+
await this.cachedFS.unlink(descPath);
|
|
2855
|
+
} catch {
|
|
2856
|
+
}
|
|
2857
|
+
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
2762
2858
|
}
|
|
2763
|
-
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
2764
2859
|
log2.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
|
|
2765
2860
|
}
|
|
2766
2861
|
async listAccountBackends() {
|
|
@@ -3143,12 +3238,25 @@ async function connect(appId, options = {}) {
|
|
|
3143
3238
|
onConflict: options.onConflict,
|
|
3144
3239
|
syncPollIntervalMs: options.syncPollIntervalMs
|
|
3145
3240
|
});
|
|
3146
|
-
const
|
|
3241
|
+
const appDataGroups2 = [];
|
|
3242
|
+
const descriptors2 = await repo2.listAppDataGroups();
|
|
3243
|
+
for (const d of descriptors2) {
|
|
3244
|
+
try {
|
|
3245
|
+
appDataGroups2.push(await repo2.getAppDataGroup(d.id));
|
|
3246
|
+
} catch (err) {
|
|
3247
|
+
log3(`connect: failed to load app data group "${d.id}":`, err);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
let dataGroup2 = appDataGroups2[0];
|
|
3251
|
+
if (!dataGroup2) {
|
|
3252
|
+
dataGroup2 = await repo2.createAppDataGroup("default", []);
|
|
3253
|
+
appDataGroups2.push(dataGroup2);
|
|
3254
|
+
}
|
|
3147
3255
|
return {
|
|
3148
3256
|
groupType: options.groupType ?? "config-sync",
|
|
3149
3257
|
repo: repo2,
|
|
3150
3258
|
dataGroup: dataGroup2,
|
|
3151
|
-
appDataGroups:
|
|
3259
|
+
appDataGroups: appDataGroups2
|
|
3152
3260
|
};
|
|
3153
3261
|
}
|
|
3154
3262
|
const { type, options: backendOptions } = options.backendInfo;
|
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
registerBackend,
|
|
10
10
|
unregisterBackend,
|
|
11
11
|
wrapZenFSFileSystem
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-S2S3XBK2.mjs";
|
|
13
13
|
|
|
14
14
|
// src/config-repo.ts
|
|
15
15
|
import {
|
|
@@ -351,7 +351,7 @@ function registerFolderBackend() {
|
|
|
351
351
|
const pathMod = await import("path");
|
|
352
352
|
const dir = options.path;
|
|
353
353
|
const store = new FolderStore({ path: dir, label: options.label, fs, pathMod });
|
|
354
|
-
return (await import("./backend-registry-
|
|
354
|
+
return (await import("./backend-registry-RBKWNJZI.mjs")).wrapZenFSFileSystem({ backend: FolderBackend, path: dir, label: options.label });
|
|
355
355
|
},
|
|
356
356
|
{
|
|
357
357
|
type: "Folder",
|
|
@@ -911,6 +911,12 @@ function identityOptions(type, options) {
|
|
|
911
911
|
for (const f of fields) {
|
|
912
912
|
if (options && options[f] !== void 0) subset[f] = options[f];
|
|
913
913
|
}
|
|
914
|
+
if (type === "Gitee" || type === "GitHub") {
|
|
915
|
+
const defaultEndpoint = type === "Gitee" ? "https://gitee.com/api/v5" : "https://api.github.com";
|
|
916
|
+
const raw = options && options.baseUrl;
|
|
917
|
+
const value = typeof raw === "string" && raw.trim() !== "" ? raw : defaultEndpoint;
|
|
918
|
+
subset["baseUrl"] = value.replace(/\/+$/, "");
|
|
919
|
+
}
|
|
914
920
|
return subset;
|
|
915
921
|
}
|
|
916
922
|
function backendDedupKey(desc) {
|
|
@@ -2301,10 +2307,15 @@ var ConfigRepo = class {
|
|
|
2301
2307
|
}
|
|
2302
2308
|
const descPath = this.appDataGroupFilePath(id);
|
|
2303
2309
|
try {
|
|
2304
|
-
await this.
|
|
2305
|
-
} catch {
|
|
2310
|
+
await this.deleteFile(descPath);
|
|
2311
|
+
} catch (err) {
|
|
2312
|
+
log2.log(`[ConfigRepo] removeAppDataGroup: deleteFile failed for "${id}":`, err);
|
|
2313
|
+
try {
|
|
2314
|
+
await this.cachedFS.unlink(descPath);
|
|
2315
|
+
} catch {
|
|
2316
|
+
}
|
|
2317
|
+
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
2306
2318
|
}
|
|
2307
|
-
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
2308
2319
|
log2.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
|
|
2309
2320
|
}
|
|
2310
2321
|
async listAccountBackends() {
|
|
@@ -2686,12 +2697,25 @@ async function connect(appId, options = {}) {
|
|
|
2686
2697
|
onConflict: options.onConflict,
|
|
2687
2698
|
syncPollIntervalMs: options.syncPollIntervalMs
|
|
2688
2699
|
});
|
|
2689
|
-
const
|
|
2700
|
+
const appDataGroups2 = [];
|
|
2701
|
+
const descriptors2 = await repo2.listAppDataGroups();
|
|
2702
|
+
for (const d of descriptors2) {
|
|
2703
|
+
try {
|
|
2704
|
+
appDataGroups2.push(await repo2.getAppDataGroup(d.id));
|
|
2705
|
+
} catch (err) {
|
|
2706
|
+
log3(`connect: failed to load app data group "${d.id}":`, err);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
let dataGroup2 = appDataGroups2[0];
|
|
2710
|
+
if (!dataGroup2) {
|
|
2711
|
+
dataGroup2 = await repo2.createAppDataGroup("default", []);
|
|
2712
|
+
appDataGroups2.push(dataGroup2);
|
|
2713
|
+
}
|
|
2690
2714
|
return {
|
|
2691
2715
|
groupType: options.groupType ?? "config-sync",
|
|
2692
2716
|
repo: repo2,
|
|
2693
2717
|
dataGroup: dataGroup2,
|
|
2694
|
-
appDataGroups:
|
|
2718
|
+
appDataGroups: appDataGroups2
|
|
2695
2719
|
};
|
|
2696
2720
|
}
|
|
2697
2721
|
const { type, options: backendOptions } = options.backendInfo;
|
package/dist/zen-fs-config.js
CHANGED
|
@@ -24478,6 +24478,9 @@ var ZenFSConfig = (() => {
|
|
|
24478
24478
|
if (typeof isolatedFS.writeFileWithMtime === "function") {
|
|
24479
24479
|
backend.writeFileWithMtime = (path, data, mtimeMs) => isolatedFS.writeFileWithMtime(path, data, mtimeMs);
|
|
24480
24480
|
}
|
|
24481
|
+
if (typeof isolatedFS.shouldSync === "function") {
|
|
24482
|
+
backend.shouldSync = (...args) => isolatedFS.shouldSync(...args);
|
|
24483
|
+
}
|
|
24481
24484
|
return backend;
|
|
24482
24485
|
}
|
|
24483
24486
|
function _cacheGet(key) {
|
|
@@ -24525,6 +24528,39 @@ var ZenFSConfig = (() => {
|
|
|
24525
24528
|
);
|
|
24526
24529
|
}
|
|
24527
24530
|
}
|
|
24531
|
+
async function resolveRemoteStorageHref(href) {
|
|
24532
|
+
const raw = String(href ?? "").trim();
|
|
24533
|
+
if (!raw) throw new Error("RemoteStorage href is required.");
|
|
24534
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw.replace(/\/+$/, "");
|
|
24535
|
+
const match = raw.match(/^(?:acct:)?([^@/]+)@([^@/]+)$/);
|
|
24536
|
+
if (!match) {
|
|
24537
|
+
throw new Error(
|
|
24538
|
+
`RemoteStorage href "${raw}" is neither an absolute URL nor a user address (user@domain).`
|
|
24539
|
+
);
|
|
24540
|
+
}
|
|
24541
|
+
const user = match[1];
|
|
24542
|
+
const domain = match[2];
|
|
24543
|
+
const webfingerUrl = `https://${domain}/.well-known/webfinger?resource=acct:${encodeURIComponent(user)}@${encodeURIComponent(domain)}`;
|
|
24544
|
+
let response;
|
|
24545
|
+
try {
|
|
24546
|
+
response = await globalThis.fetch(webfingerUrl, {
|
|
24547
|
+
headers: { Accept: "application/jrd+json, application/json" }
|
|
24548
|
+
});
|
|
24549
|
+
} catch (err2) {
|
|
24550
|
+
throw new Error(
|
|
24551
|
+
`RemoteStorage WebFinger request failed (${webfingerUrl}): ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
24552
|
+
);
|
|
24553
|
+
}
|
|
24554
|
+
if (!response.ok) {
|
|
24555
|
+
throw new Error(`RemoteStorage WebFinger lookup failed (HTTP ${response.status}): ${webfingerUrl}`);
|
|
24556
|
+
}
|
|
24557
|
+
const jrd = await response.json();
|
|
24558
|
+
const link5 = (jrd?.links ?? []).find(
|
|
24559
|
+
(item) => item && item.rel === "http://tools.ietf.org/id/draft-dejong-remotestorage" && item.href
|
|
24560
|
+
);
|
|
24561
|
+
if (!link5) throw new Error(`WebFinger response has no RemoteStorage link: ${webfingerUrl}`);
|
|
24562
|
+
return String(link5.href).replace(/\/+$/, "");
|
|
24563
|
+
}
|
|
24528
24564
|
var registry, metadataRegistry, inMemoryCounter, _memCache, OPTIONAL_BACKEND_GLOBALS;
|
|
24529
24565
|
var init_backend_registry = __esm({
|
|
24530
24566
|
"src/backend-registry.ts"() {
|
|
@@ -24573,7 +24609,8 @@ var ZenFSConfig = (() => {
|
|
|
24573
24609
|
// third-party Gitee widget) would shadow the real backend and break mounting
|
|
24574
24610
|
// with "Invalid mount configuration".
|
|
24575
24611
|
"zen-fs-gitee": ["ZenFSGitee"],
|
|
24576
|
-
"zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
|
|
24612
|
+
"zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"],
|
|
24613
|
+
"zen-fs-webdav": ["ZenFSWebDAV"]
|
|
24577
24614
|
};
|
|
24578
24615
|
registerBackend("GitHub", async (options) => {
|
|
24579
24616
|
const _gh = await importOptionalBackend("zen-fs-github");
|
|
@@ -24635,9 +24672,9 @@ var ZenFSConfig = (() => {
|
|
|
24635
24672
|
registerBackend("Gitee", async (options) => {
|
|
24636
24673
|
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
24637
24674
|
const Gitee = _gt?.Gitee ?? _gt?.default;
|
|
24638
|
-
if (typeof Gitee !== "function") {
|
|
24675
|
+
if (!Gitee || typeof Gitee !== "function" && typeof Gitee.create !== "function") {
|
|
24639
24676
|
throw new Error(
|
|
24640
|
-
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a
|
|
24677
|
+
`Backend package "zen-fs-gitee" loaded, but its Gitee export is not a usable backend (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
|
|
24641
24678
|
);
|
|
24642
24679
|
}
|
|
24643
24680
|
return wrapZenFSFileSystem({
|
|
@@ -24673,7 +24710,7 @@ var ZenFSConfig = (() => {
|
|
|
24673
24710
|
);
|
|
24674
24711
|
}
|
|
24675
24712
|
return new RemoteStorageFileSystem({
|
|
24676
|
-
href: options.href,
|
|
24713
|
+
href: await resolveRemoteStorageHref(options.href),
|
|
24677
24714
|
token: options.token,
|
|
24678
24715
|
basePath: options.basePath || void 0,
|
|
24679
24716
|
preciseMtime: true,
|
|
@@ -24684,7 +24721,7 @@ var ZenFSConfig = (() => {
|
|
|
24684
24721
|
label: "RemoteStorage",
|
|
24685
24722
|
icon: "\u{1F4E1}",
|
|
24686
24723
|
fields: [
|
|
24687
|
-
{ key: "href", label: "User Address (href)", type: "text", placeholder: "user@5apps.com", required: true },
|
|
24724
|
+
{ key: "href", label: "User Address / Storage URL (href)", type: "text", placeholder: "user@5apps.com \u6216 https://storage.5apps.com/user", required: true },
|
|
24688
24725
|
{ key: "token", label: "Bearer Token", type: "password", placeholder: "rs-xxxxxxxx", required: true },
|
|
24689
24726
|
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
|
|
24690
24727
|
],
|
|
@@ -24695,6 +24732,53 @@ var ZenFSConfig = (() => {
|
|
|
24695
24732
|
// connections to the same endpoint with different tuning are deduplicated.
|
|
24696
24733
|
identityFields: ["href", "token", "basePath"]
|
|
24697
24734
|
});
|
|
24735
|
+
registerBackend("WebDAV", async (options) => {
|
|
24736
|
+
const _dav = await importOptionalBackend("zen-fs-webdav");
|
|
24737
|
+
const SyncableWebDAVFS = _dav?.SyncableWebDAVFS ?? _dav?.default?.SyncableWebDAVFS ?? globalThis.ZenFSWebDAV?.SyncableWebDAVFS;
|
|
24738
|
+
if (typeof SyncableWebDAVFS !== "function") {
|
|
24739
|
+
throw new Error(
|
|
24740
|
+
`Backend package "zen-fs-webdav" loaded, but its SyncableWebDAVFS export is not a constructor (typeof=${typeof SyncableWebDAVFS}). Run: npm install zen-fs-webdav, or load its UMD bundle via <script src=".../zen-fs-webdav.global.js"> so it exposes the global "ZenFSWebDAV".`
|
|
24741
|
+
);
|
|
24742
|
+
}
|
|
24743
|
+
const instance = new SyncableWebDAVFS({
|
|
24744
|
+
baseUrl: options.baseUrl,
|
|
24745
|
+
username: options.username,
|
|
24746
|
+
password: options.password,
|
|
24747
|
+
token: options.token,
|
|
24748
|
+
headers: options.headers,
|
|
24749
|
+
timeout: options.timeout,
|
|
24750
|
+
preciseMtime: true
|
|
24751
|
+
});
|
|
24752
|
+
if (options.purgeLegacyMtime === true) {
|
|
24753
|
+
try {
|
|
24754
|
+
await instance.purgeLegacyMtimeSidecars({ root: options.basePath ?? "/" });
|
|
24755
|
+
} catch {
|
|
24756
|
+
}
|
|
24757
|
+
}
|
|
24758
|
+
return instance;
|
|
24759
|
+
}, {
|
|
24760
|
+
type: "WebDAV",
|
|
24761
|
+
label: "WebDAV",
|
|
24762
|
+
icon: "\u2601\uFE0F",
|
|
24763
|
+
fields: [
|
|
24764
|
+
{ key: "baseUrl", label: "Base URL", type: "text", placeholder: "https://dav.example.com/remote.php/dav/files/user/", required: true },
|
|
24765
|
+
{ key: "username", label: "Username", type: "text", placeholder: "admin" },
|
|
24766
|
+
{ key: "password", label: "Password", type: "password" },
|
|
24767
|
+
{ key: "token", label: "Token", type: "password", placeholder: "Bearer token (optional)" },
|
|
24768
|
+
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" },
|
|
24769
|
+
{ key: "purgeLegacyMtime", label: "Purge legacy .mtime sidecars", type: "select", options: [
|
|
24770
|
+
{ value: "false", label: "no (default)" },
|
|
24771
|
+
{ value: "true", label: "yes (one-time migration)" }
|
|
24772
|
+
] }
|
|
24773
|
+
],
|
|
24774
|
+
defaultOptions: { baseUrl: "", username: "", password: "", token: "", basePath: "/", purgeLegacyMtime: "false" },
|
|
24775
|
+
// accountFields: credentials live on the endpoint, so a data-sync group can
|
|
24776
|
+
// reuse them from an already-connected config-sync WebDAV backend.
|
|
24777
|
+
accountFields: ["username", "password", "token"],
|
|
24778
|
+
// identity = the WebDAV endpoint. Credentials are auth, not location, so two
|
|
24779
|
+
// connections to the same URL are deduplicated as one replica.
|
|
24780
|
+
identityFields: ["baseUrl"]
|
|
24781
|
+
});
|
|
24698
24782
|
}
|
|
24699
24783
|
});
|
|
24700
24784
|
|
|
@@ -27300,6 +27384,12 @@ var ZenFSConfig = (() => {
|
|
|
27300
27384
|
for (const f of fields) {
|
|
27301
27385
|
if (options && options[f] !== void 0) subset[f] = options[f];
|
|
27302
27386
|
}
|
|
27387
|
+
if (type === "Gitee" || type === "GitHub") {
|
|
27388
|
+
const defaultEndpoint = type === "Gitee" ? "https://gitee.com/api/v5" : "https://api.github.com";
|
|
27389
|
+
const raw = options && options.baseUrl;
|
|
27390
|
+
const value = typeof raw === "string" && raw.trim() !== "" ? raw : defaultEndpoint;
|
|
27391
|
+
subset["baseUrl"] = value.replace(/\/+$/, "");
|
|
27392
|
+
}
|
|
27303
27393
|
return subset;
|
|
27304
27394
|
}
|
|
27305
27395
|
function backendDedupKey(desc) {
|
|
@@ -28690,10 +28780,15 @@ var ZenFSConfig = (() => {
|
|
|
28690
28780
|
}
|
|
28691
28781
|
const descPath = this.appDataGroupFilePath(id);
|
|
28692
28782
|
try {
|
|
28693
|
-
await this.
|
|
28694
|
-
} catch {
|
|
28783
|
+
await this.deleteFile(descPath);
|
|
28784
|
+
} catch (err2) {
|
|
28785
|
+
log6.log(`[ConfigRepo] removeAppDataGroup: deleteFile failed for "${id}":`, err2);
|
|
28786
|
+
try {
|
|
28787
|
+
await this.cachedFS.unlink(descPath);
|
|
28788
|
+
} catch {
|
|
28789
|
+
}
|
|
28790
|
+
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
28695
28791
|
}
|
|
28696
|
-
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
28697
28792
|
log6.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
|
|
28698
28793
|
}
|
|
28699
28794
|
async listAccountBackends() {
|
|
@@ -29076,12 +29171,25 @@ var ZenFSConfig = (() => {
|
|
|
29076
29171
|
onConflict: options.onConflict,
|
|
29077
29172
|
syncPollIntervalMs: options.syncPollIntervalMs
|
|
29078
29173
|
});
|
|
29079
|
-
const
|
|
29174
|
+
const appDataGroups2 = [];
|
|
29175
|
+
const descriptors2 = await repo2.listAppDataGroups();
|
|
29176
|
+
for (const d of descriptors2) {
|
|
29177
|
+
try {
|
|
29178
|
+
appDataGroups2.push(await repo2.getAppDataGroup(d.id));
|
|
29179
|
+
} catch (err2) {
|
|
29180
|
+
log7(`connect: failed to load app data group "${d.id}":`, err2);
|
|
29181
|
+
}
|
|
29182
|
+
}
|
|
29183
|
+
let dataGroup2 = appDataGroups2[0];
|
|
29184
|
+
if (!dataGroup2) {
|
|
29185
|
+
dataGroup2 = await repo2.createAppDataGroup("default", []);
|
|
29186
|
+
appDataGroups2.push(dataGroup2);
|
|
29187
|
+
}
|
|
29080
29188
|
return {
|
|
29081
29189
|
groupType: options.groupType ?? "config-sync",
|
|
29082
29190
|
repo: repo2,
|
|
29083
29191
|
dataGroup: dataGroup2,
|
|
29084
|
-
appDataGroups:
|
|
29192
|
+
appDataGroups: appDataGroups2
|
|
29085
29193
|
};
|
|
29086
29194
|
}
|
|
29087
29195
|
const { type, options: backendOptions } = options.backendInfo;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-fs-config",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.41",
|
|
4
4
|
"description": "Distributed config management library built on ZenFS, zen-fs-cache, and zen-fs-sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"zen-fs-sync": ">=0.4.7",
|
|
53
53
|
"zen-fs-github": "*",
|
|
54
54
|
"zen-fs-gitee": "*",
|
|
55
|
-
"zen-fs-remotestoragejs": "*"
|
|
55
|
+
"zen-fs-remotestoragejs": "*",
|
|
56
|
+
"zen-fs-webdav": "*"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"@richard432/localstorage-logger": "^0.1.0"
|
|
@@ -72,6 +73,9 @@
|
|
|
72
73
|
},
|
|
73
74
|
"zen-fs-remotestoragejs": {
|
|
74
75
|
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"zen-fs-webdav": {
|
|
78
|
+
"optional": true
|
|
75
79
|
}
|
|
76
80
|
},
|
|
77
81
|
"devDependencies": {
|