6dc79a2c12
- 私人剪贴板新增限时直通URL生成功能 - doc_cloud_keeper新增标题复制按钮 - 新增贪蛇出动游戏工具 - 新增OSS中转直链工具 - 添加批量抽帧测试脚本
169 lines
6.0 KiB
JavaScript
169 lines
6.0 KiB
JavaScript
/**
|
||
* 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('<Key>', idx)
|
||
if (s < 0) break
|
||
const e = xml.indexOf('</Key>', 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)
|
||
})
|