zen-fs-config 0.3.12 → 0.3.14
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 +79 -251
- package/dist/index.mjs +79 -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
|
@@ -410,227 +410,65 @@ registerBackend("WebStorage", async (options) => {
|
|
|
410
410
|
return wrapZenFSFileSystem({ backend: WebStorage, storage });
|
|
411
411
|
});
|
|
412
412
|
registerBackend("GitHub", async (options) => {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
"User-Agent": "zen-fs-config"
|
|
422
|
-
};
|
|
423
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
424
|
-
const apiUrl = (path) => {
|
|
425
|
-
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
426
|
-
return `${baseUrl}/repos/${owner}/${repo}/contents/${p}?ref=${branch}`;
|
|
427
|
-
};
|
|
428
|
-
const treeUrl = () => `${baseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
429
|
-
const ghStat = (item) => ({
|
|
430
|
-
isFile: () => item.type === "file",
|
|
431
|
-
isDirectory: () => item.type === "dir",
|
|
432
|
-
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
|
|
433
421
|
});
|
|
434
|
-
const fetchJson = async (url) => {
|
|
435
|
-
const res = await fetch(url, { headers });
|
|
436
|
-
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`);
|
|
437
|
-
return res.json();
|
|
438
|
-
};
|
|
439
|
-
const backend = {
|
|
440
|
-
async readFile(path, ...args) {
|
|
441
|
-
const data = await fetchJson(apiUrl(path));
|
|
442
|
-
if (data.encoding === "base64") {
|
|
443
|
-
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
444
|
-
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
445
|
-
return raw;
|
|
446
|
-
}
|
|
447
|
-
return data;
|
|
448
|
-
},
|
|
449
|
-
async writeFile(path, data, options2) {
|
|
450
|
-
const message = options2?.message || `Update ${path}`;
|
|
451
|
-
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
452
|
-
const sha = await (async () => {
|
|
453
|
-
try {
|
|
454
|
-
const d = await fetchJson(apiUrl(path));
|
|
455
|
-
return d.sha;
|
|
456
|
-
} catch {
|
|
457
|
-
return void 0;
|
|
458
|
-
}
|
|
459
|
-
})();
|
|
460
|
-
await fetch(apiUrl(path), {
|
|
461
|
-
method: "PUT",
|
|
462
|
-
headers,
|
|
463
|
-
body: JSON.stringify({ message, content, sha, branch })
|
|
464
|
-
});
|
|
465
|
-
},
|
|
466
|
-
async readdir(path) {
|
|
467
|
-
const data = await fetchJson(apiUrl(path));
|
|
468
|
-
return data.map((item) => item.name);
|
|
469
|
-
},
|
|
470
|
-
async stat(path, ...args) {
|
|
471
|
-
try {
|
|
472
|
-
const data = await fetchJson(apiUrl(path));
|
|
473
|
-
if (Array.isArray(data)) {
|
|
474
|
-
return { isFile: () => false, isDirectory: () => true, size: 0 };
|
|
475
|
-
}
|
|
476
|
-
return ghStat(data);
|
|
477
|
-
} catch {
|
|
478
|
-
throw new Error(`ENOENT: ${path}`);
|
|
479
|
-
}
|
|
480
|
-
},
|
|
481
|
-
async exists(path) {
|
|
482
|
-
try {
|
|
483
|
-
await fetchJson(apiUrl(path));
|
|
484
|
-
return true;
|
|
485
|
-
} catch {
|
|
486
|
-
return false;
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
async mkdir(path, options2) {
|
|
490
|
-
const dirPath = path.replace(/\/$/, "");
|
|
491
|
-
const keepPath = `${dirPath}/.gitkeep`;
|
|
492
|
-
const message = options2?.message || `Create directory ${dirPath}`;
|
|
493
|
-
const content = btoa("");
|
|
494
|
-
await fetch(apiUrl(keepPath), {
|
|
495
|
-
method: "PUT",
|
|
496
|
-
headers,
|
|
497
|
-
body: JSON.stringify({ message, content, branch })
|
|
498
|
-
});
|
|
499
|
-
},
|
|
500
|
-
async unlink(path) {
|
|
501
|
-
const data = await fetchJson(apiUrl(path));
|
|
502
|
-
await fetch(apiUrl(path), {
|
|
503
|
-
method: "DELETE",
|
|
504
|
-
headers,
|
|
505
|
-
body: JSON.stringify({ message: `Delete ${path}`, sha: data.sha, branch })
|
|
506
|
-
});
|
|
507
|
-
},
|
|
508
|
-
async rmdir(path) {
|
|
509
|
-
const items = await fetchJson(apiUrl(path));
|
|
510
|
-
if (Array.isArray(items)) {
|
|
511
|
-
for (const item of items) {
|
|
512
|
-
const itemPath = `${path}/${item.name}`;
|
|
513
|
-
if (item.type === "dir") {
|
|
514
|
-
await backend.rmdir(itemPath);
|
|
515
|
-
} else {
|
|
516
|
-
await backend.unlink(itemPath);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
},
|
|
521
|
-
async rename(oldPath, newPath) {
|
|
522
|
-
const content = await backend.readFile(oldPath);
|
|
523
|
-
await backend.writeFile(newPath, content);
|
|
524
|
-
await backend.unlink(oldPath);
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
return backend;
|
|
528
422
|
});
|
|
529
423
|
registerBackend("Gitee", async (options) => {
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const apiUrl = (path) => {
|
|
542
|
-
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
543
|
-
const params = new URLSearchParams({ access_token: token, ref: branch, path: p });
|
|
544
|
-
return `${baseUrl}/repos/${owner}/${repo}/contents?${params}`;
|
|
545
|
-
};
|
|
546
|
-
const ghStat = (item) => ({
|
|
547
|
-
isFile: () => item.type === "file",
|
|
548
|
-
isDirectory: () => item.type === "dir",
|
|
549
|
-
size: item.size ?? 0
|
|
550
|
-
});
|
|
551
|
-
const backend = {
|
|
552
|
-
async readFile(path, ...args) {
|
|
553
|
-
const data = await fetchJson(apiUrl(path));
|
|
554
|
-
if (data.content) {
|
|
555
|
-
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
556
|
-
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
557
|
-
return raw;
|
|
558
|
-
}
|
|
559
|
-
return data;
|
|
560
|
-
},
|
|
561
|
-
async writeFile(path, data, options2) {
|
|
562
|
-
const message = options2?.message || `Update ${path}`;
|
|
563
|
-
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
564
|
-
const sha = await (async () => {
|
|
565
|
-
try {
|
|
566
|
-
const d = await fetchJson(apiUrl(path));
|
|
567
|
-
return d.sha;
|
|
568
|
-
} catch {
|
|
569
|
-
return void 0;
|
|
570
|
-
}
|
|
571
|
-
})();
|
|
572
|
-
await fetch(apiUrl(path), {
|
|
573
|
-
method: "POST",
|
|
574
|
-
headers: { "Content-Type": "application/json" },
|
|
575
|
-
body: JSON.stringify({ access_token: token, message, content, sha, branch })
|
|
576
|
-
});
|
|
577
|
-
},
|
|
578
|
-
async readdir(path) {
|
|
579
|
-
const data = await fetchJson(apiUrl(path));
|
|
580
|
-
return Array.isArray(data) ? data.map((i) => i.name) : [];
|
|
581
|
-
},
|
|
582
|
-
async stat(path) {
|
|
583
|
-
try {
|
|
584
|
-
const data = await fetchJson(apiUrl(path));
|
|
585
|
-
if (Array.isArray(data)) return ghStat({ type: "dir", size: 0 });
|
|
586
|
-
return ghStat(data);
|
|
587
|
-
} catch {
|
|
588
|
-
throw new Error(`ENOENT: ${path}`);
|
|
589
|
-
}
|
|
590
|
-
},
|
|
591
|
-
async exists(path) {
|
|
592
|
-
try {
|
|
593
|
-
await fetchJson(apiUrl(path));
|
|
594
|
-
return true;
|
|
595
|
-
} catch {
|
|
596
|
-
return false;
|
|
597
|
-
}
|
|
598
|
-
},
|
|
599
|
-
async mkdir(path, options2) {
|
|
600
|
-
const dirPath = path.replace(/\/$/, "");
|
|
601
|
-
const keepPath = `${dirPath}/.gitkeep`;
|
|
602
|
-
const message = options2?.message || `Create directory ${dirPath}`;
|
|
603
|
-
await fetch(apiUrl(keepPath), {
|
|
604
|
-
method: "POST",
|
|
605
|
-
headers: { "Content-Type": "application/json" },
|
|
606
|
-
body: JSON.stringify({ access_token: token, message, content: btoa(""), branch })
|
|
607
|
-
});
|
|
608
|
-
},
|
|
609
|
-
async unlink(path) {
|
|
610
|
-
const data = await fetchJson(apiUrl(path));
|
|
611
|
-
await fetch(apiUrl(path), {
|
|
612
|
-
method: "DELETE",
|
|
613
|
-
headers: { "Content-Type": "application/json" },
|
|
614
|
-
body: JSON.stringify({ access_token: token, message: `Delete ${path}`, sha: data.sha, branch })
|
|
615
|
-
});
|
|
616
|
-
},
|
|
617
|
-
async rmdir(path) {
|
|
618
|
-
const items = await fetchJson(apiUrl(path));
|
|
619
|
-
if (Array.isArray(items)) {
|
|
620
|
-
for (const item of items) {
|
|
621
|
-
const itemPath = `${path}/${item.name}`;
|
|
622
|
-
if (item.type === "dir") await backend.rmdir(itemPath);
|
|
623
|
-
else await backend.unlink(itemPath);
|
|
624
|
-
}
|
|
424
|
+
const zenGitee = await import("zen-fs-gitee");
|
|
425
|
+
const { GiteeFS } = zenGitee;
|
|
426
|
+
const origInit = GiteeFS.prototype.init;
|
|
427
|
+
const patched = /* @__PURE__ */ new WeakSet();
|
|
428
|
+
GiteeFS.prototype.init = async function() {
|
|
429
|
+
let firstErr;
|
|
430
|
+
try {
|
|
431
|
+
return await origInit.call(this);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
if (!err.message?.includes("404") && !err.message?.includes("Tree not found")) {
|
|
434
|
+
throw err;
|
|
625
435
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
436
|
+
firstErr = err;
|
|
437
|
+
}
|
|
438
|
+
if (patched.has(this)) throw firstErr;
|
|
439
|
+
patched.add(this);
|
|
440
|
+
console.log(`[Gitee] init failed with branch="${this.api.branch}", resolving to SHA...`);
|
|
441
|
+
const baseUrl = this.api.baseUrl || "https://gitee.com/api/v5";
|
|
442
|
+
const auth = `access_token=${this.api.token}`;
|
|
443
|
+
const branchUrl = `${baseUrl}/repos/${this.api.owner}/${this.api.repo}/branches/${this.api.branch}?${auth}`;
|
|
444
|
+
const branchRes = await fetch(branchUrl);
|
|
445
|
+
if (!branchRes.ok) throw new Error(`Gitee: branch "${this.api.branch}" not found (${branchRes.status})`);
|
|
446
|
+
const branchData = await branchRes.json();
|
|
447
|
+
const commitSha = branchData.commit?.sha;
|
|
448
|
+
if (!commitSha) throw new Error(`Gitee: could not get commit SHA for branch "${this.api.branch}"`);
|
|
449
|
+
const commitUrl = `${baseUrl}/repos/${this.api.owner}/${this.api.repo}/git/commits/${commitSha}?${auth}`;
|
|
450
|
+
const commitRes = await fetch(commitUrl);
|
|
451
|
+
if (!commitRes.ok) throw new Error(`Gitee: commit ${commitSha} not found (${commitRes.status})`);
|
|
452
|
+
const commitData = await commitRes.json();
|
|
453
|
+
const treeSha = commitData.tree?.sha;
|
|
454
|
+
if (!treeSha) throw new Error(`Gitee: could not get tree SHA from commit ${commitSha}`);
|
|
455
|
+
const realBranch = this.api.branch;
|
|
456
|
+
this.api.branch = treeSha;
|
|
457
|
+
console.log(`[Gitee] Resolved branch="${realBranch}" \u2192 commit=${commitSha.slice(0, 8)} \u2192 tree=${treeSha.slice(0, 8)}`);
|
|
458
|
+
try {
|
|
459
|
+
return await origInit.call(this);
|
|
460
|
+
} finally {
|
|
461
|
+
this.api.branch = realBranch;
|
|
631
462
|
}
|
|
632
463
|
};
|
|
633
|
-
return
|
|
464
|
+
return wrapZenFSFileSystem({
|
|
465
|
+
backend: zenGitee.Gitee,
|
|
466
|
+
token: options.token,
|
|
467
|
+
owner: options.owner,
|
|
468
|
+
repo: options.repo,
|
|
469
|
+
branch: options.branch,
|
|
470
|
+
baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
|
|
471
|
+
});
|
|
634
472
|
});
|
|
635
473
|
registerBackend("WebDAV", async (options) => {
|
|
636
474
|
const url = options.url ?? "";
|
|
@@ -857,6 +695,10 @@ var ConfigRepo = class {
|
|
|
857
695
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
858
696
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
859
697
|
}
|
|
698
|
+
/** Full path to this node's directory on the primary backend. */
|
|
699
|
+
get nodePath() {
|
|
700
|
+
return `/nodes/${this.nodeId}`;
|
|
701
|
+
}
|
|
860
702
|
// -----------------------------------------------------------------------
|
|
861
703
|
// IConfigRepo — Load
|
|
862
704
|
// -----------------------------------------------------------------------
|
|
@@ -1075,11 +917,14 @@ var ConfigRepo = class {
|
|
|
1075
917
|
// -----------------------------------------------------------------------
|
|
1076
918
|
// Internal — Setup
|
|
1077
919
|
// -----------------------------------------------------------------------
|
|
1078
|
-
async setupSync(
|
|
920
|
+
async setupSync(backends, primaryBackendId) {
|
|
1079
921
|
console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId}`);
|
|
1080
|
-
console.log(`[ConfigRepo] setupSync: rules=`, JSON.stringify(rules, null, 2));
|
|
1081
922
|
for (const desc of backends) {
|
|
1082
923
|
if (desc.id === primaryBackendId) continue;
|
|
924
|
+
if (desc.enabled === false) {
|
|
925
|
+
console.log(`[ConfigRepo] Skipping disabled replica: ${desc.id}`);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
1083
928
|
console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
1084
929
|
try {
|
|
1085
930
|
const instance = await createBackend(desc);
|
|
@@ -1091,39 +936,23 @@ var ConfigRepo = class {
|
|
|
1091
936
|
}
|
|
1092
937
|
}
|
|
1093
938
|
console.log(`[ConfigRepo] Available replicas:`, Array.from(this.replicaBackends.keys()));
|
|
1094
|
-
for (const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
{
|
|
1112
|
-
direction: zenSyncDirection,
|
|
1113
|
-
conflictStrategy: rule.conflictStrategy,
|
|
1114
|
-
filter: {
|
|
1115
|
-
includePrefixes: [rule.prefix]
|
|
1116
|
-
}
|
|
1117
|
-
},
|
|
1118
|
-
"/"
|
|
1119
|
-
);
|
|
1120
|
-
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, prefix=${rule.prefix}, dir=${rule.direction}, replica=${replicaId}`);
|
|
1121
|
-
const conflictHandler = (event) => {
|
|
1122
|
-
this.handleConflict(event, rule);
|
|
1123
|
-
};
|
|
1124
|
-
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
1125
|
-
this.syncEngine.watch(pair.pairId);
|
|
1126
|
-
}
|
|
939
|
+
for (const [replicaId, replica] of this.replicaBackends.entries()) {
|
|
940
|
+
const pair = this.syncEngine.addPair(
|
|
941
|
+
this.fullFS,
|
|
942
|
+
replica.syncable,
|
|
943
|
+
{
|
|
944
|
+
direction: import_zen_fs_sync.SyncDirection.OneWay,
|
|
945
|
+
conflictStrategy: "source-wins"
|
|
946
|
+
// No filter = sync everything under root
|
|
947
|
+
},
|
|
948
|
+
"/"
|
|
949
|
+
);
|
|
950
|
+
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
|
|
951
|
+
const conflictHandler = (event) => {
|
|
952
|
+
this.handleConflict(event, { prefix: "/", direction: "one-way" });
|
|
953
|
+
};
|
|
954
|
+
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
955
|
+
this.syncEngine.watch(pair.pairId);
|
|
1127
956
|
}
|
|
1128
957
|
console.log(`[ConfigRepo] setupSync complete. Sync statuses:`, this.getSyncStatuses());
|
|
1129
958
|
}
|
|
@@ -1441,7 +1270,6 @@ async function createConfigRepo(appId, options) {
|
|
|
1441
1270
|
options.onConflict
|
|
1442
1271
|
);
|
|
1443
1272
|
await repo.setupSync(
|
|
1444
|
-
syncRulesMeta.rules,
|
|
1445
1273
|
backendsMeta.backends,
|
|
1446
1274
|
options.primaryBackendId
|
|
1447
1275
|
);
|
package/dist/index.mjs
CHANGED
|
@@ -363,227 +363,65 @@ registerBackend("WebStorage", async (options) => {
|
|
|
363
363
|
return wrapZenFSFileSystem({ backend: WebStorage, storage });
|
|
364
364
|
});
|
|
365
365
|
registerBackend("GitHub", async (options) => {
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"User-Agent": "zen-fs-config"
|
|
375
|
-
};
|
|
376
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
377
|
-
const apiUrl = (path) => {
|
|
378
|
-
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
379
|
-
return `${baseUrl}/repos/${owner}/${repo}/contents/${p}?ref=${branch}`;
|
|
380
|
-
};
|
|
381
|
-
const treeUrl = () => `${baseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
382
|
-
const ghStat = (item) => ({
|
|
383
|
-
isFile: () => item.type === "file",
|
|
384
|
-
isDirectory: () => item.type === "dir",
|
|
385
|
-
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
|
|
386
374
|
});
|
|
387
|
-
const fetchJson = async (url) => {
|
|
388
|
-
const res = await fetch(url, { headers });
|
|
389
|
-
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`);
|
|
390
|
-
return res.json();
|
|
391
|
-
};
|
|
392
|
-
const backend = {
|
|
393
|
-
async readFile(path, ...args) {
|
|
394
|
-
const data = await fetchJson(apiUrl(path));
|
|
395
|
-
if (data.encoding === "base64") {
|
|
396
|
-
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
397
|
-
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
398
|
-
return raw;
|
|
399
|
-
}
|
|
400
|
-
return data;
|
|
401
|
-
},
|
|
402
|
-
async writeFile(path, data, options2) {
|
|
403
|
-
const message = options2?.message || `Update ${path}`;
|
|
404
|
-
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
405
|
-
const sha = await (async () => {
|
|
406
|
-
try {
|
|
407
|
-
const d = await fetchJson(apiUrl(path));
|
|
408
|
-
return d.sha;
|
|
409
|
-
} catch {
|
|
410
|
-
return void 0;
|
|
411
|
-
}
|
|
412
|
-
})();
|
|
413
|
-
await fetch(apiUrl(path), {
|
|
414
|
-
method: "PUT",
|
|
415
|
-
headers,
|
|
416
|
-
body: JSON.stringify({ message, content, sha, branch })
|
|
417
|
-
});
|
|
418
|
-
},
|
|
419
|
-
async readdir(path) {
|
|
420
|
-
const data = await fetchJson(apiUrl(path));
|
|
421
|
-
return data.map((item) => item.name);
|
|
422
|
-
},
|
|
423
|
-
async stat(path, ...args) {
|
|
424
|
-
try {
|
|
425
|
-
const data = await fetchJson(apiUrl(path));
|
|
426
|
-
if (Array.isArray(data)) {
|
|
427
|
-
return { isFile: () => false, isDirectory: () => true, size: 0 };
|
|
428
|
-
}
|
|
429
|
-
return ghStat(data);
|
|
430
|
-
} catch {
|
|
431
|
-
throw new Error(`ENOENT: ${path}`);
|
|
432
|
-
}
|
|
433
|
-
},
|
|
434
|
-
async exists(path) {
|
|
435
|
-
try {
|
|
436
|
-
await fetchJson(apiUrl(path));
|
|
437
|
-
return true;
|
|
438
|
-
} catch {
|
|
439
|
-
return false;
|
|
440
|
-
}
|
|
441
|
-
},
|
|
442
|
-
async mkdir(path, options2) {
|
|
443
|
-
const dirPath = path.replace(/\/$/, "");
|
|
444
|
-
const keepPath = `${dirPath}/.gitkeep`;
|
|
445
|
-
const message = options2?.message || `Create directory ${dirPath}`;
|
|
446
|
-
const content = btoa("");
|
|
447
|
-
await fetch(apiUrl(keepPath), {
|
|
448
|
-
method: "PUT",
|
|
449
|
-
headers,
|
|
450
|
-
body: JSON.stringify({ message, content, branch })
|
|
451
|
-
});
|
|
452
|
-
},
|
|
453
|
-
async unlink(path) {
|
|
454
|
-
const data = await fetchJson(apiUrl(path));
|
|
455
|
-
await fetch(apiUrl(path), {
|
|
456
|
-
method: "DELETE",
|
|
457
|
-
headers,
|
|
458
|
-
body: JSON.stringify({ message: `Delete ${path}`, sha: data.sha, branch })
|
|
459
|
-
});
|
|
460
|
-
},
|
|
461
|
-
async rmdir(path) {
|
|
462
|
-
const items = await fetchJson(apiUrl(path));
|
|
463
|
-
if (Array.isArray(items)) {
|
|
464
|
-
for (const item of items) {
|
|
465
|
-
const itemPath = `${path}/${item.name}`;
|
|
466
|
-
if (item.type === "dir") {
|
|
467
|
-
await backend.rmdir(itemPath);
|
|
468
|
-
} else {
|
|
469
|
-
await backend.unlink(itemPath);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
},
|
|
474
|
-
async rename(oldPath, newPath) {
|
|
475
|
-
const content = await backend.readFile(oldPath);
|
|
476
|
-
await backend.writeFile(newPath, content);
|
|
477
|
-
await backend.unlink(oldPath);
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
return backend;
|
|
481
375
|
});
|
|
482
376
|
registerBackend("Gitee", async (options) => {
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const apiUrl = (path) => {
|
|
495
|
-
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
496
|
-
const params = new URLSearchParams({ access_token: token, ref: branch, path: p });
|
|
497
|
-
return `${baseUrl}/repos/${owner}/${repo}/contents?${params}`;
|
|
498
|
-
};
|
|
499
|
-
const ghStat = (item) => ({
|
|
500
|
-
isFile: () => item.type === "file",
|
|
501
|
-
isDirectory: () => item.type === "dir",
|
|
502
|
-
size: item.size ?? 0
|
|
503
|
-
});
|
|
504
|
-
const backend = {
|
|
505
|
-
async readFile(path, ...args) {
|
|
506
|
-
const data = await fetchJson(apiUrl(path));
|
|
507
|
-
if (data.content) {
|
|
508
|
-
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
509
|
-
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
510
|
-
return raw;
|
|
511
|
-
}
|
|
512
|
-
return data;
|
|
513
|
-
},
|
|
514
|
-
async writeFile(path, data, options2) {
|
|
515
|
-
const message = options2?.message || `Update ${path}`;
|
|
516
|
-
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
517
|
-
const sha = await (async () => {
|
|
518
|
-
try {
|
|
519
|
-
const d = await fetchJson(apiUrl(path));
|
|
520
|
-
return d.sha;
|
|
521
|
-
} catch {
|
|
522
|
-
return void 0;
|
|
523
|
-
}
|
|
524
|
-
})();
|
|
525
|
-
await fetch(apiUrl(path), {
|
|
526
|
-
method: "POST",
|
|
527
|
-
headers: { "Content-Type": "application/json" },
|
|
528
|
-
body: JSON.stringify({ access_token: token, message, content, sha, branch })
|
|
529
|
-
});
|
|
530
|
-
},
|
|
531
|
-
async readdir(path) {
|
|
532
|
-
const data = await fetchJson(apiUrl(path));
|
|
533
|
-
return Array.isArray(data) ? data.map((i) => i.name) : [];
|
|
534
|
-
},
|
|
535
|
-
async stat(path) {
|
|
536
|
-
try {
|
|
537
|
-
const data = await fetchJson(apiUrl(path));
|
|
538
|
-
if (Array.isArray(data)) return ghStat({ type: "dir", size: 0 });
|
|
539
|
-
return ghStat(data);
|
|
540
|
-
} catch {
|
|
541
|
-
throw new Error(`ENOENT: ${path}`);
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
async exists(path) {
|
|
545
|
-
try {
|
|
546
|
-
await fetchJson(apiUrl(path));
|
|
547
|
-
return true;
|
|
548
|
-
} catch {
|
|
549
|
-
return false;
|
|
550
|
-
}
|
|
551
|
-
},
|
|
552
|
-
async mkdir(path, options2) {
|
|
553
|
-
const dirPath = path.replace(/\/$/, "");
|
|
554
|
-
const keepPath = `${dirPath}/.gitkeep`;
|
|
555
|
-
const message = options2?.message || `Create directory ${dirPath}`;
|
|
556
|
-
await fetch(apiUrl(keepPath), {
|
|
557
|
-
method: "POST",
|
|
558
|
-
headers: { "Content-Type": "application/json" },
|
|
559
|
-
body: JSON.stringify({ access_token: token, message, content: btoa(""), branch })
|
|
560
|
-
});
|
|
561
|
-
},
|
|
562
|
-
async unlink(path) {
|
|
563
|
-
const data = await fetchJson(apiUrl(path));
|
|
564
|
-
await fetch(apiUrl(path), {
|
|
565
|
-
method: "DELETE",
|
|
566
|
-
headers: { "Content-Type": "application/json" },
|
|
567
|
-
body: JSON.stringify({ access_token: token, message: `Delete ${path}`, sha: data.sha, branch })
|
|
568
|
-
});
|
|
569
|
-
},
|
|
570
|
-
async rmdir(path) {
|
|
571
|
-
const items = await fetchJson(apiUrl(path));
|
|
572
|
-
if (Array.isArray(items)) {
|
|
573
|
-
for (const item of items) {
|
|
574
|
-
const itemPath = `${path}/${item.name}`;
|
|
575
|
-
if (item.type === "dir") await backend.rmdir(itemPath);
|
|
576
|
-
else await backend.unlink(itemPath);
|
|
577
|
-
}
|
|
377
|
+
const zenGitee = await import("zen-fs-gitee");
|
|
378
|
+
const { GiteeFS } = zenGitee;
|
|
379
|
+
const origInit = GiteeFS.prototype.init;
|
|
380
|
+
const patched = /* @__PURE__ */ new WeakSet();
|
|
381
|
+
GiteeFS.prototype.init = async function() {
|
|
382
|
+
let firstErr;
|
|
383
|
+
try {
|
|
384
|
+
return await origInit.call(this);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (!err.message?.includes("404") && !err.message?.includes("Tree not found")) {
|
|
387
|
+
throw err;
|
|
578
388
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
389
|
+
firstErr = err;
|
|
390
|
+
}
|
|
391
|
+
if (patched.has(this)) throw firstErr;
|
|
392
|
+
patched.add(this);
|
|
393
|
+
console.log(`[Gitee] init failed with branch="${this.api.branch}", resolving to SHA...`);
|
|
394
|
+
const baseUrl = this.api.baseUrl || "https://gitee.com/api/v5";
|
|
395
|
+
const auth = `access_token=${this.api.token}`;
|
|
396
|
+
const branchUrl = `${baseUrl}/repos/${this.api.owner}/${this.api.repo}/branches/${this.api.branch}?${auth}`;
|
|
397
|
+
const branchRes = await fetch(branchUrl);
|
|
398
|
+
if (!branchRes.ok) throw new Error(`Gitee: branch "${this.api.branch}" not found (${branchRes.status})`);
|
|
399
|
+
const branchData = await branchRes.json();
|
|
400
|
+
const commitSha = branchData.commit?.sha;
|
|
401
|
+
if (!commitSha) throw new Error(`Gitee: could not get commit SHA for branch "${this.api.branch}"`);
|
|
402
|
+
const commitUrl = `${baseUrl}/repos/${this.api.owner}/${this.api.repo}/git/commits/${commitSha}?${auth}`;
|
|
403
|
+
const commitRes = await fetch(commitUrl);
|
|
404
|
+
if (!commitRes.ok) throw new Error(`Gitee: commit ${commitSha} not found (${commitRes.status})`);
|
|
405
|
+
const commitData = await commitRes.json();
|
|
406
|
+
const treeSha = commitData.tree?.sha;
|
|
407
|
+
if (!treeSha) throw new Error(`Gitee: could not get tree SHA from commit ${commitSha}`);
|
|
408
|
+
const realBranch = this.api.branch;
|
|
409
|
+
this.api.branch = treeSha;
|
|
410
|
+
console.log(`[Gitee] Resolved branch="${realBranch}" \u2192 commit=${commitSha.slice(0, 8)} \u2192 tree=${treeSha.slice(0, 8)}`);
|
|
411
|
+
try {
|
|
412
|
+
return await origInit.call(this);
|
|
413
|
+
} finally {
|
|
414
|
+
this.api.branch = realBranch;
|
|
584
415
|
}
|
|
585
416
|
};
|
|
586
|
-
return
|
|
417
|
+
return wrapZenFSFileSystem({
|
|
418
|
+
backend: zenGitee.Gitee,
|
|
419
|
+
token: options.token,
|
|
420
|
+
owner: options.owner,
|
|
421
|
+
repo: options.repo,
|
|
422
|
+
branch: options.branch,
|
|
423
|
+
baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
|
|
424
|
+
});
|
|
587
425
|
});
|
|
588
426
|
registerBackend("WebDAV", async (options) => {
|
|
589
427
|
const url = options.url ?? "";
|
|
@@ -810,6 +648,10 @@ var ConfigRepo = class {
|
|
|
810
648
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
811
649
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
812
650
|
}
|
|
651
|
+
/** Full path to this node's directory on the primary backend. */
|
|
652
|
+
get nodePath() {
|
|
653
|
+
return `/nodes/${this.nodeId}`;
|
|
654
|
+
}
|
|
813
655
|
// -----------------------------------------------------------------------
|
|
814
656
|
// IConfigRepo — Load
|
|
815
657
|
// -----------------------------------------------------------------------
|
|
@@ -1028,11 +870,14 @@ var ConfigRepo = class {
|
|
|
1028
870
|
// -----------------------------------------------------------------------
|
|
1029
871
|
// Internal — Setup
|
|
1030
872
|
// -----------------------------------------------------------------------
|
|
1031
|
-
async setupSync(
|
|
873
|
+
async setupSync(backends, primaryBackendId) {
|
|
1032
874
|
console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId}`);
|
|
1033
|
-
console.log(`[ConfigRepo] setupSync: rules=`, JSON.stringify(rules, null, 2));
|
|
1034
875
|
for (const desc of backends) {
|
|
1035
876
|
if (desc.id === primaryBackendId) continue;
|
|
877
|
+
if (desc.enabled === false) {
|
|
878
|
+
console.log(`[ConfigRepo] Skipping disabled replica: ${desc.id}`);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
1036
881
|
console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
1037
882
|
try {
|
|
1038
883
|
const instance = await createBackend(desc);
|
|
@@ -1044,39 +889,23 @@ var ConfigRepo = class {
|
|
|
1044
889
|
}
|
|
1045
890
|
}
|
|
1046
891
|
console.log(`[ConfigRepo] Available replicas:`, Array.from(this.replicaBackends.keys()));
|
|
1047
|
-
for (const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
{
|
|
1065
|
-
direction: zenSyncDirection,
|
|
1066
|
-
conflictStrategy: rule.conflictStrategy,
|
|
1067
|
-
filter: {
|
|
1068
|
-
includePrefixes: [rule.prefix]
|
|
1069
|
-
}
|
|
1070
|
-
},
|
|
1071
|
-
"/"
|
|
1072
|
-
);
|
|
1073
|
-
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, prefix=${rule.prefix}, dir=${rule.direction}, replica=${replicaId}`);
|
|
1074
|
-
const conflictHandler = (event) => {
|
|
1075
|
-
this.handleConflict(event, rule);
|
|
1076
|
-
};
|
|
1077
|
-
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
1078
|
-
this.syncEngine.watch(pair.pairId);
|
|
1079
|
-
}
|
|
892
|
+
for (const [replicaId, replica] of this.replicaBackends.entries()) {
|
|
893
|
+
const pair = this.syncEngine.addPair(
|
|
894
|
+
this.fullFS,
|
|
895
|
+
replica.syncable,
|
|
896
|
+
{
|
|
897
|
+
direction: SyncDirection.OneWay,
|
|
898
|
+
conflictStrategy: "source-wins"
|
|
899
|
+
// No filter = sync everything under root
|
|
900
|
+
},
|
|
901
|
+
"/"
|
|
902
|
+
);
|
|
903
|
+
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
|
|
904
|
+
const conflictHandler = (event) => {
|
|
905
|
+
this.handleConflict(event, { prefix: "/", direction: "one-way" });
|
|
906
|
+
};
|
|
907
|
+
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
908
|
+
this.syncEngine.watch(pair.pairId);
|
|
1080
909
|
}
|
|
1081
910
|
console.log(`[ConfigRepo] setupSync complete. Sync statuses:`, this.getSyncStatuses());
|
|
1082
911
|
}
|
|
@@ -1394,7 +1223,6 @@ async function createConfigRepo(appId, options) {
|
|
|
1394
1223
|
options.onConflict
|
|
1395
1224
|
);
|
|
1396
1225
|
await repo.setupSync(
|
|
1397
|
-
syncRulesMeta.rules,
|
|
1398
1226
|
backendsMeta.backends,
|
|
1399
1227
|
options.primaryBackendId
|
|
1400
1228
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-fs-config",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
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": {
|