zen-fs-config 0.3.11 → 0.3.13
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/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +74 -251
- package/dist/index.mjs +74 -251
- package/package.json +5 -1
package/dist/index.d.mts
CHANGED
|
@@ -242,6 +242,8 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
242
242
|
private disposed;
|
|
243
243
|
private configCache;
|
|
244
244
|
constructor(appId: string, nodeId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
|
|
245
|
+
/** Full path to this node's directory on the primary backend. */
|
|
246
|
+
get nodePath(): string;
|
|
245
247
|
load(rawConfig?: string): Promise<void>;
|
|
246
248
|
getConfig<T = unknown>(path: string): T;
|
|
247
249
|
setConfig(path: string, data: unknown): void;
|
|
@@ -256,7 +258,7 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
256
258
|
resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
|
|
257
259
|
listConflicts(): Promise<ConflictArchive[]>;
|
|
258
260
|
dispose(): Promise<void>;
|
|
259
|
-
setupSync(
|
|
261
|
+
setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
|
|
260
262
|
private persistConfig;
|
|
261
263
|
private reloadConfigCache;
|
|
262
264
|
private handleConflict;
|
package/dist/index.d.ts
CHANGED
|
@@ -242,6 +242,8 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
242
242
|
private disposed;
|
|
243
243
|
private configCache;
|
|
244
244
|
constructor(appId: string, nodeId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
|
|
245
|
+
/** Full path to this node's directory on the primary backend. */
|
|
246
|
+
get nodePath(): string;
|
|
245
247
|
load(rawConfig?: string): Promise<void>;
|
|
246
248
|
getConfig<T = unknown>(path: string): T;
|
|
247
249
|
setConfig(path: string, data: unknown): void;
|
|
@@ -256,7 +258,7 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
256
258
|
resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
|
|
257
259
|
listConflicts(): Promise<ConflictArchive[]>;
|
|
258
260
|
dispose(): Promise<void>;
|
|
259
|
-
setupSync(
|
|
261
|
+
setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
|
|
260
262
|
private persistConfig;
|
|
261
263
|
private reloadConfigCache;
|
|
262
264
|
private handleConflict;
|
package/dist/index.js
CHANGED
|
@@ -353,6 +353,7 @@ async function wrapZenFSFileSystem(config) {
|
|
|
353
353
|
await isolatedFS.createFile(path, { uid: 0, gid: 0, mode: 420 });
|
|
354
354
|
}
|
|
355
355
|
await isolatedFS.write(path, bytes, 0);
|
|
356
|
+
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: Date.now() });
|
|
356
357
|
},
|
|
357
358
|
async readdir(path) {
|
|
358
359
|
return isolatedFS.readdir(path);
|
|
@@ -409,227 +410,59 @@ registerBackend("WebStorage", async (options) => {
|
|
|
409
410
|
return wrapZenFSFileSystem({ backend: WebStorage, storage });
|
|
410
411
|
});
|
|
411
412
|
registerBackend("GitHub", async (options) => {
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"User-Agent": "zen-fs-config"
|
|
421
|
-
};
|
|
422
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
423
|
-
const apiUrl = (path) => {
|
|
424
|
-
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
425
|
-
return `${baseUrl}/repos/${owner}/${repo}/contents/${p}?ref=${branch}`;
|
|
426
|
-
};
|
|
427
|
-
const treeUrl = () => `${baseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
428
|
-
const ghStat = (item) => ({
|
|
429
|
-
isFile: () => item.type === "file",
|
|
430
|
-
isDirectory: () => item.type === "dir",
|
|
431
|
-
size: item.size ?? 0
|
|
413
|
+
const { Github } = await import("zen-fs-github");
|
|
414
|
+
return wrapZenFSFileSystem({
|
|
415
|
+
backend: Github,
|
|
416
|
+
token: options.token,
|
|
417
|
+
owner: options.owner,
|
|
418
|
+
repo: options.repo,
|
|
419
|
+
branch: options.branch,
|
|
420
|
+
baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
|
|
432
421
|
});
|
|
433
|
-
const fetchJson = async (url) => {
|
|
434
|
-
const res = await fetch(url, { headers });
|
|
435
|
-
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`);
|
|
436
|
-
return res.json();
|
|
437
|
-
};
|
|
438
|
-
const backend = {
|
|
439
|
-
async readFile(path, ...args) {
|
|
440
|
-
const data = await fetchJson(apiUrl(path));
|
|
441
|
-
if (data.encoding === "base64") {
|
|
442
|
-
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
443
|
-
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
444
|
-
return raw;
|
|
445
|
-
}
|
|
446
|
-
return data;
|
|
447
|
-
},
|
|
448
|
-
async writeFile(path, data, options2) {
|
|
449
|
-
const message = options2?.message || `Update ${path}`;
|
|
450
|
-
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
451
|
-
const sha = await (async () => {
|
|
452
|
-
try {
|
|
453
|
-
const d = await fetchJson(apiUrl(path));
|
|
454
|
-
return d.sha;
|
|
455
|
-
} catch {
|
|
456
|
-
return void 0;
|
|
457
|
-
}
|
|
458
|
-
})();
|
|
459
|
-
await fetch(apiUrl(path), {
|
|
460
|
-
method: "PUT",
|
|
461
|
-
headers,
|
|
462
|
-
body: JSON.stringify({ message, content, sha, branch })
|
|
463
|
-
});
|
|
464
|
-
},
|
|
465
|
-
async readdir(path) {
|
|
466
|
-
const data = await fetchJson(apiUrl(path));
|
|
467
|
-
return data.map((item) => item.name);
|
|
468
|
-
},
|
|
469
|
-
async stat(path, ...args) {
|
|
470
|
-
try {
|
|
471
|
-
const data = await fetchJson(apiUrl(path));
|
|
472
|
-
if (Array.isArray(data)) {
|
|
473
|
-
return { isFile: () => false, isDirectory: () => true, size: 0 };
|
|
474
|
-
}
|
|
475
|
-
return ghStat(data);
|
|
476
|
-
} catch {
|
|
477
|
-
throw new Error(`ENOENT: ${path}`);
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
async exists(path) {
|
|
481
|
-
try {
|
|
482
|
-
await fetchJson(apiUrl(path));
|
|
483
|
-
return true;
|
|
484
|
-
} catch {
|
|
485
|
-
return false;
|
|
486
|
-
}
|
|
487
|
-
},
|
|
488
|
-
async mkdir(path, options2) {
|
|
489
|
-
const dirPath = path.replace(/\/$/, "");
|
|
490
|
-
const keepPath = `${dirPath}/.gitkeep`;
|
|
491
|
-
const message = options2?.message || `Create directory ${dirPath}`;
|
|
492
|
-
const content = btoa("");
|
|
493
|
-
await fetch(apiUrl(keepPath), {
|
|
494
|
-
method: "PUT",
|
|
495
|
-
headers,
|
|
496
|
-
body: JSON.stringify({ message, content, branch })
|
|
497
|
-
});
|
|
498
|
-
},
|
|
499
|
-
async unlink(path) {
|
|
500
|
-
const data = await fetchJson(apiUrl(path));
|
|
501
|
-
await fetch(apiUrl(path), {
|
|
502
|
-
method: "DELETE",
|
|
503
|
-
headers,
|
|
504
|
-
body: JSON.stringify({ message: `Delete ${path}`, sha: data.sha, branch })
|
|
505
|
-
});
|
|
506
|
-
},
|
|
507
|
-
async rmdir(path) {
|
|
508
|
-
const items = await fetchJson(apiUrl(path));
|
|
509
|
-
if (Array.isArray(items)) {
|
|
510
|
-
for (const item of items) {
|
|
511
|
-
const itemPath = `${path}/${item.name}`;
|
|
512
|
-
if (item.type === "dir") {
|
|
513
|
-
await backend.rmdir(itemPath);
|
|
514
|
-
} else {
|
|
515
|
-
await backend.unlink(itemPath);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
},
|
|
520
|
-
async rename(oldPath, newPath) {
|
|
521
|
-
const content = await backend.readFile(oldPath);
|
|
522
|
-
await backend.writeFile(newPath, content);
|
|
523
|
-
await backend.unlink(oldPath);
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
return backend;
|
|
527
422
|
});
|
|
528
423
|
registerBackend("Gitee", async (options) => {
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return res.json();
|
|
539
|
-
};
|
|
540
|
-
const apiUrl = (path) => {
|
|
541
|
-
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
542
|
-
const params = new URLSearchParams({ access_token: token, ref: branch, path: p });
|
|
543
|
-
return `${baseUrl}/repos/${owner}/${repo}/contents?${params}`;
|
|
544
|
-
};
|
|
545
|
-
const ghStat = (item) => ({
|
|
546
|
-
isFile: () => item.type === "file",
|
|
547
|
-
isDirectory: () => item.type === "dir",
|
|
548
|
-
size: item.size ?? 0
|
|
549
|
-
});
|
|
550
|
-
const backend = {
|
|
551
|
-
async readFile(path, ...args) {
|
|
552
|
-
const data = await fetchJson(apiUrl(path));
|
|
553
|
-
if (data.content) {
|
|
554
|
-
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
555
|
-
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
556
|
-
return raw;
|
|
557
|
-
}
|
|
558
|
-
return data;
|
|
559
|
-
},
|
|
560
|
-
async writeFile(path, data, options2) {
|
|
561
|
-
const message = options2?.message || `Update ${path}`;
|
|
562
|
-
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
563
|
-
const sha = await (async () => {
|
|
564
|
-
try {
|
|
565
|
-
const d = await fetchJson(apiUrl(path));
|
|
566
|
-
return d.sha;
|
|
567
|
-
} catch {
|
|
568
|
-
return void 0;
|
|
569
|
-
}
|
|
570
|
-
})();
|
|
571
|
-
await fetch(apiUrl(path), {
|
|
572
|
-
method: "POST",
|
|
573
|
-
headers: { "Content-Type": "application/json" },
|
|
574
|
-
body: JSON.stringify({ access_token: token, message, content, sha, branch })
|
|
575
|
-
});
|
|
576
|
-
},
|
|
577
|
-
async readdir(path) {
|
|
578
|
-
const data = await fetchJson(apiUrl(path));
|
|
579
|
-
return Array.isArray(data) ? data.map((i) => i.name) : [];
|
|
580
|
-
},
|
|
581
|
-
async stat(path) {
|
|
582
|
-
try {
|
|
583
|
-
const data = await fetchJson(apiUrl(path));
|
|
584
|
-
if (Array.isArray(data)) return ghStat({ type: "dir", size: 0 });
|
|
585
|
-
return ghStat(data);
|
|
586
|
-
} catch {
|
|
587
|
-
throw new Error(`ENOENT: ${path}`);
|
|
588
|
-
}
|
|
589
|
-
},
|
|
590
|
-
async exists(path) {
|
|
591
|
-
try {
|
|
592
|
-
await fetchJson(apiUrl(path));
|
|
593
|
-
return true;
|
|
594
|
-
} catch {
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
},
|
|
598
|
-
async mkdir(path, options2) {
|
|
599
|
-
const dirPath = path.replace(/\/$/, "");
|
|
600
|
-
const keepPath = `${dirPath}/.gitkeep`;
|
|
601
|
-
const message = options2?.message || `Create directory ${dirPath}`;
|
|
602
|
-
await fetch(apiUrl(keepPath), {
|
|
603
|
-
method: "POST",
|
|
604
|
-
headers: { "Content-Type": "application/json" },
|
|
605
|
-
body: JSON.stringify({ access_token: token, message, content: btoa(""), branch })
|
|
606
|
-
});
|
|
607
|
-
},
|
|
608
|
-
async unlink(path) {
|
|
609
|
-
const data = await fetchJson(apiUrl(path));
|
|
610
|
-
await fetch(apiUrl(path), {
|
|
611
|
-
method: "DELETE",
|
|
612
|
-
headers: { "Content-Type": "application/json" },
|
|
613
|
-
body: JSON.stringify({ access_token: token, message: `Delete ${path}`, sha: data.sha, branch })
|
|
614
|
-
});
|
|
615
|
-
},
|
|
616
|
-
async rmdir(path) {
|
|
617
|
-
const items = await fetchJson(apiUrl(path));
|
|
618
|
-
if (Array.isArray(items)) {
|
|
619
|
-
for (const item of items) {
|
|
620
|
-
const itemPath = `${path}/${item.name}`;
|
|
621
|
-
if (item.type === "dir") await backend.rmdir(itemPath);
|
|
622
|
-
else await backend.unlink(itemPath);
|
|
623
|
-
}
|
|
424
|
+
const zenGitee = await import("zen-fs-gitee");
|
|
425
|
+
const { GiteeAPI } = await import("zen-fs-gitee/dist/gitee-api.js");
|
|
426
|
+
const originalGetTree = GiteeAPI.prototype.getTree;
|
|
427
|
+
GiteeAPI.prototype.getTree = async function(recursive = true) {
|
|
428
|
+
try {
|
|
429
|
+
return await originalGetTree.call(this, recursive);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
if (!err.message?.includes("404") && !err.message?.includes("Tree not found")) {
|
|
432
|
+
throw err;
|
|
624
433
|
}
|
|
625
|
-
},
|
|
626
|
-
async rename(oldPath, newPath) {
|
|
627
|
-
const content = await backend.readFile(oldPath);
|
|
628
|
-
await backend.writeFile(newPath, content);
|
|
629
|
-
await backend.unlink(oldPath);
|
|
630
434
|
}
|
|
435
|
+
console.log(`[Gitee] getTree failed with branch="${this.branch}", resolving to SHA...`);
|
|
436
|
+
const baseUrl = this.baseUrl || "https://gitee.com/api/v5";
|
|
437
|
+
const sep = "?";
|
|
438
|
+
const auth = `access_token=${this.token}`;
|
|
439
|
+
const branchUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/branches/${this.branch}${sep}${auth}`;
|
|
440
|
+
const branchRes = await fetch(branchUrl);
|
|
441
|
+
if (!branchRes.ok) throw new Error(`Gitee: branch "${this.branch}" not found (${branchRes.status})`);
|
|
442
|
+
const branchData = await branchRes.json();
|
|
443
|
+
const commitSha = branchData.commit?.sha;
|
|
444
|
+
if (!commitSha) throw new Error(`Gitee: could not get commit SHA for branch "${this.branch}"`);
|
|
445
|
+
const commitUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/git/commits/${commitSha}${sep}${auth}`;
|
|
446
|
+
const commitRes = await fetch(commitUrl);
|
|
447
|
+
if (!commitRes.ok) throw new Error(`Gitee: commit ${commitSha} not found (${commitRes.status})`);
|
|
448
|
+
const commitData = await commitRes.json();
|
|
449
|
+
const treeSha = commitData.tree?.sha;
|
|
450
|
+
if (!treeSha) throw new Error(`Gitee: could not get tree SHA from commit ${commitSha}`);
|
|
451
|
+
console.log(`[Gitee] Resolved branch="${this.branch}" \u2192 commit=${commitSha.slice(0, 8)} \u2192 tree=${treeSha.slice(0, 8)}`);
|
|
452
|
+
const treeUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/git/trees/${treeSha}${sep}recursive=${recursive ? 1 : 0}&${auth}`;
|
|
453
|
+
const treeRes = await fetch(treeUrl);
|
|
454
|
+
if (!treeRes.ok) throw new Error(`Gitee: tree ${treeSha} not found (${treeRes.status})`);
|
|
455
|
+
const treeData = await treeRes.json();
|
|
456
|
+
return treeData.tree || [];
|
|
631
457
|
};
|
|
632
|
-
return
|
|
458
|
+
return wrapZenFSFileSystem({
|
|
459
|
+
backend: zenGitee.Gitee,
|
|
460
|
+
token: options.token,
|
|
461
|
+
owner: options.owner,
|
|
462
|
+
repo: options.repo,
|
|
463
|
+
branch: options.branch,
|
|
464
|
+
baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
|
|
465
|
+
});
|
|
633
466
|
});
|
|
634
467
|
registerBackend("WebDAV", async (options) => {
|
|
635
468
|
const url = options.url ?? "";
|
|
@@ -856,6 +689,10 @@ var ConfigRepo = class {
|
|
|
856
689
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
857
690
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
858
691
|
}
|
|
692
|
+
/** Full path to this node's directory on the primary backend. */
|
|
693
|
+
get nodePath() {
|
|
694
|
+
return `/nodes/${this.nodeId}`;
|
|
695
|
+
}
|
|
859
696
|
// -----------------------------------------------------------------------
|
|
860
697
|
// IConfigRepo — Load
|
|
861
698
|
// -----------------------------------------------------------------------
|
|
@@ -1074,11 +911,14 @@ var ConfigRepo = class {
|
|
|
1074
911
|
// -----------------------------------------------------------------------
|
|
1075
912
|
// Internal — Setup
|
|
1076
913
|
// -----------------------------------------------------------------------
|
|
1077
|
-
async setupSync(
|
|
914
|
+
async setupSync(backends, primaryBackendId) {
|
|
1078
915
|
console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId}`);
|
|
1079
|
-
console.log(`[ConfigRepo] setupSync: rules=`, JSON.stringify(rules, null, 2));
|
|
1080
916
|
for (const desc of backends) {
|
|
1081
917
|
if (desc.id === primaryBackendId) continue;
|
|
918
|
+
if (desc.enabled === false) {
|
|
919
|
+
console.log(`[ConfigRepo] Skipping disabled replica: ${desc.id}`);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
1082
922
|
console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
1083
923
|
try {
|
|
1084
924
|
const instance = await createBackend(desc);
|
|
@@ -1090,39 +930,23 @@ var ConfigRepo = class {
|
|
|
1090
930
|
}
|
|
1091
931
|
}
|
|
1092
932
|
console.log(`[ConfigRepo] Available replicas:`, Array.from(this.replicaBackends.keys()));
|
|
1093
|
-
for (const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
{
|
|
1111
|
-
direction: zenSyncDirection,
|
|
1112
|
-
conflictStrategy: rule.conflictStrategy,
|
|
1113
|
-
filter: {
|
|
1114
|
-
includePrefixes: [rule.prefix]
|
|
1115
|
-
}
|
|
1116
|
-
},
|
|
1117
|
-
"/"
|
|
1118
|
-
);
|
|
1119
|
-
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, prefix=${rule.prefix}, dir=${rule.direction}, replica=${replicaId}`);
|
|
1120
|
-
const conflictHandler = (event) => {
|
|
1121
|
-
this.handleConflict(event, rule);
|
|
1122
|
-
};
|
|
1123
|
-
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
1124
|
-
this.syncEngine.watch(pair.pairId);
|
|
1125
|
-
}
|
|
933
|
+
for (const [replicaId, replica] of this.replicaBackends.entries()) {
|
|
934
|
+
const pair = this.syncEngine.addPair(
|
|
935
|
+
this.fullFS,
|
|
936
|
+
replica.syncable,
|
|
937
|
+
{
|
|
938
|
+
direction: import_zen_fs_sync.SyncDirection.OneWay,
|
|
939
|
+
conflictStrategy: "source-wins"
|
|
940
|
+
// No filter = sync everything under root
|
|
941
|
+
},
|
|
942
|
+
"/"
|
|
943
|
+
);
|
|
944
|
+
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
|
|
945
|
+
const conflictHandler = (event) => {
|
|
946
|
+
this.handleConflict(event, { prefix: "/", direction: "one-way" });
|
|
947
|
+
};
|
|
948
|
+
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
949
|
+
this.syncEngine.watch(pair.pairId);
|
|
1126
950
|
}
|
|
1127
951
|
console.log(`[ConfigRepo] setupSync complete. Sync statuses:`, this.getSyncStatuses());
|
|
1128
952
|
}
|
|
@@ -1440,7 +1264,6 @@ async function createConfigRepo(appId, options) {
|
|
|
1440
1264
|
options.onConflict
|
|
1441
1265
|
);
|
|
1442
1266
|
await repo.setupSync(
|
|
1443
|
-
syncRulesMeta.rules,
|
|
1444
1267
|
backendsMeta.backends,
|
|
1445
1268
|
options.primaryBackendId
|
|
1446
1269
|
);
|
package/dist/index.mjs
CHANGED
|
@@ -306,6 +306,7 @@ async function wrapZenFSFileSystem(config) {
|
|
|
306
306
|
await isolatedFS.createFile(path, { uid: 0, gid: 0, mode: 420 });
|
|
307
307
|
}
|
|
308
308
|
await isolatedFS.write(path, bytes, 0);
|
|
309
|
+
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: Date.now() });
|
|
309
310
|
},
|
|
310
311
|
async readdir(path) {
|
|
311
312
|
return isolatedFS.readdir(path);
|
|
@@ -362,227 +363,59 @@ registerBackend("WebStorage", async (options) => {
|
|
|
362
363
|
return wrapZenFSFileSystem({ backend: WebStorage, storage });
|
|
363
364
|
});
|
|
364
365
|
registerBackend("GitHub", async (options) => {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
"User-Agent": "zen-fs-config"
|
|
374
|
-
};
|
|
375
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
376
|
-
const apiUrl = (path) => {
|
|
377
|
-
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
378
|
-
return `${baseUrl}/repos/${owner}/${repo}/contents/${p}?ref=${branch}`;
|
|
379
|
-
};
|
|
380
|
-
const treeUrl = () => `${baseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
381
|
-
const ghStat = (item) => ({
|
|
382
|
-
isFile: () => item.type === "file",
|
|
383
|
-
isDirectory: () => item.type === "dir",
|
|
384
|
-
size: item.size ?? 0
|
|
366
|
+
const { Github } = await import("zen-fs-github");
|
|
367
|
+
return wrapZenFSFileSystem({
|
|
368
|
+
backend: Github,
|
|
369
|
+
token: options.token,
|
|
370
|
+
owner: options.owner,
|
|
371
|
+
repo: options.repo,
|
|
372
|
+
branch: options.branch,
|
|
373
|
+
baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
|
|
385
374
|
});
|
|
386
|
-
const fetchJson = async (url) => {
|
|
387
|
-
const res = await fetch(url, { headers });
|
|
388
|
-
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`);
|
|
389
|
-
return res.json();
|
|
390
|
-
};
|
|
391
|
-
const backend = {
|
|
392
|
-
async readFile(path, ...args) {
|
|
393
|
-
const data = await fetchJson(apiUrl(path));
|
|
394
|
-
if (data.encoding === "base64") {
|
|
395
|
-
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
396
|
-
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
397
|
-
return raw;
|
|
398
|
-
}
|
|
399
|
-
return data;
|
|
400
|
-
},
|
|
401
|
-
async writeFile(path, data, options2) {
|
|
402
|
-
const message = options2?.message || `Update ${path}`;
|
|
403
|
-
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
404
|
-
const sha = await (async () => {
|
|
405
|
-
try {
|
|
406
|
-
const d = await fetchJson(apiUrl(path));
|
|
407
|
-
return d.sha;
|
|
408
|
-
} catch {
|
|
409
|
-
return void 0;
|
|
410
|
-
}
|
|
411
|
-
})();
|
|
412
|
-
await fetch(apiUrl(path), {
|
|
413
|
-
method: "PUT",
|
|
414
|
-
headers,
|
|
415
|
-
body: JSON.stringify({ message, content, sha, branch })
|
|
416
|
-
});
|
|
417
|
-
},
|
|
418
|
-
async readdir(path) {
|
|
419
|
-
const data = await fetchJson(apiUrl(path));
|
|
420
|
-
return data.map((item) => item.name);
|
|
421
|
-
},
|
|
422
|
-
async stat(path, ...args) {
|
|
423
|
-
try {
|
|
424
|
-
const data = await fetchJson(apiUrl(path));
|
|
425
|
-
if (Array.isArray(data)) {
|
|
426
|
-
return { isFile: () => false, isDirectory: () => true, size: 0 };
|
|
427
|
-
}
|
|
428
|
-
return ghStat(data);
|
|
429
|
-
} catch {
|
|
430
|
-
throw new Error(`ENOENT: ${path}`);
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
|
-
async exists(path) {
|
|
434
|
-
try {
|
|
435
|
-
await fetchJson(apiUrl(path));
|
|
436
|
-
return true;
|
|
437
|
-
} catch {
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
},
|
|
441
|
-
async mkdir(path, options2) {
|
|
442
|
-
const dirPath = path.replace(/\/$/, "");
|
|
443
|
-
const keepPath = `${dirPath}/.gitkeep`;
|
|
444
|
-
const message = options2?.message || `Create directory ${dirPath}`;
|
|
445
|
-
const content = btoa("");
|
|
446
|
-
await fetch(apiUrl(keepPath), {
|
|
447
|
-
method: "PUT",
|
|
448
|
-
headers,
|
|
449
|
-
body: JSON.stringify({ message, content, branch })
|
|
450
|
-
});
|
|
451
|
-
},
|
|
452
|
-
async unlink(path) {
|
|
453
|
-
const data = await fetchJson(apiUrl(path));
|
|
454
|
-
await fetch(apiUrl(path), {
|
|
455
|
-
method: "DELETE",
|
|
456
|
-
headers,
|
|
457
|
-
body: JSON.stringify({ message: `Delete ${path}`, sha: data.sha, branch })
|
|
458
|
-
});
|
|
459
|
-
},
|
|
460
|
-
async rmdir(path) {
|
|
461
|
-
const items = await fetchJson(apiUrl(path));
|
|
462
|
-
if (Array.isArray(items)) {
|
|
463
|
-
for (const item of items) {
|
|
464
|
-
const itemPath = `${path}/${item.name}`;
|
|
465
|
-
if (item.type === "dir") {
|
|
466
|
-
await backend.rmdir(itemPath);
|
|
467
|
-
} else {
|
|
468
|
-
await backend.unlink(itemPath);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
},
|
|
473
|
-
async rename(oldPath, newPath) {
|
|
474
|
-
const content = await backend.readFile(oldPath);
|
|
475
|
-
await backend.writeFile(newPath, content);
|
|
476
|
-
await backend.unlink(oldPath);
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
return backend;
|
|
480
375
|
});
|
|
481
376
|
registerBackend("Gitee", async (options) => {
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return res.json();
|
|
492
|
-
};
|
|
493
|
-
const apiUrl = (path) => {
|
|
494
|
-
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
495
|
-
const params = new URLSearchParams({ access_token: token, ref: branch, path: p });
|
|
496
|
-
return `${baseUrl}/repos/${owner}/${repo}/contents?${params}`;
|
|
497
|
-
};
|
|
498
|
-
const ghStat = (item) => ({
|
|
499
|
-
isFile: () => item.type === "file",
|
|
500
|
-
isDirectory: () => item.type === "dir",
|
|
501
|
-
size: item.size ?? 0
|
|
502
|
-
});
|
|
503
|
-
const backend = {
|
|
504
|
-
async readFile(path, ...args) {
|
|
505
|
-
const data = await fetchJson(apiUrl(path));
|
|
506
|
-
if (data.content) {
|
|
507
|
-
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
508
|
-
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
509
|
-
return raw;
|
|
510
|
-
}
|
|
511
|
-
return data;
|
|
512
|
-
},
|
|
513
|
-
async writeFile(path, data, options2) {
|
|
514
|
-
const message = options2?.message || `Update ${path}`;
|
|
515
|
-
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
516
|
-
const sha = await (async () => {
|
|
517
|
-
try {
|
|
518
|
-
const d = await fetchJson(apiUrl(path));
|
|
519
|
-
return d.sha;
|
|
520
|
-
} catch {
|
|
521
|
-
return void 0;
|
|
522
|
-
}
|
|
523
|
-
})();
|
|
524
|
-
await fetch(apiUrl(path), {
|
|
525
|
-
method: "POST",
|
|
526
|
-
headers: { "Content-Type": "application/json" },
|
|
527
|
-
body: JSON.stringify({ access_token: token, message, content, sha, branch })
|
|
528
|
-
});
|
|
529
|
-
},
|
|
530
|
-
async readdir(path) {
|
|
531
|
-
const data = await fetchJson(apiUrl(path));
|
|
532
|
-
return Array.isArray(data) ? data.map((i) => i.name) : [];
|
|
533
|
-
},
|
|
534
|
-
async stat(path) {
|
|
535
|
-
try {
|
|
536
|
-
const data = await fetchJson(apiUrl(path));
|
|
537
|
-
if (Array.isArray(data)) return ghStat({ type: "dir", size: 0 });
|
|
538
|
-
return ghStat(data);
|
|
539
|
-
} catch {
|
|
540
|
-
throw new Error(`ENOENT: ${path}`);
|
|
541
|
-
}
|
|
542
|
-
},
|
|
543
|
-
async exists(path) {
|
|
544
|
-
try {
|
|
545
|
-
await fetchJson(apiUrl(path));
|
|
546
|
-
return true;
|
|
547
|
-
} catch {
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
},
|
|
551
|
-
async mkdir(path, options2) {
|
|
552
|
-
const dirPath = path.replace(/\/$/, "");
|
|
553
|
-
const keepPath = `${dirPath}/.gitkeep`;
|
|
554
|
-
const message = options2?.message || `Create directory ${dirPath}`;
|
|
555
|
-
await fetch(apiUrl(keepPath), {
|
|
556
|
-
method: "POST",
|
|
557
|
-
headers: { "Content-Type": "application/json" },
|
|
558
|
-
body: JSON.stringify({ access_token: token, message, content: btoa(""), branch })
|
|
559
|
-
});
|
|
560
|
-
},
|
|
561
|
-
async unlink(path) {
|
|
562
|
-
const data = await fetchJson(apiUrl(path));
|
|
563
|
-
await fetch(apiUrl(path), {
|
|
564
|
-
method: "DELETE",
|
|
565
|
-
headers: { "Content-Type": "application/json" },
|
|
566
|
-
body: JSON.stringify({ access_token: token, message: `Delete ${path}`, sha: data.sha, branch })
|
|
567
|
-
});
|
|
568
|
-
},
|
|
569
|
-
async rmdir(path) {
|
|
570
|
-
const items = await fetchJson(apiUrl(path));
|
|
571
|
-
if (Array.isArray(items)) {
|
|
572
|
-
for (const item of items) {
|
|
573
|
-
const itemPath = `${path}/${item.name}`;
|
|
574
|
-
if (item.type === "dir") await backend.rmdir(itemPath);
|
|
575
|
-
else await backend.unlink(itemPath);
|
|
576
|
-
}
|
|
377
|
+
const zenGitee = await import("zen-fs-gitee");
|
|
378
|
+
const { GiteeAPI } = await import("zen-fs-gitee/dist/gitee-api.js");
|
|
379
|
+
const originalGetTree = GiteeAPI.prototype.getTree;
|
|
380
|
+
GiteeAPI.prototype.getTree = async function(recursive = true) {
|
|
381
|
+
try {
|
|
382
|
+
return await originalGetTree.call(this, recursive);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (!err.message?.includes("404") && !err.message?.includes("Tree not found")) {
|
|
385
|
+
throw err;
|
|
577
386
|
}
|
|
578
|
-
},
|
|
579
|
-
async rename(oldPath, newPath) {
|
|
580
|
-
const content = await backend.readFile(oldPath);
|
|
581
|
-
await backend.writeFile(newPath, content);
|
|
582
|
-
await backend.unlink(oldPath);
|
|
583
387
|
}
|
|
388
|
+
console.log(`[Gitee] getTree failed with branch="${this.branch}", resolving to SHA...`);
|
|
389
|
+
const baseUrl = this.baseUrl || "https://gitee.com/api/v5";
|
|
390
|
+
const sep = "?";
|
|
391
|
+
const auth = `access_token=${this.token}`;
|
|
392
|
+
const branchUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/branches/${this.branch}${sep}${auth}`;
|
|
393
|
+
const branchRes = await fetch(branchUrl);
|
|
394
|
+
if (!branchRes.ok) throw new Error(`Gitee: branch "${this.branch}" not found (${branchRes.status})`);
|
|
395
|
+
const branchData = await branchRes.json();
|
|
396
|
+
const commitSha = branchData.commit?.sha;
|
|
397
|
+
if (!commitSha) throw new Error(`Gitee: could not get commit SHA for branch "${this.branch}"`);
|
|
398
|
+
const commitUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/git/commits/${commitSha}${sep}${auth}`;
|
|
399
|
+
const commitRes = await fetch(commitUrl);
|
|
400
|
+
if (!commitRes.ok) throw new Error(`Gitee: commit ${commitSha} not found (${commitRes.status})`);
|
|
401
|
+
const commitData = await commitRes.json();
|
|
402
|
+
const treeSha = commitData.tree?.sha;
|
|
403
|
+
if (!treeSha) throw new Error(`Gitee: could not get tree SHA from commit ${commitSha}`);
|
|
404
|
+
console.log(`[Gitee] Resolved branch="${this.branch}" \u2192 commit=${commitSha.slice(0, 8)} \u2192 tree=${treeSha.slice(0, 8)}`);
|
|
405
|
+
const treeUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/git/trees/${treeSha}${sep}recursive=${recursive ? 1 : 0}&${auth}`;
|
|
406
|
+
const treeRes = await fetch(treeUrl);
|
|
407
|
+
if (!treeRes.ok) throw new Error(`Gitee: tree ${treeSha} not found (${treeRes.status})`);
|
|
408
|
+
const treeData = await treeRes.json();
|
|
409
|
+
return treeData.tree || [];
|
|
584
410
|
};
|
|
585
|
-
return
|
|
411
|
+
return wrapZenFSFileSystem({
|
|
412
|
+
backend: zenGitee.Gitee,
|
|
413
|
+
token: options.token,
|
|
414
|
+
owner: options.owner,
|
|
415
|
+
repo: options.repo,
|
|
416
|
+
branch: options.branch,
|
|
417
|
+
baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
|
|
418
|
+
});
|
|
586
419
|
});
|
|
587
420
|
registerBackend("WebDAV", async (options) => {
|
|
588
421
|
const url = options.url ?? "";
|
|
@@ -809,6 +642,10 @@ var ConfigRepo = class {
|
|
|
809
642
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
810
643
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
811
644
|
}
|
|
645
|
+
/** Full path to this node's directory on the primary backend. */
|
|
646
|
+
get nodePath() {
|
|
647
|
+
return `/nodes/${this.nodeId}`;
|
|
648
|
+
}
|
|
812
649
|
// -----------------------------------------------------------------------
|
|
813
650
|
// IConfigRepo — Load
|
|
814
651
|
// -----------------------------------------------------------------------
|
|
@@ -1027,11 +864,14 @@ var ConfigRepo = class {
|
|
|
1027
864
|
// -----------------------------------------------------------------------
|
|
1028
865
|
// Internal — Setup
|
|
1029
866
|
// -----------------------------------------------------------------------
|
|
1030
|
-
async setupSync(
|
|
867
|
+
async setupSync(backends, primaryBackendId) {
|
|
1031
868
|
console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId}`);
|
|
1032
|
-
console.log(`[ConfigRepo] setupSync: rules=`, JSON.stringify(rules, null, 2));
|
|
1033
869
|
for (const desc of backends) {
|
|
1034
870
|
if (desc.id === primaryBackendId) continue;
|
|
871
|
+
if (desc.enabled === false) {
|
|
872
|
+
console.log(`[ConfigRepo] Skipping disabled replica: ${desc.id}`);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
1035
875
|
console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
1036
876
|
try {
|
|
1037
877
|
const instance = await createBackend(desc);
|
|
@@ -1043,39 +883,23 @@ var ConfigRepo = class {
|
|
|
1043
883
|
}
|
|
1044
884
|
}
|
|
1045
885
|
console.log(`[ConfigRepo] Available replicas:`, Array.from(this.replicaBackends.keys()));
|
|
1046
|
-
for (const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
{
|
|
1064
|
-
direction: zenSyncDirection,
|
|
1065
|
-
conflictStrategy: rule.conflictStrategy,
|
|
1066
|
-
filter: {
|
|
1067
|
-
includePrefixes: [rule.prefix]
|
|
1068
|
-
}
|
|
1069
|
-
},
|
|
1070
|
-
"/"
|
|
1071
|
-
);
|
|
1072
|
-
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, prefix=${rule.prefix}, dir=${rule.direction}, replica=${replicaId}`);
|
|
1073
|
-
const conflictHandler = (event) => {
|
|
1074
|
-
this.handleConflict(event, rule);
|
|
1075
|
-
};
|
|
1076
|
-
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
1077
|
-
this.syncEngine.watch(pair.pairId);
|
|
1078
|
-
}
|
|
886
|
+
for (const [replicaId, replica] of this.replicaBackends.entries()) {
|
|
887
|
+
const pair = this.syncEngine.addPair(
|
|
888
|
+
this.fullFS,
|
|
889
|
+
replica.syncable,
|
|
890
|
+
{
|
|
891
|
+
direction: SyncDirection.OneWay,
|
|
892
|
+
conflictStrategy: "source-wins"
|
|
893
|
+
// No filter = sync everything under root
|
|
894
|
+
},
|
|
895
|
+
"/"
|
|
896
|
+
);
|
|
897
|
+
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
|
|
898
|
+
const conflictHandler = (event) => {
|
|
899
|
+
this.handleConflict(event, { prefix: "/", direction: "one-way" });
|
|
900
|
+
};
|
|
901
|
+
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
902
|
+
this.syncEngine.watch(pair.pairId);
|
|
1079
903
|
}
|
|
1080
904
|
console.log(`[ConfigRepo] setupSync complete. Sync statuses:`, this.getSyncStatuses());
|
|
1081
905
|
}
|
|
@@ -1393,7 +1217,6 @@ async function createConfigRepo(appId, options) {
|
|
|
1393
1217
|
options.onConflict
|
|
1394
1218
|
);
|
|
1395
1219
|
await repo.setupSync(
|
|
1396
|
-
syncRulesMeta.rules,
|
|
1397
1220
|
backendsMeta.backends,
|
|
1398
1221
|
options.primaryBackendId
|
|
1399
1222
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-fs-config",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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",
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@zenfs/core": ">=2.3.0",
|
|
41
41
|
"zen-fs-cache": ">=1.0.0",
|
|
42
|
+
"zen-fs-gitee": ">=1.0.0",
|
|
43
|
+
"zen-fs-github": ">=1.0.0",
|
|
42
44
|
"zen-fs-sync": ">=0.1.0"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
@@ -46,6 +48,8 @@
|
|
|
46
48
|
"tsup": "^8.5.1",
|
|
47
49
|
"typescript": "^5.9.3",
|
|
48
50
|
"zen-fs-cache": "^1.0.1",
|
|
51
|
+
"zen-fs-gitee": "^1.0.0",
|
|
52
|
+
"zen-fs-github": "^1.0.0",
|
|
49
53
|
"zen-fs-sync": "^0.1.0"
|
|
50
54
|
},
|
|
51
55
|
"dependencies": {
|