uploader.mjs 3.8 KB

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