zen-fs-config 0.1.3 → 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 +332 -0
- package/dist/index.mjs +332 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -364,6 +364,338 @@ registerBackend("InMemory", async (options) => {
|
|
|
364
364
|
};
|
|
365
365
|
return backend;
|
|
366
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);
|
|
424
|
+
},
|
|
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
|
+
}
|
|
435
|
+
},
|
|
436
|
+
async exists(path) {
|
|
437
|
+
try {
|
|
438
|
+
await fetchJson(apiUrl(path));
|
|
439
|
+
return true;
|
|
440
|
+
} catch {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
},
|
|
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
|
+
}
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
async rename(oldPath, newPath) {
|
|
477
|
+
const content = await backend.readFile(oldPath);
|
|
478
|
+
await backend.writeFile(newPath, content);
|
|
479
|
+
await backend.unlink(oldPath);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
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;
|
|
698
|
+
});
|
|
367
699
|
|
|
368
700
|
// src/version.ts
|
|
369
701
|
function versionPathFor(configFilePath) {
|
package/dist/index.mjs
CHANGED
|
@@ -317,6 +317,338 @@ registerBackend("InMemory", async (options) => {
|
|
|
317
317
|
};
|
|
318
318
|
return backend;
|
|
319
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);
|
|
377
|
+
},
|
|
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
|
+
}
|
|
388
|
+
},
|
|
389
|
+
async exists(path) {
|
|
390
|
+
try {
|
|
391
|
+
await fetchJson(apiUrl(path));
|
|
392
|
+
return true;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
},
|
|
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
|
+
}
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
async rename(oldPath, newPath) {
|
|
430
|
+
const content = await backend.readFile(oldPath);
|
|
431
|
+
await backend.writeFile(newPath, content);
|
|
432
|
+
await backend.unlink(oldPath);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
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;
|
|
651
|
+
});
|
|
320
652
|
|
|
321
653
|
// src/version.ts
|
|
322
654
|
function versionPathFor(configFilePath) {
|