|
|
@@ -0,0 +1,149 @@
|
|
|
+#!/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: '7l4+5w4fqxAtUTWNahLJkSMUQ4qFXnntX5T1FzO8',
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+})();
|