uploader.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. #!/usr/bin/env node
  2. #!/usr/bin/env node
  3. /**
  4. * fmode-storage uploader — 对象存储上传/公开链接/ACL
  5. * 零依赖(Node ≥18)。凭据按 4 级优先级自动解析,永不入库。
  6. *
  7. * 用法:
  8. * node uploader.mjs put <file> --key <objectKey> [--acl public-read] [--endpoint ...] [--bucket ...]
  9. * node uploader.mjs setacl --key <prefix|key> --acl public-read [-r]
  10. */
  11. import fs from 'fs';
  12. import os from 'os';
  13. import path from 'path';
  14. import { execSync, spawnSync } from 'child_process';
  15. const HOME = os.homedir();
  16. /** 4 级凭据/配置解析: env → ~/.fmode/config.json → ./.fmode/config.json → obsutil 默认链 */
  17. export function resolveStorageConfig() {
  18. const cfg = { obsUtilPath: 'obsutil', endpoint: 'obs.cn-south-1.myhuaweicloud.com', bucket: null, cdnDomain: null };
  19. // 1) 环境变量
  20. if (process.env.FMODE_OBS_CONF) {
  21. try { Object.assign(cfg, JSON.parse(fs.readFileSync(process.env.FMODE_OBS_CONF, 'utf8'))); } catch {}
  22. }
  23. // 2) ~/.fmode/config.json
  24. for (const p of [path.join(HOME, '.fmode', 'config.json'), path.join(process.cwd(), '.fmode', 'config.json')]) {
  25. try {
  26. const j = JSON.parse(fs.readFileSync(p, 'utf8'));
  27. if (j.obsUtilPath) cfg.obsUtilPath = j.obsUtilPath;
  28. if (j.obsBucket) cfg.bucket = j.obsBucket;
  29. if (j.obsEndpoint) cfg.endpoint = j.obsEndpoint;
  30. if (j.cdnDomain) cfg.cdnDomain = j.cdnDomain;
  31. } catch {}
  32. }
  33. if (!cfg.bucket) {
  34. // 3) obsutil 默认链: 若 obsutil 已配置, 从其 log/配置探测 bucket
  35. try {
  36. const out = execSync(`${cfg.obsUtilPath} ls -limit=1 2>/dev/null`, { encoding: 'utf8', timeout: 15000 });
  37. const m = out.match(/obs:\/\/([^/]+)/);
  38. if (m) cfg.bucket = m[1];
  39. } catch {}
  40. }
  41. return cfg;
  42. }
  43. /** 公开 URL: cdnDomain 优先, 否则桶域名 */
  44. export function publicUrl(cfg, key) {
  45. if (cfg.cdnDomain) return `https://${cfg.cdnDomain}/${key}`;
  46. return `https://${cfg.bucket}.${cfg.endpoint}/${key}`.replace('.myhuaweicloud.com', '.myhuaweicloud.com');
  47. }
  48. function obs(args, cfg, timeout = 120000) {
  49. const r = spawnSync(cfg.obsUtilPath, [...args, '-e', cfg.endpoint], { encoding: 'utf8', timeout });
  50. const out = (r.stdout || '') + (r.stderr || '');
  51. return { ok: /Upload successfully|Download successfully|Set the acl/i.test(out) || r.status === 0, out };
  52. }
  53. function main() {
  54. const [cmd, ...rest] = process.argv.slice(2);
  55. const cfg = resolveStorageConfig();
  56. if (!cfg.bucket && cmd !== 'help') {
  57. console.error('未解析到 bucket:请配置 ~/.fmode/config.json 的 obsBucket/obsEndpoint 或 obsutil config。');
  58. console.error('(技能绝不内置密钥;AK/SK 用 obsutil config 自行配置)');
  59. process.exit(2);
  60. }
  61. const arg = (name) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : null; };
  62. if (cmd === 'put') {
  63. const file = rest[0];
  64. let key = arg('--key');
  65. if (!file || !key) { console.error('用法: put <file> --key <objectKey> [--acl public-read]'); process.exit(2); }
  66. const acl = arg('--acl') || 'public-read';
  67. const bucketArg = arg('--bucket'); if (bucketArg) cfg.bucket = bucketArg;
  68. const r = obs(['cp', file, `obs://${cfg.bucket}/${key}`], cfg);
  69. if (!r.ok) { console.error('上传失败:', r.out.slice(-300)); process.exit(1); }
  70. console.log(JSON.stringify({ ok: true, key, url: publicUrl(cfg, key), bucket: cfg.bucket }, null, 2));
  71. return;
  72. }
  73. if (cmd === 'setacl') {
  74. const key = arg('--key'); const acl = arg('--acl') || 'public-read';
  75. const recursive = rest.includes('-r');
  76. const r = obs(['chattri', `obs://${cfg.bucket}/${key}`, '-acl', acl, ...(recursive ? ['-r', '-f'] : ['-f'])], cfg);
  77. console.log(JSON.stringify({ ok: r.ok }, null, 2));
  78. return;
  79. }
  80. if (cmd === 'config') { console.log(JSON.stringify({ ...cfg }, null, 2)); return; }
  81. console.log('用法: put | setacl | config');
  82. }
  83. main();