zen-fs-config 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +364 -38
- package/dist/index.mjs +364 -38
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -316,59 +316,385 @@ function hasBackend(type) {
|
|
|
316
316
|
function listBackends() {
|
|
317
317
|
return Array.from(registry.keys());
|
|
318
318
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
319
|
+
var inMemoryCounter = 0;
|
|
320
|
+
registerBackend("InMemory", async (options) => {
|
|
321
|
+
const zenfs = await import("@zenfs/core");
|
|
322
|
+
const { InMemory } = zenfs;
|
|
323
|
+
const maxSize = options.maxSize ?? 100 * 1024 * 1024;
|
|
324
|
+
const label = options.label ?? `zen-fs-config-${++inMemoryCounter}`;
|
|
325
|
+
await zenfs.configureSingle({ backend: InMemory, maxSize, label });
|
|
326
|
+
const pfs = zenfs.fs.promises;
|
|
327
|
+
const backend = {
|
|
328
|
+
async readFile(path, ...args) {
|
|
329
|
+
if (args.length > 0) {
|
|
330
|
+
return pfs.readFile(path, ...args);
|
|
331
|
+
}
|
|
332
|
+
return pfs.readFile(path);
|
|
324
333
|
},
|
|
325
|
-
writeFile(path, data,
|
|
326
|
-
|
|
327
|
-
return Promise.resolve();
|
|
334
|
+
async writeFile(path, data, options2) {
|
|
335
|
+
return pfs.writeFile(path, data, options2);
|
|
328
336
|
},
|
|
329
|
-
readdir(path) {
|
|
330
|
-
const entries =
|
|
331
|
-
return
|
|
337
|
+
async readdir(path) {
|
|
338
|
+
const entries = await pfs.readdir(path);
|
|
339
|
+
return entries.map((e) => typeof e === "string" ? e : e.name);
|
|
332
340
|
},
|
|
333
|
-
stat(path, ...args) {
|
|
334
|
-
return
|
|
341
|
+
async stat(path, ...args) {
|
|
342
|
+
return pfs.stat(path, ...args);
|
|
335
343
|
},
|
|
336
|
-
exists(path) {
|
|
344
|
+
async exists(path) {
|
|
337
345
|
try {
|
|
338
|
-
|
|
339
|
-
return
|
|
346
|
+
await pfs.stat(path);
|
|
347
|
+
return true;
|
|
340
348
|
} catch {
|
|
341
|
-
return
|
|
349
|
+
return false;
|
|
342
350
|
}
|
|
343
351
|
},
|
|
344
|
-
mkdir(path,
|
|
345
|
-
|
|
346
|
-
|
|
352
|
+
async mkdir(path, options2) {
|
|
353
|
+
return pfs.mkdir(path, options2);
|
|
354
|
+
},
|
|
355
|
+
async unlink(path) {
|
|
356
|
+
return pfs.unlink(path);
|
|
357
|
+
},
|
|
358
|
+
async rmdir(path) {
|
|
359
|
+
return pfs.rmdir(path);
|
|
360
|
+
},
|
|
361
|
+
async rename(oldPath, newPath) {
|
|
362
|
+
return pfs.rename(oldPath, newPath);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
return backend;
|
|
366
|
+
});
|
|
367
|
+
registerBackend("GitHub", async (options) => {
|
|
368
|
+
const token = options.token ?? "";
|
|
369
|
+
const owner = options.owner ?? "";
|
|
370
|
+
const repo = options.repo ?? "";
|
|
371
|
+
const branch = options.branch ?? "main";
|
|
372
|
+
const baseUrl = options.baseUrl ?? "https://api.github.com";
|
|
373
|
+
if (!owner || !repo) throw new Error('GitHub backend requires "owner" and "repo" options');
|
|
374
|
+
const headers = {
|
|
375
|
+
"Accept": "application/vnd.github.v3+json",
|
|
376
|
+
"User-Agent": "zen-fs-config"
|
|
377
|
+
};
|
|
378
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
379
|
+
const apiUrl = (path) => {
|
|
380
|
+
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
381
|
+
return `${baseUrl}/repos/${owner}/${repo}/contents/${p}?ref=${branch}`;
|
|
382
|
+
};
|
|
383
|
+
const treeUrl = () => `${baseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
384
|
+
const ghStat = (item) => ({
|
|
385
|
+
isFile: () => item.type === "file",
|
|
386
|
+
isDirectory: () => item.type === "dir",
|
|
387
|
+
size: item.size ?? 0
|
|
388
|
+
});
|
|
389
|
+
const fetchJson = async (url) => {
|
|
390
|
+
const res = await fetch(url, { headers });
|
|
391
|
+
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`);
|
|
392
|
+
return res.json();
|
|
393
|
+
};
|
|
394
|
+
const backend = {
|
|
395
|
+
async readFile(path, ...args) {
|
|
396
|
+
const data = await fetchJson(apiUrl(path));
|
|
397
|
+
if (data.encoding === "base64") {
|
|
398
|
+
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
399
|
+
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
400
|
+
return raw;
|
|
401
|
+
}
|
|
402
|
+
return data;
|
|
403
|
+
},
|
|
404
|
+
async writeFile(path, data, options2) {
|
|
405
|
+
const message = options2?.message || `Update ${path}`;
|
|
406
|
+
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
407
|
+
const sha = await (async () => {
|
|
408
|
+
try {
|
|
409
|
+
const d = await fetchJson(apiUrl(path));
|
|
410
|
+
return d.sha;
|
|
411
|
+
} catch {
|
|
412
|
+
return void 0;
|
|
413
|
+
}
|
|
414
|
+
})();
|
|
415
|
+
await fetch(apiUrl(path), {
|
|
416
|
+
method: "PUT",
|
|
417
|
+
headers,
|
|
418
|
+
body: JSON.stringify({ message, content, sha, branch })
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
async readdir(path) {
|
|
422
|
+
const data = await fetchJson(apiUrl(path));
|
|
423
|
+
return data.map((item) => item.name);
|
|
347
424
|
},
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
425
|
+
async stat(path, ...args) {
|
|
426
|
+
try {
|
|
427
|
+
const data = await fetchJson(apiUrl(path));
|
|
428
|
+
if (Array.isArray(data)) {
|
|
429
|
+
return { isFile: () => false, isDirectory: () => true, size: 0 };
|
|
430
|
+
}
|
|
431
|
+
return ghStat(data);
|
|
432
|
+
} catch {
|
|
433
|
+
throw new Error(`ENOENT: ${path}`);
|
|
434
|
+
}
|
|
351
435
|
},
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
436
|
+
async exists(path) {
|
|
437
|
+
try {
|
|
438
|
+
await fetchJson(apiUrl(path));
|
|
439
|
+
return true;
|
|
440
|
+
} catch {
|
|
441
|
+
return false;
|
|
355
442
|
}
|
|
356
|
-
return Promise.resolve();
|
|
357
443
|
},
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
444
|
+
async mkdir(path, options2) {
|
|
445
|
+
const dirPath = path.replace(/\/$/, "");
|
|
446
|
+
const keepPath = `${dirPath}/.gitkeep`;
|
|
447
|
+
const message = options2?.message || `Create directory ${dirPath}`;
|
|
448
|
+
const content = btoa("");
|
|
449
|
+
await fetch(apiUrl(keepPath), {
|
|
450
|
+
method: "PUT",
|
|
451
|
+
headers,
|
|
452
|
+
body: JSON.stringify({ message, content, branch })
|
|
453
|
+
});
|
|
454
|
+
},
|
|
455
|
+
async unlink(path) {
|
|
456
|
+
const data = await fetchJson(apiUrl(path));
|
|
457
|
+
await fetch(apiUrl(path), {
|
|
458
|
+
method: "DELETE",
|
|
459
|
+
headers,
|
|
460
|
+
body: JSON.stringify({ message: `Delete ${path}`, sha: data.sha, branch })
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
async rmdir(path) {
|
|
464
|
+
const items = await fetchJson(apiUrl(path));
|
|
465
|
+
if (Array.isArray(items)) {
|
|
466
|
+
for (const item of items) {
|
|
467
|
+
const itemPath = `${path}/${item.name}`;
|
|
468
|
+
if (item.type === "dir") {
|
|
469
|
+
await backend.rmdir(itemPath);
|
|
470
|
+
} else {
|
|
471
|
+
await backend.unlink(itemPath);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
361
474
|
}
|
|
362
|
-
|
|
475
|
+
},
|
|
476
|
+
async rename(oldPath, newPath) {
|
|
477
|
+
const content = await backend.readFile(oldPath);
|
|
478
|
+
await backend.writeFile(newPath, content);
|
|
479
|
+
await backend.unlink(oldPath);
|
|
363
480
|
}
|
|
364
481
|
};
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
482
|
+
return backend;
|
|
483
|
+
});
|
|
484
|
+
registerBackend("Gitee", async (options) => {
|
|
485
|
+
const token = options.token ?? "";
|
|
486
|
+
const owner = options.owner ?? "";
|
|
487
|
+
const repo = options.repo ?? "";
|
|
488
|
+
const branch = options.branch ?? "master";
|
|
489
|
+
const baseUrl = options.baseUrl ?? "https://gitee.com/api/v5";
|
|
490
|
+
if (!owner || !repo) throw new Error('Gitee backend requires "owner" and "repo" options');
|
|
491
|
+
const fetchJson = async (url) => {
|
|
492
|
+
const res = await fetch(url);
|
|
493
|
+
if (!res.ok) throw new Error(`Gitee API ${res.status}: ${url}`);
|
|
494
|
+
return res.json();
|
|
495
|
+
};
|
|
496
|
+
const apiUrl = (path) => {
|
|
497
|
+
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
498
|
+
const params = new URLSearchParams({ access_token: token, ref: branch, path: p });
|
|
499
|
+
return `${baseUrl}/repos/${owner}/${repo}/contents?${params}`;
|
|
500
|
+
};
|
|
501
|
+
const ghStat = (item) => ({
|
|
502
|
+
isFile: () => item.type === "file",
|
|
503
|
+
isDirectory: () => item.type === "dir",
|
|
504
|
+
size: item.size ?? 0
|
|
505
|
+
});
|
|
506
|
+
const backend = {
|
|
507
|
+
async readFile(path, ...args) {
|
|
508
|
+
const data = await fetchJson(apiUrl(path));
|
|
509
|
+
if (data.content) {
|
|
510
|
+
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
511
|
+
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
512
|
+
return raw;
|
|
513
|
+
}
|
|
514
|
+
return data;
|
|
515
|
+
},
|
|
516
|
+
async writeFile(path, data, options2) {
|
|
517
|
+
const message = options2?.message || `Update ${path}`;
|
|
518
|
+
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
519
|
+
const sha = await (async () => {
|
|
520
|
+
try {
|
|
521
|
+
const d = await fetchJson(apiUrl(path));
|
|
522
|
+
return d.sha;
|
|
523
|
+
} catch {
|
|
524
|
+
return void 0;
|
|
525
|
+
}
|
|
526
|
+
})();
|
|
527
|
+
await fetch(apiUrl(path), {
|
|
528
|
+
method: "POST",
|
|
529
|
+
headers: { "Content-Type": "application/json" },
|
|
530
|
+
body: JSON.stringify({ access_token: token, message, content, sha, branch })
|
|
531
|
+
});
|
|
532
|
+
},
|
|
533
|
+
async readdir(path) {
|
|
534
|
+
const data = await fetchJson(apiUrl(path));
|
|
535
|
+
return Array.isArray(data) ? data.map((i) => i.name) : [];
|
|
536
|
+
},
|
|
537
|
+
async stat(path) {
|
|
538
|
+
try {
|
|
539
|
+
const data = await fetchJson(apiUrl(path));
|
|
540
|
+
if (Array.isArray(data)) return ghStat({ type: "dir", size: 0 });
|
|
541
|
+
return ghStat(data);
|
|
542
|
+
} catch {
|
|
543
|
+
throw new Error(`ENOENT: ${path}`);
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
async exists(path) {
|
|
547
|
+
try {
|
|
548
|
+
await fetchJson(apiUrl(path));
|
|
549
|
+
return true;
|
|
550
|
+
} catch {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
async mkdir(path, options2) {
|
|
555
|
+
const dirPath = path.replace(/\/$/, "");
|
|
556
|
+
const keepPath = `${dirPath}/.gitkeep`;
|
|
557
|
+
const message = options2?.message || `Create directory ${dirPath}`;
|
|
558
|
+
await fetch(apiUrl(keepPath), {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: { "Content-Type": "application/json" },
|
|
561
|
+
body: JSON.stringify({ access_token: token, message, content: btoa(""), branch })
|
|
562
|
+
});
|
|
563
|
+
},
|
|
564
|
+
async unlink(path) {
|
|
565
|
+
const data = await fetchJson(apiUrl(path));
|
|
566
|
+
await fetch(apiUrl(path), {
|
|
567
|
+
method: "DELETE",
|
|
568
|
+
headers: { "Content-Type": "application/json" },
|
|
569
|
+
body: JSON.stringify({ access_token: token, message: `Delete ${path}`, sha: data.sha, branch })
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
async rmdir(path) {
|
|
573
|
+
const items = await fetchJson(apiUrl(path));
|
|
574
|
+
if (Array.isArray(items)) {
|
|
575
|
+
for (const item of items) {
|
|
576
|
+
const itemPath = `${path}/${item.name}`;
|
|
577
|
+
if (item.type === "dir") await backend.rmdir(itemPath);
|
|
578
|
+
else await backend.unlink(itemPath);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
async rename(oldPath, newPath) {
|
|
583
|
+
const content = await backend.readFile(oldPath);
|
|
584
|
+
await backend.writeFile(newPath, content);
|
|
585
|
+
await backend.unlink(oldPath);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
return backend;
|
|
589
|
+
});
|
|
590
|
+
registerBackend("WebDAV", async (options) => {
|
|
591
|
+
const url = options.url ?? "";
|
|
592
|
+
const username = options.username ?? "";
|
|
593
|
+
const password = options.password ?? "";
|
|
594
|
+
const rootPath = options.rootPath ?? "/";
|
|
595
|
+
if (!url) throw new Error('WebDAV backend requires "url" option');
|
|
596
|
+
const authHeader = username ? `Basic ${btoa(`${username}:${password}`)}` : "";
|
|
597
|
+
const davUrl = (path) => {
|
|
598
|
+
const cleanRoot = rootPath.replace(/\/$/, "");
|
|
599
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
600
|
+
return `${url.replace(/\/$/, "")}${cleanRoot}${cleanPath}`;
|
|
601
|
+
};
|
|
602
|
+
const davFetch = async (path, method, body) => {
|
|
603
|
+
const headers = {};
|
|
604
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
605
|
+
if (body) headers["Content-Type"] = "application/xml";
|
|
606
|
+
const res = await fetch(davUrl(path), { method, headers, body });
|
|
607
|
+
if (!res.ok && res.status !== 404) throw new Error(`WebDAV ${res.status} ${method} ${davUrl(path)}`);
|
|
608
|
+
return res;
|
|
609
|
+
};
|
|
610
|
+
const parseMultiStatus = async (res) => {
|
|
611
|
+
const text = await res.text();
|
|
612
|
+
const results = [];
|
|
613
|
+
const responses = text.match(/<D:response[^>]*>[\s\S]*?<\/D:response>/gi) || [];
|
|
614
|
+
for (const resp of responses) {
|
|
615
|
+
const href = (resp.match(/<D:href>([^<]+)<\/D:href>/i) || [])[1] || "";
|
|
616
|
+
const isDir = /<D:collection\s*\/>/i.test(resp) || /<D:resourcetype>.*<D:collection/.test(resp);
|
|
617
|
+
const sizeMatch = resp.match(/<D:getcontentlength>([^<]+)<\/D:getcontentlength>/i);
|
|
618
|
+
const size = sizeMatch ? parseInt(sizeMatch[1]) : 0;
|
|
619
|
+
const decoded = decodeURIComponent(href);
|
|
620
|
+
results.push({ path: decoded, isDir, size });
|
|
621
|
+
}
|
|
622
|
+
return results;
|
|
623
|
+
};
|
|
624
|
+
const exists = async (path) => {
|
|
625
|
+
const res = await davFetch(path, "PROPFIND");
|
|
626
|
+
return res.ok;
|
|
627
|
+
};
|
|
628
|
+
const backend = {
|
|
629
|
+
async readFile(path, ...args) {
|
|
630
|
+
const res = await davFetch(path, "GET");
|
|
631
|
+
if (!res.ok) throw new Error(`ENOENT: ${path}`);
|
|
632
|
+
if (args[0] === "utf-8") return res.text();
|
|
633
|
+
const buf = await res.arrayBuffer();
|
|
634
|
+
return new Uint8Array(buf);
|
|
635
|
+
},
|
|
636
|
+
async writeFile(path, data, _options) {
|
|
637
|
+
const headers = { "Content-Type": "application/octet-stream" };
|
|
638
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
639
|
+
await fetch(davUrl(path), {
|
|
640
|
+
method: "PUT",
|
|
641
|
+
headers,
|
|
642
|
+
body: data instanceof ArrayBuffer ? data : data instanceof Uint8Array ? new Uint8Array(data).buffer : new TextEncoder().encode(data)
|
|
643
|
+
});
|
|
644
|
+
},
|
|
645
|
+
async readdir(path) {
|
|
646
|
+
const headers = { Depth: "1" };
|
|
647
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
648
|
+
const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
|
|
649
|
+
if (!res.ok) throw new Error(`WebDAV PROPFIND failed: ${res.status}`);
|
|
650
|
+
const items = await parseMultiStatus(res);
|
|
651
|
+
const prefix = davUrl(path);
|
|
652
|
+
return items.filter((i) => i.path !== prefix && i.path !== `${prefix}/`).map((i) => i.path.split("/").filter(Boolean).pop() || "");
|
|
653
|
+
},
|
|
654
|
+
async stat(path) {
|
|
655
|
+
const headers = { Depth: "0" };
|
|
656
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
657
|
+
const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
|
|
658
|
+
if (!res.ok) throw new Error(`ENOENT: ${path}`);
|
|
659
|
+
const items = await parseMultiStatus(res);
|
|
660
|
+
const item = items[0];
|
|
661
|
+
return { isFile: () => !item.isDir, isDirectory: () => item.isDir, size: item.size };
|
|
662
|
+
},
|
|
663
|
+
async exists(path) {
|
|
664
|
+
return exists(path);
|
|
665
|
+
},
|
|
666
|
+
async mkdir(path) {
|
|
667
|
+
const headers = {};
|
|
668
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
669
|
+
const res = await fetch(davUrl(path), { method: "MKCOL", headers });
|
|
670
|
+
if (!res.ok && res.status !== 405) throw new Error(`WebDAV MKCOL failed: ${res.status}`);
|
|
671
|
+
},
|
|
672
|
+
async unlink(path) {
|
|
673
|
+
await davFetch(path, "DELETE");
|
|
674
|
+
},
|
|
675
|
+
async rmdir(path) {
|
|
676
|
+
const items = await (async () => {
|
|
677
|
+
const headers = { Depth: "1" };
|
|
678
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
679
|
+
const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
|
|
680
|
+
if (!res.ok) return [];
|
|
681
|
+
const parsed = await parseMultiStatus(res);
|
|
682
|
+
const prefix = davUrl(path);
|
|
683
|
+
return parsed.filter((i) => i.path !== prefix && i.path !== `${prefix}/`);
|
|
684
|
+
})();
|
|
685
|
+
for (const item of items) {
|
|
686
|
+
if (item.isDir) await backend.rmdir(item.path);
|
|
687
|
+
else await backend.unlink(item.path);
|
|
688
|
+
}
|
|
689
|
+
await davFetch(path, "DELETE");
|
|
690
|
+
},
|
|
691
|
+
async rename(oldPath, newPath) {
|
|
692
|
+
const headers = { Destination: davUrl(newPath) };
|
|
693
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
694
|
+
await fetch(davUrl(oldPath), { method: "MOVE", headers });
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
return backend;
|
|
372
698
|
});
|
|
373
699
|
|
|
374
700
|
// src/version.ts
|
package/dist/index.mjs
CHANGED
|
@@ -269,59 +269,385 @@ function hasBackend(type) {
|
|
|
269
269
|
function listBackends() {
|
|
270
270
|
return Array.from(registry.keys());
|
|
271
271
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
var inMemoryCounter = 0;
|
|
273
|
+
registerBackend("InMemory", async (options) => {
|
|
274
|
+
const zenfs = await import("@zenfs/core");
|
|
275
|
+
const { InMemory } = zenfs;
|
|
276
|
+
const maxSize = options.maxSize ?? 100 * 1024 * 1024;
|
|
277
|
+
const label = options.label ?? `zen-fs-config-${++inMemoryCounter}`;
|
|
278
|
+
await zenfs.configureSingle({ backend: InMemory, maxSize, label });
|
|
279
|
+
const pfs = zenfs.fs.promises;
|
|
280
|
+
const backend = {
|
|
281
|
+
async readFile(path, ...args) {
|
|
282
|
+
if (args.length > 0) {
|
|
283
|
+
return pfs.readFile(path, ...args);
|
|
284
|
+
}
|
|
285
|
+
return pfs.readFile(path);
|
|
277
286
|
},
|
|
278
|
-
writeFile(path, data,
|
|
279
|
-
|
|
280
|
-
return Promise.resolve();
|
|
287
|
+
async writeFile(path, data, options2) {
|
|
288
|
+
return pfs.writeFile(path, data, options2);
|
|
281
289
|
},
|
|
282
|
-
readdir(path) {
|
|
283
|
-
const entries =
|
|
284
|
-
return
|
|
290
|
+
async readdir(path) {
|
|
291
|
+
const entries = await pfs.readdir(path);
|
|
292
|
+
return entries.map((e) => typeof e === "string" ? e : e.name);
|
|
285
293
|
},
|
|
286
|
-
stat(path, ...args) {
|
|
287
|
-
return
|
|
294
|
+
async stat(path, ...args) {
|
|
295
|
+
return pfs.stat(path, ...args);
|
|
288
296
|
},
|
|
289
|
-
exists(path) {
|
|
297
|
+
async exists(path) {
|
|
290
298
|
try {
|
|
291
|
-
|
|
292
|
-
return
|
|
299
|
+
await pfs.stat(path);
|
|
300
|
+
return true;
|
|
293
301
|
} catch {
|
|
294
|
-
return
|
|
302
|
+
return false;
|
|
295
303
|
}
|
|
296
304
|
},
|
|
297
|
-
mkdir(path,
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
async mkdir(path, options2) {
|
|
306
|
+
return pfs.mkdir(path, options2);
|
|
307
|
+
},
|
|
308
|
+
async unlink(path) {
|
|
309
|
+
return pfs.unlink(path);
|
|
310
|
+
},
|
|
311
|
+
async rmdir(path) {
|
|
312
|
+
return pfs.rmdir(path);
|
|
313
|
+
},
|
|
314
|
+
async rename(oldPath, newPath) {
|
|
315
|
+
return pfs.rename(oldPath, newPath);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
return backend;
|
|
319
|
+
});
|
|
320
|
+
registerBackend("GitHub", async (options) => {
|
|
321
|
+
const token = options.token ?? "";
|
|
322
|
+
const owner = options.owner ?? "";
|
|
323
|
+
const repo = options.repo ?? "";
|
|
324
|
+
const branch = options.branch ?? "main";
|
|
325
|
+
const baseUrl = options.baseUrl ?? "https://api.github.com";
|
|
326
|
+
if (!owner || !repo) throw new Error('GitHub backend requires "owner" and "repo" options');
|
|
327
|
+
const headers = {
|
|
328
|
+
"Accept": "application/vnd.github.v3+json",
|
|
329
|
+
"User-Agent": "zen-fs-config"
|
|
330
|
+
};
|
|
331
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
332
|
+
const apiUrl = (path) => {
|
|
333
|
+
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
334
|
+
return `${baseUrl}/repos/${owner}/${repo}/contents/${p}?ref=${branch}`;
|
|
335
|
+
};
|
|
336
|
+
const treeUrl = () => `${baseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
337
|
+
const ghStat = (item) => ({
|
|
338
|
+
isFile: () => item.type === "file",
|
|
339
|
+
isDirectory: () => item.type === "dir",
|
|
340
|
+
size: item.size ?? 0
|
|
341
|
+
});
|
|
342
|
+
const fetchJson = async (url) => {
|
|
343
|
+
const res = await fetch(url, { headers });
|
|
344
|
+
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`);
|
|
345
|
+
return res.json();
|
|
346
|
+
};
|
|
347
|
+
const backend = {
|
|
348
|
+
async readFile(path, ...args) {
|
|
349
|
+
const data = await fetchJson(apiUrl(path));
|
|
350
|
+
if (data.encoding === "base64") {
|
|
351
|
+
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
352
|
+
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
353
|
+
return raw;
|
|
354
|
+
}
|
|
355
|
+
return data;
|
|
356
|
+
},
|
|
357
|
+
async writeFile(path, data, options2) {
|
|
358
|
+
const message = options2?.message || `Update ${path}`;
|
|
359
|
+
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
360
|
+
const sha = await (async () => {
|
|
361
|
+
try {
|
|
362
|
+
const d = await fetchJson(apiUrl(path));
|
|
363
|
+
return d.sha;
|
|
364
|
+
} catch {
|
|
365
|
+
return void 0;
|
|
366
|
+
}
|
|
367
|
+
})();
|
|
368
|
+
await fetch(apiUrl(path), {
|
|
369
|
+
method: "PUT",
|
|
370
|
+
headers,
|
|
371
|
+
body: JSON.stringify({ message, content, sha, branch })
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
async readdir(path) {
|
|
375
|
+
const data = await fetchJson(apiUrl(path));
|
|
376
|
+
return data.map((item) => item.name);
|
|
300
377
|
},
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
378
|
+
async stat(path, ...args) {
|
|
379
|
+
try {
|
|
380
|
+
const data = await fetchJson(apiUrl(path));
|
|
381
|
+
if (Array.isArray(data)) {
|
|
382
|
+
return { isFile: () => false, isDirectory: () => true, size: 0 };
|
|
383
|
+
}
|
|
384
|
+
return ghStat(data);
|
|
385
|
+
} catch {
|
|
386
|
+
throw new Error(`ENOENT: ${path}`);
|
|
387
|
+
}
|
|
304
388
|
},
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
389
|
+
async exists(path) {
|
|
390
|
+
try {
|
|
391
|
+
await fetchJson(apiUrl(path));
|
|
392
|
+
return true;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
308
395
|
}
|
|
309
|
-
return Promise.resolve();
|
|
310
396
|
},
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
397
|
+
async mkdir(path, options2) {
|
|
398
|
+
const dirPath = path.replace(/\/$/, "");
|
|
399
|
+
const keepPath = `${dirPath}/.gitkeep`;
|
|
400
|
+
const message = options2?.message || `Create directory ${dirPath}`;
|
|
401
|
+
const content = btoa("");
|
|
402
|
+
await fetch(apiUrl(keepPath), {
|
|
403
|
+
method: "PUT",
|
|
404
|
+
headers,
|
|
405
|
+
body: JSON.stringify({ message, content, branch })
|
|
406
|
+
});
|
|
407
|
+
},
|
|
408
|
+
async unlink(path) {
|
|
409
|
+
const data = await fetchJson(apiUrl(path));
|
|
410
|
+
await fetch(apiUrl(path), {
|
|
411
|
+
method: "DELETE",
|
|
412
|
+
headers,
|
|
413
|
+
body: JSON.stringify({ message: `Delete ${path}`, sha: data.sha, branch })
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
async rmdir(path) {
|
|
417
|
+
const items = await fetchJson(apiUrl(path));
|
|
418
|
+
if (Array.isArray(items)) {
|
|
419
|
+
for (const item of items) {
|
|
420
|
+
const itemPath = `${path}/${item.name}`;
|
|
421
|
+
if (item.type === "dir") {
|
|
422
|
+
await backend.rmdir(itemPath);
|
|
423
|
+
} else {
|
|
424
|
+
await backend.unlink(itemPath);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
314
427
|
}
|
|
315
|
-
|
|
428
|
+
},
|
|
429
|
+
async rename(oldPath, newPath) {
|
|
430
|
+
const content = await backend.readFile(oldPath);
|
|
431
|
+
await backend.writeFile(newPath, content);
|
|
432
|
+
await backend.unlink(oldPath);
|
|
316
433
|
}
|
|
317
434
|
};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
|
|
435
|
+
return backend;
|
|
436
|
+
});
|
|
437
|
+
registerBackend("Gitee", async (options) => {
|
|
438
|
+
const token = options.token ?? "";
|
|
439
|
+
const owner = options.owner ?? "";
|
|
440
|
+
const repo = options.repo ?? "";
|
|
441
|
+
const branch = options.branch ?? "master";
|
|
442
|
+
const baseUrl = options.baseUrl ?? "https://gitee.com/api/v5";
|
|
443
|
+
if (!owner || !repo) throw new Error('Gitee backend requires "owner" and "repo" options');
|
|
444
|
+
const fetchJson = async (url) => {
|
|
445
|
+
const res = await fetch(url);
|
|
446
|
+
if (!res.ok) throw new Error(`Gitee API ${res.status}: ${url}`);
|
|
447
|
+
return res.json();
|
|
448
|
+
};
|
|
449
|
+
const apiUrl = (path) => {
|
|
450
|
+
const p = path.startsWith("/") ? path.slice(1) : path;
|
|
451
|
+
const params = new URLSearchParams({ access_token: token, ref: branch, path: p });
|
|
452
|
+
return `${baseUrl}/repos/${owner}/${repo}/contents?${params}`;
|
|
453
|
+
};
|
|
454
|
+
const ghStat = (item) => ({
|
|
455
|
+
isFile: () => item.type === "file",
|
|
456
|
+
isDirectory: () => item.type === "dir",
|
|
457
|
+
size: item.size ?? 0
|
|
458
|
+
});
|
|
459
|
+
const backend = {
|
|
460
|
+
async readFile(path, ...args) {
|
|
461
|
+
const data = await fetchJson(apiUrl(path));
|
|
462
|
+
if (data.content) {
|
|
463
|
+
const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
|
|
464
|
+
if (args[0] === "utf-8") return new TextDecoder().decode(raw);
|
|
465
|
+
return raw;
|
|
466
|
+
}
|
|
467
|
+
return data;
|
|
468
|
+
},
|
|
469
|
+
async writeFile(path, data, options2) {
|
|
470
|
+
const message = options2?.message || `Update ${path}`;
|
|
471
|
+
const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
|
|
472
|
+
const sha = await (async () => {
|
|
473
|
+
try {
|
|
474
|
+
const d = await fetchJson(apiUrl(path));
|
|
475
|
+
return d.sha;
|
|
476
|
+
} catch {
|
|
477
|
+
return void 0;
|
|
478
|
+
}
|
|
479
|
+
})();
|
|
480
|
+
await fetch(apiUrl(path), {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: { "Content-Type": "application/json" },
|
|
483
|
+
body: JSON.stringify({ access_token: token, message, content, sha, branch })
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
async readdir(path) {
|
|
487
|
+
const data = await fetchJson(apiUrl(path));
|
|
488
|
+
return Array.isArray(data) ? data.map((i) => i.name) : [];
|
|
489
|
+
},
|
|
490
|
+
async stat(path) {
|
|
491
|
+
try {
|
|
492
|
+
const data = await fetchJson(apiUrl(path));
|
|
493
|
+
if (Array.isArray(data)) return ghStat({ type: "dir", size: 0 });
|
|
494
|
+
return ghStat(data);
|
|
495
|
+
} catch {
|
|
496
|
+
throw new Error(`ENOENT: ${path}`);
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
async exists(path) {
|
|
500
|
+
try {
|
|
501
|
+
await fetchJson(apiUrl(path));
|
|
502
|
+
return true;
|
|
503
|
+
} catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
async mkdir(path, options2) {
|
|
508
|
+
const dirPath = path.replace(/\/$/, "");
|
|
509
|
+
const keepPath = `${dirPath}/.gitkeep`;
|
|
510
|
+
const message = options2?.message || `Create directory ${dirPath}`;
|
|
511
|
+
await fetch(apiUrl(keepPath), {
|
|
512
|
+
method: "POST",
|
|
513
|
+
headers: { "Content-Type": "application/json" },
|
|
514
|
+
body: JSON.stringify({ access_token: token, message, content: btoa(""), branch })
|
|
515
|
+
});
|
|
516
|
+
},
|
|
517
|
+
async unlink(path) {
|
|
518
|
+
const data = await fetchJson(apiUrl(path));
|
|
519
|
+
await fetch(apiUrl(path), {
|
|
520
|
+
method: "DELETE",
|
|
521
|
+
headers: { "Content-Type": "application/json" },
|
|
522
|
+
body: JSON.stringify({ access_token: token, message: `Delete ${path}`, sha: data.sha, branch })
|
|
523
|
+
});
|
|
524
|
+
},
|
|
525
|
+
async rmdir(path) {
|
|
526
|
+
const items = await fetchJson(apiUrl(path));
|
|
527
|
+
if (Array.isArray(items)) {
|
|
528
|
+
for (const item of items) {
|
|
529
|
+
const itemPath = `${path}/${item.name}`;
|
|
530
|
+
if (item.type === "dir") await backend.rmdir(itemPath);
|
|
531
|
+
else await backend.unlink(itemPath);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
async rename(oldPath, newPath) {
|
|
536
|
+
const content = await backend.readFile(oldPath);
|
|
537
|
+
await backend.writeFile(newPath, content);
|
|
538
|
+
await backend.unlink(oldPath);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
return backend;
|
|
542
|
+
});
|
|
543
|
+
registerBackend("WebDAV", async (options) => {
|
|
544
|
+
const url = options.url ?? "";
|
|
545
|
+
const username = options.username ?? "";
|
|
546
|
+
const password = options.password ?? "";
|
|
547
|
+
const rootPath = options.rootPath ?? "/";
|
|
548
|
+
if (!url) throw new Error('WebDAV backend requires "url" option');
|
|
549
|
+
const authHeader = username ? `Basic ${btoa(`${username}:${password}`)}` : "";
|
|
550
|
+
const davUrl = (path) => {
|
|
551
|
+
const cleanRoot = rootPath.replace(/\/$/, "");
|
|
552
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
553
|
+
return `${url.replace(/\/$/, "")}${cleanRoot}${cleanPath}`;
|
|
554
|
+
};
|
|
555
|
+
const davFetch = async (path, method, body) => {
|
|
556
|
+
const headers = {};
|
|
557
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
558
|
+
if (body) headers["Content-Type"] = "application/xml";
|
|
559
|
+
const res = await fetch(davUrl(path), { method, headers, body });
|
|
560
|
+
if (!res.ok && res.status !== 404) throw new Error(`WebDAV ${res.status} ${method} ${davUrl(path)}`);
|
|
561
|
+
return res;
|
|
562
|
+
};
|
|
563
|
+
const parseMultiStatus = async (res) => {
|
|
564
|
+
const text = await res.text();
|
|
565
|
+
const results = [];
|
|
566
|
+
const responses = text.match(/<D:response[^>]*>[\s\S]*?<\/D:response>/gi) || [];
|
|
567
|
+
for (const resp of responses) {
|
|
568
|
+
const href = (resp.match(/<D:href>([^<]+)<\/D:href>/i) || [])[1] || "";
|
|
569
|
+
const isDir = /<D:collection\s*\/>/i.test(resp) || /<D:resourcetype>.*<D:collection/.test(resp);
|
|
570
|
+
const sizeMatch = resp.match(/<D:getcontentlength>([^<]+)<\/D:getcontentlength>/i);
|
|
571
|
+
const size = sizeMatch ? parseInt(sizeMatch[1]) : 0;
|
|
572
|
+
const decoded = decodeURIComponent(href);
|
|
573
|
+
results.push({ path: decoded, isDir, size });
|
|
574
|
+
}
|
|
575
|
+
return results;
|
|
576
|
+
};
|
|
577
|
+
const exists = async (path) => {
|
|
578
|
+
const res = await davFetch(path, "PROPFIND");
|
|
579
|
+
return res.ok;
|
|
580
|
+
};
|
|
581
|
+
const backend = {
|
|
582
|
+
async readFile(path, ...args) {
|
|
583
|
+
const res = await davFetch(path, "GET");
|
|
584
|
+
if (!res.ok) throw new Error(`ENOENT: ${path}`);
|
|
585
|
+
if (args[0] === "utf-8") return res.text();
|
|
586
|
+
const buf = await res.arrayBuffer();
|
|
587
|
+
return new Uint8Array(buf);
|
|
588
|
+
},
|
|
589
|
+
async writeFile(path, data, _options) {
|
|
590
|
+
const headers = { "Content-Type": "application/octet-stream" };
|
|
591
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
592
|
+
await fetch(davUrl(path), {
|
|
593
|
+
method: "PUT",
|
|
594
|
+
headers,
|
|
595
|
+
body: data instanceof ArrayBuffer ? data : data instanceof Uint8Array ? new Uint8Array(data).buffer : new TextEncoder().encode(data)
|
|
596
|
+
});
|
|
597
|
+
},
|
|
598
|
+
async readdir(path) {
|
|
599
|
+
const headers = { Depth: "1" };
|
|
600
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
601
|
+
const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
|
|
602
|
+
if (!res.ok) throw new Error(`WebDAV PROPFIND failed: ${res.status}`);
|
|
603
|
+
const items = await parseMultiStatus(res);
|
|
604
|
+
const prefix = davUrl(path);
|
|
605
|
+
return items.filter((i) => i.path !== prefix && i.path !== `${prefix}/`).map((i) => i.path.split("/").filter(Boolean).pop() || "");
|
|
606
|
+
},
|
|
607
|
+
async stat(path) {
|
|
608
|
+
const headers = { Depth: "0" };
|
|
609
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
610
|
+
const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
|
|
611
|
+
if (!res.ok) throw new Error(`ENOENT: ${path}`);
|
|
612
|
+
const items = await parseMultiStatus(res);
|
|
613
|
+
const item = items[0];
|
|
614
|
+
return { isFile: () => !item.isDir, isDirectory: () => item.isDir, size: item.size };
|
|
615
|
+
},
|
|
616
|
+
async exists(path) {
|
|
617
|
+
return exists(path);
|
|
618
|
+
},
|
|
619
|
+
async mkdir(path) {
|
|
620
|
+
const headers = {};
|
|
621
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
622
|
+
const res = await fetch(davUrl(path), { method: "MKCOL", headers });
|
|
623
|
+
if (!res.ok && res.status !== 405) throw new Error(`WebDAV MKCOL failed: ${res.status}`);
|
|
624
|
+
},
|
|
625
|
+
async unlink(path) {
|
|
626
|
+
await davFetch(path, "DELETE");
|
|
627
|
+
},
|
|
628
|
+
async rmdir(path) {
|
|
629
|
+
const items = await (async () => {
|
|
630
|
+
const headers = { Depth: "1" };
|
|
631
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
632
|
+
const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
|
|
633
|
+
if (!res.ok) return [];
|
|
634
|
+
const parsed = await parseMultiStatus(res);
|
|
635
|
+
const prefix = davUrl(path);
|
|
636
|
+
return parsed.filter((i) => i.path !== prefix && i.path !== `${prefix}/`);
|
|
637
|
+
})();
|
|
638
|
+
for (const item of items) {
|
|
639
|
+
if (item.isDir) await backend.rmdir(item.path);
|
|
640
|
+
else await backend.unlink(item.path);
|
|
641
|
+
}
|
|
642
|
+
await davFetch(path, "DELETE");
|
|
643
|
+
},
|
|
644
|
+
async rename(oldPath, newPath) {
|
|
645
|
+
const headers = { Destination: davUrl(newPath) };
|
|
646
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
647
|
+
await fetch(davUrl(oldPath), { method: "MOVE", headers });
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
return backend;
|
|
325
651
|
});
|
|
326
652
|
|
|
327
653
|
// src/version.ts
|