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.
Files changed (3) hide show
  1. package/dist/index.js +364 -38
  2. package/dist/index.mjs +364 -38
  3. 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
- function syncToAsync(backend) {
320
- return {
321
- readFile(path, ...args) {
322
- const result = backend.readFile(path, ...args);
323
- return Promise.resolve(result);
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, options) {
326
- backend.writeFile(path, data, options);
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 = backend.readdir(path);
331
- return Promise.resolve(entries.map((e) => typeof e === "string" ? e : e.name));
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 Promise.resolve(backend.stat(path, ...args));
341
+ async stat(path, ...args) {
342
+ return pfs.stat(path, ...args);
335
343
  },
336
- exists(path) {
344
+ async exists(path) {
337
345
  try {
338
- backend.stat(path);
339
- return Promise.resolve(true);
346
+ await pfs.stat(path);
347
+ return true;
340
348
  } catch {
341
- return Promise.resolve(false);
349
+ return false;
342
350
  }
343
351
  },
344
- mkdir(path, options) {
345
- backend.mkdir(path, options);
346
- return Promise.resolve();
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
- unlink(path) {
349
- backend.unlink(path);
350
- return Promise.resolve();
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
- rmdir(path) {
353
- if (typeof backend.rmdir === "function") {
354
- backend.rmdir(path);
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
- rename(oldPath, newPath) {
359
- if (typeof backend.rename === "function") {
360
- backend.rename(oldPath, newPath);
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
- return Promise.resolve();
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
- registerBackend("InMemory", async (options) => {
367
- const { InMemory } = await import("@zenfs/core");
368
- const maxSize = options.maxSize ?? 100 * 1024 * 1024;
369
- const label = options.label ?? "zen-fs-config";
370
- const fs = InMemory.create({ maxSize, label });
371
- return syncToAsync(fs);
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
- function syncToAsync(backend) {
273
- return {
274
- readFile(path, ...args) {
275
- const result = backend.readFile(path, ...args);
276
- return Promise.resolve(result);
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, options) {
279
- backend.writeFile(path, data, options);
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 = backend.readdir(path);
284
- return Promise.resolve(entries.map((e) => typeof e === "string" ? e : e.name));
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 Promise.resolve(backend.stat(path, ...args));
294
+ async stat(path, ...args) {
295
+ return pfs.stat(path, ...args);
288
296
  },
289
- exists(path) {
297
+ async exists(path) {
290
298
  try {
291
- backend.stat(path);
292
- return Promise.resolve(true);
299
+ await pfs.stat(path);
300
+ return true;
293
301
  } catch {
294
- return Promise.resolve(false);
302
+ return false;
295
303
  }
296
304
  },
297
- mkdir(path, options) {
298
- backend.mkdir(path, options);
299
- return Promise.resolve();
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
- unlink(path) {
302
- backend.unlink(path);
303
- return Promise.resolve();
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
- rmdir(path) {
306
- if (typeof backend.rmdir === "function") {
307
- backend.rmdir(path);
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
- rename(oldPath, newPath) {
312
- if (typeof backend.rename === "function") {
313
- backend.rename(oldPath, newPath);
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
- return Promise.resolve();
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
- registerBackend("InMemory", async (options) => {
320
- const { InMemory } = await import("@zenfs/core");
321
- const maxSize = options.maxSize ?? 100 * 1024 * 1024;
322
- const label = options.label ?? "zen-fs-config";
323
- const fs = InMemory.create({ maxSize, label });
324
- return syncToAsync(fs);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
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",