| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384 |
- // 零依赖 OSS 客户端(通用安装、无服务端项目时的回退实现):
- // OSS ListObjectsV2 + V1 签名(Header Authorization: OSS AK:SIG),与 server/services/oss-client.js 行为对齐
- // 凭据只从进程环境读取(scripts/oss.mjs 已先 loadEnv 注入),本模块不含任何密钥
- import crypto from 'crypto';
- export function ossConfig() {
- return {
- bucket: process.env.OSS_BUCKET || '',
- region: process.env.OSS_REGION || '',
- accessKeyId: process.env.OSS_ACCESS_KEY_ID || '',
- accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || '',
- };
- }
- export function ossEnabled() {
- const c = ossConfig();
- return Boolean(c.bucket && c.region && c.accessKeyId && c.accessKeySecret);
- }
- /** 公开读直链域名: 默认桶默认域名(oss-<region>.aliyuncs.com),配 OSS_PUBLIC_DOMAIN 可切换(同服务端规则) */
- export function publicDomain() {
- const c = ossConfig();
- return (process.env.OSS_PUBLIC_DOMAIN || `${c.bucket}.oss-${c.region}.aliyuncs.com`).replace(/^https?:\/\//, '');
- }
- function xmlDecode(s) {
- return String(s)
- .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
- .replace(/"/g, '"').replace(/'/g, "'")
- .replace(/&#x([0-9a-f]+);/gi, (m, n) => { try { return String.fromCharCode(parseInt(n, 16)); } catch { return m; } })
- .replace(/&#(\d+);/g, (m, n) => { try { return String.fromCharCode(Number(n)); } catch { return m; } });
- }
- function xmlTag(text, name) {
- const m = new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`).exec(text);
- return m ? xmlDecode(m[1]) : null;
- }
- // 与 ali-oss 一致的 V1 签名:
- // - canonicalized resource = /bucket/ + (?continuation-token=<原值> 仅当存在) —— continue-token 是子资源
- // - list-type/max-keys/prefix 为普通 query,不参与签名,只在请求 URL 中 percent 编码
- // - 日期走 x-oss-date 头(GMT)
- export async function _listPage(params, token) {
- const c = ossConfig();
- const date = new Date().toUTCString();
- const urlQ = Object.entries(params)
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
- .concat(token ? [`continuation-token=${encodeURIComponent(token)}`] : [])
- .join('&');
- const canonicalResource = `/${c.bucket}/${token ? `?continuation-token=${token}` : ''}`;
- const stringToSign = ['GET', '', '', date, `x-oss-date:${date}`, canonicalResource].join('\n');
- const sig = crypto.createHmac('sha1', c.accessKeySecret).update(stringToSign).digest('base64');
- const url = `https://${c.bucket}.oss-${c.region}.aliyuncs.com/?${urlQ}`;
- const res = await fetch(url, {
- headers: { 'x-oss-date': date, Authorization: `OSS ${c.accessKeyId}:${sig}` },
- });
- if (!res.ok) throw new Error(`OSS 列举失败 HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
- return await res.text();
- }
- /** 列举桶内对象(prefix 为空=全桶),与 server/services/oss-client.js listObjects 返回结构一致 */
- export async function listObjects(prefix = '') {
- const objects = [];
- let token = null;
- for (;;) {
- const params = prefix ? { 'list-type': '2', 'max-keys': '1000', prefix } : { 'list-type': '2', 'max-keys': '1000' };
- const text = await _listPage(params, token);
- const contents = new RegExp('<Contents>([\\s\\S]*?)<\\/Contents>', 'g');
- let m;
- while ((m = contents.exec(text))) {
- const size = Number(xmlTag(m[1], 'Size')) || 0;
- const last = xmlTag(m[1], 'LastModified');
- objects.push({
- name: xmlTag(m[1], 'Key'),
- size,
- lastModified: last ? new Date(last).toISOString() : null,
- });
- }
- if (xmlTag(text, 'IsTruncated') !== 'true') break;
- token = xmlTag(text, 'NextContinuationToken');
- if (!token) break;
- }
- return objects;
- }
|