/** * OSS 中转脚本:把 oss://android-o1-images/trae-files-temp/oss-browser-win32-x64.zip * 下载到本地 public/download_files/,成功后立即删除 OSS 对象。 * * 密钥来源:~/Toolbox_local_creds.env.local(DOC_CLOUD_KEEPER_OSS_ACCESS_KEY_ID / SECRET) * 用法:node dev_test_scripts/runners/run_oss_browser_zip_transit.js [--keep-oss] * --keep-oss 仅下载不删除 OSS 对象(调试用,默认下载成功后删除) */ const fs = require('fs') const os = require('os') const path = require('path') const crypto = require('crypto') const https = require('https') const BUCKET = 'android-o1-images' const ENDPOINT = 'oss-cn-shanghai.aliyuncs.com' const OSS_PREFIX = 'trae-files-temp/' const TARGET_NAME = 'oss-browser-win32-x64.zip' const LOCAL_DIR = path.join(process.cwd(), 'public', 'download_files') const LOCAL_FILE = path.join(LOCAL_DIR, TARGET_NAME) const loadEnv = () => { const out = {} try { const p = path.join(os.homedir(), 'Toolbox_local_creds.env.local') const text = fs.readFileSync(p, 'utf8') for (const line of text.split(/\r?\n/)) { const t = line.trim() if (!t || t.startsWith('#')) continue const eq = t.indexOf('=') if (eq <= 0) continue out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim() } } catch {} return out } const ENV = loadEnv() const AK = ENV.DOC_CLOUD_KEEPER_OSS_ACCESS_KEY_ID || '' const SK = ENV.DOC_CLOUD_KEEPER_OSS_ACCESS_KEY_SECRET || '' if (!AK || !SK) { console.error('[FAIL] 缺少 DOC_CLOUD_KEEPER_OSS_ACCESS_KEY_ID / SECRET(~/Toolbox_local_creds.env.local)') process.exit(1) } const sign = (method, date, resource, contentType = '', canonicalHeaders = '') => { const stringToSign = `${method}\n\n${contentType}\n${date}\n${canonicalHeaders ? canonicalHeaders + '\n' : ''}${resource}` return crypto.createHmac('sha1', SK).update(stringToSign).digest('base64') } const ossRequest = (method, urlPath, resource) => new Promise((resolve, reject) => { const date = new Date().toUTCString() const req = https.request({ hostname: `${BUCKET}.${ENDPOINT}`, path: urlPath, method, headers: { Host: `${BUCKET}.${ENDPOINT}`, Date: date, Authorization: `OSS ${AK}:${sign(method, date, resource)}` } }, (res) => { const chunks = [] res.on('data', c => chunks.push(c)) res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })) }) req.on('error', reject) req.setTimeout(120000, () => req.destroy(new Error('timeout'))) req.end() }) const listPrefix = async (prefix) => { const resp = await ossRequest('GET', `/?prefix=${encodeURIComponent(prefix)}&max-keys=100`, `/${BUCKET}/`) if (resp.status !== 200) throw new Error(`OSS list 失败: ${resp.status} ${resp.body.toString('utf8').slice(0, 200)}`) const xml = resp.body.toString('utf8') const keys = [] let idx = 0 while (true) { const s = xml.indexOf('', idx) if (s < 0) break const e = xml.indexOf('', s) if (e < 0) break keys.push(xml.slice(s + 5, e)) idx = e } return keys } const downloadToFile = (key, localPath) => new Promise((resolve, reject) => { const date = new Date().toUTCString() const resource = `/${BUCKET}/${key}` const req = https.request({ hostname: `${BUCKET}.${ENDPOINT}`, path: `/${key.split('/').map(encodeURIComponent).join('/')}`, method: 'GET', headers: { Host: `${BUCKET}.${ENDPOINT}`, Date: date, Authorization: `OSS ${AK}:${sign('GET', date, resource)}` } }, (res) => { if (res.statusCode !== 200) { const chunks = [] res.on('data', c => chunks.push(c)) res.on('end', () => reject(new Error(`OSS 下载失败: ${res.statusCode} ${Buffer.concat(chunks).toString('utf8').slice(0, 200)}`))) return } const ws = fs.createWriteStream(localPath) let received = 0 res.on('data', c => { received += c.length }) res.pipe(ws) ws.on('finish', () => resolve(received)) ws.on('error', reject) }) req.on('error', reject) req.setTimeout(600000, () => req.destroy(new Error('下载超时'))) req.end() }) const deleteObject = async (key) => { const resource = `/${BUCKET}/${key}` const urlPath = `/${key.split('/').map(encodeURIComponent).join('/')}` const resp = await ossRequest('DELETE', urlPath, resource) if (resp.status !== 204 && resp.status !== 200) { throw new Error(`OSS 删除失败: ${resp.status} ${resp.body.toString('utf8').slice(0, 200)}`) } return true } const bjNow = () => new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai' }) ;(async () => { console.log(`[${bjNow()}] 列举 ${BUCKET}/${OSS_PREFIX} ...`) const keys = await listPrefix(OSS_PREFIX) console.log(` 前缀下共 ${keys.length} 个对象`) const zipKey = keys.find(k => k.endsWith('/' + TARGET_NAME) || k === TARGET_NAME) if (!zipKey) { console.error(`[FAIL] 未找到 ${OSS_PREFIX}${TARGET_NAME},现有对象:`) keys.forEach(k => console.error(' - ' + k)) process.exit(1) } console.log(` 找到对象: ${zipKey}`) if (!fs.existsSync(LOCAL_DIR)) fs.mkdirSync(LOCAL_DIR, { recursive: true }) const tmpFile = LOCAL_FILE + '.part' console.log(`[${bjNow()}] 开始下载到 ${LOCAL_FILE} ...`) const received = await downloadToFile(zipKey, tmpFile) const size = fs.statSync(tmpFile).size if (size < 1024 * 1024) { fs.unlinkSync(tmpFile) throw new Error(`下载文件过小(${size} 字节),疑似异常,已丢弃临时文件`) } if (fs.existsSync(LOCAL_FILE)) fs.unlinkSync(LOCAL_FILE) fs.renameSync(tmpFile, LOCAL_FILE) console.log(` 下载完成: ${(size / 1024 / 1024).toFixed(2)} MB`) if (process.argv.includes('--keep-oss')) { console.log('[SKIP] --keep-oss:保留 OSS 对象不删除') } else { console.log(`[${bjNow()}] 删除 OSS 对象 ${zipKey} ...`) await deleteObject(zipKey) console.log(' OSS 对象已删除(中转完成)') } console.log(`[${bjNow()}] 中转成功:本地 ${LOCAL_FILE}`) })().catch(e => { console.error('[FAIL]', e.message || e) process.exit(1) })