oss-client-mini.mjs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. // 零依赖 OSS 客户端(通用安装、无服务端项目时的回退实现):
  2. // OSS ListObjectsV2 + V1 签名(Header Authorization: OSS AK:SIG),与 server/services/oss-client.js 行为对齐
  3. // 凭据只从进程环境读取(scripts/oss.mjs 已先 loadEnv 注入),本模块不含任何密钥
  4. import crypto from 'crypto';
  5. export function ossConfig() {
  6. return {
  7. bucket: process.env.OSS_BUCKET || '',
  8. region: process.env.OSS_REGION || '',
  9. accessKeyId: process.env.OSS_ACCESS_KEY_ID || '',
  10. accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || '',
  11. };
  12. }
  13. export function ossEnabled() {
  14. const c = ossConfig();
  15. return Boolean(c.bucket && c.region && c.accessKeyId && c.accessKeySecret);
  16. }
  17. /** 公开读直链域名: 默认桶默认域名(oss-<region>.aliyuncs.com),配 OSS_PUBLIC_DOMAIN 可切换(同服务端规则) */
  18. export function publicDomain() {
  19. const c = ossConfig();
  20. return (process.env.OSS_PUBLIC_DOMAIN || `${c.bucket}.oss-${c.region}.aliyuncs.com`).replace(/^https?:\/\//, '');
  21. }
  22. function xmlDecode(s) {
  23. return String(s)
  24. .replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
  25. .replace(/&quot;/g, '"').replace(/&apos;/g, "'")
  26. .replace(/&#x([0-9a-f]+);/gi, (m, n) => { try { return String.fromCharCode(parseInt(n, 16)); } catch { return m; } })
  27. .replace(/&#(\d+);/g, (m, n) => { try { return String.fromCharCode(Number(n)); } catch { return m; } });
  28. }
  29. function xmlTag(text, name) {
  30. const m = new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`).exec(text);
  31. return m ? xmlDecode(m[1]) : null;
  32. }
  33. // 与 ali-oss 一致的 V1 签名:
  34. // - canonicalized resource = /bucket/ + (?continuation-token=<原值> 仅当存在) —— continue-token 是子资源
  35. // - list-type/max-keys/prefix 为普通 query,不参与签名,只在请求 URL 中 percent 编码
  36. // - 日期走 x-oss-date 头(GMT)
  37. export async function _listPage(params, token) {
  38. const c = ossConfig();
  39. const date = new Date().toUTCString();
  40. const urlQ = Object.entries(params)
  41. .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
  42. .concat(token ? [`continuation-token=${encodeURIComponent(token)}`] : [])
  43. .join('&');
  44. const canonicalResource = `/${c.bucket}/${token ? `?continuation-token=${token}` : ''}`;
  45. const stringToSign = ['GET', '', '', date, `x-oss-date:${date}`, canonicalResource].join('\n');
  46. const sig = crypto.createHmac('sha1', c.accessKeySecret).update(stringToSign).digest('base64');
  47. const url = `https://${c.bucket}.oss-${c.region}.aliyuncs.com/?${urlQ}`;
  48. const res = await fetch(url, {
  49. headers: { 'x-oss-date': date, Authorization: `OSS ${c.accessKeyId}:${sig}` },
  50. });
  51. if (!res.ok) throw new Error(`OSS 列举失败 HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
  52. return await res.text();
  53. }
  54. /** 列举桶内对象(prefix 为空=全桶),与 server/services/oss-client.js listObjects 返回结构一致 */
  55. export async function listObjects(prefix = '') {
  56. const objects = [];
  57. let token = null;
  58. for (;;) {
  59. const params = prefix ? { 'list-type': '2', 'max-keys': '1000', prefix } : { 'list-type': '2', 'max-keys': '1000' };
  60. const text = await _listPage(params, token);
  61. const contents = new RegExp('<Contents>([\\s\\S]*?)<\\/Contents>', 'g');
  62. let m;
  63. while ((m = contents.exec(text))) {
  64. const size = Number(xmlTag(m[1], 'Size')) || 0;
  65. const last = xmlTag(m[1], 'LastModified');
  66. objects.push({
  67. name: xmlTag(m[1], 'Key'),
  68. size,
  69. lastModified: last ? new Date(last).toISOString() : null,
  70. });
  71. }
  72. if (xmlTag(text, 'IsTruncated') !== 'true') break;
  73. token = xmlTag(text, 'NextContinuationToken');
  74. if (!token) break;
  75. }
  76. return objects;
  77. }