#!/usr/bin/env node /** * S3 文件上传脚本 * 用法:node upload-s3.mjs <文件路径> [S3目录前缀] * * 示例: * node upload-s3.mjs ./test.png → https://ibitcex.s3.ap-southeast-1.amazonaws.com/test.png * node upload-s3.mjs ./logo.png images → https://ibitcex.s3.ap-southeast-1.amazonaws.com/images/logo.png */ import { readFileSync, existsSync } from 'fs'; import { basename, resolve } from 'path'; import { fileURLToPath } from 'url'; import { createHash, createHmac } from 'crypto'; import { request } from 'https'; // ========== S3 配置 ========== const CONFIG = { accessKeyId: 'AKIARHQKVVUBD3M6E3XV', secretAccessKey: '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', bucket: 'ibitcex', region: 'ap-southeast-1', }; // ========== AWS Signature V4 签名 ========== const SERVICE = 's3'; const ALGORITHM = 'AWS4-HMAC-SHA256'; function hmac(key, data) { return createHmac('sha256', key).update(data).digest(); } function sha256(data) { return createHash('sha256').update(data).digest('hex'); } function getSigningKey(dateStamp) { const kDate = hmac('AWS4' + CONFIG.secretAccessKey, dateStamp); const kRegion = hmac(kDate, CONFIG.region); const kService = hmac(kRegion, SERVICE); return hmac(kService, 'aws4_request'); } function getSignature(key, stringToSign) { return hmac(key, stringToSign).toString('hex'); } // ========== 上传 ========== function uploadFile(filePath, s3Key) { const fileContent = readFileSync(filePath); const host = `${CONFIG.bucket}.s3.${CONFIG.region}.amazonaws.com`; const amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); const dateStamp = amzDate.substring(0, 8); const payloadHash = sha256(fileContent); const headers = { 'Host': host, 'Content-Length': String(fileContent.length), 'Content-Type': 'application/octet-stream', 'x-amz-content-sha256': payloadHash, 'x-amz-date': amzDate, }; const signedHeaders = Object.keys(headers) .sort() .map(k => k.toLowerCase()) .join(';'); // Canonical Request const canonicalRequest = [ 'PUT', '/' + encodeURIComponent(s3Key).replace(/%2F/g, '/'), '', ...Object.keys(headers).sort().map(k => `${k.toLowerCase()}:${headers[k].trim()}`), '', signedHeaders, payloadHash, ].join('\n'); // String to Sign const stringToSign = [ ALGORITHM, amzDate, `${dateStamp}/${CONFIG.region}/${SERVICE}/aws4_request`, sha256(canonicalRequest), ].join('\n'); const signingKey = getSigningKey(dateStamp); const signature = getSignature(signingKey, stringToSign); const authorization = `${ALGORITHM} Credential=${CONFIG.accessKeyId}/${dateStamp}/${CONFIG.region}/${SERVICE}/aws4_request,SignedHeaders=${signedHeaders},Signature=${signature}`; return new Promise((resolvePromise, reject) => { const req = request( { hostname: host, path: '/' + s3Key, method: 'PUT', headers: { ...headers, Authorization: authorization }, }, (res) => { let body = ''; res.on('data', (chunk) => (body += chunk)); res.on('end', () => { if (res.statusCode === 200) { resolvePromise(`https://${host}/${s3Key}`); } else { reject(new Error(`上传失败 HTTP ${res.statusCode}: ${body}`)); } }); } ); req.on('error', reject); req.write(fileContent); req.end(); }); } // ========== 主函数 ========== (async () => { const args = process.argv.slice(2); if (args.length < 1) { console.error('用法: node upload-s3.mjs <文件路径> [S3目录前缀]'); process.exit(1); } const filePath = resolve(args[0]); if (!existsSync(filePath)) { console.error(`文件不存在: ${filePath}`); process.exit(1); } const prefix = args[1] ? args[1].replace(/\/$/g, '') + '/' : ''; const s3Key = prefix + basename(filePath); console.log(`上传: ${filePath} → s3://${CONFIG.bucket}/${s3Key}`); console.log(`大小: ${(readFileSync(filePath).length / 1024).toFixed(1)} KB`); try { const url = await uploadFile(filePath, s3Key); console.log(`\n✓ 上传成功`); console.log(`URL: ${url}`); } catch (e) { console.error(`\n✗ ${e.message}`); process.exit(1); } })();