upload-s3.mjs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. #!/usr/bin/env node
  2. /**
  3. * S3 文件上传脚本
  4. * 用法:node upload-s3.mjs <文件路径> [S3目录前缀]
  5. *
  6. * 示例:
  7. * node upload-s3.mjs ./test.png → https://ibitcex.s3.ap-southeast-1.amazonaws.com/test.png
  8. * node upload-s3.mjs ./logo.png images → https://ibitcex.s3.ap-southeast-1.amazonaws.com/images/logo.png
  9. */
  10. import { readFileSync, existsSync } from 'fs';
  11. import { basename, resolve } from 'path';
  12. import { fileURLToPath } from 'url';
  13. import { createHash, createHmac } from 'crypto';
  14. import { request } from 'https';
  15. // ========== S3 配置 ==========
  16. const CONFIG = {
  17. accessKeyId: 'AKIARHQKVVUBD3M6E3XV',
  18. secretAccessKey: '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',
  19. bucket: 'ibitcex',
  20. region: 'ap-southeast-1',
  21. };
  22. // ========== AWS Signature V4 签名 ==========
  23. const SERVICE = 's3';
  24. const ALGORITHM = 'AWS4-HMAC-SHA256';
  25. function hmac(key, data) {
  26. return createHmac('sha256', key).update(data).digest();
  27. }
  28. function sha256(data) {
  29. return createHash('sha256').update(data).digest('hex');
  30. }
  31. function getSigningKey(dateStamp) {
  32. const kDate = hmac('AWS4' + CONFIG.secretAccessKey, dateStamp);
  33. const kRegion = hmac(kDate, CONFIG.region);
  34. const kService = hmac(kRegion, SERVICE);
  35. return hmac(kService, 'aws4_request');
  36. }
  37. function getSignature(key, stringToSign) {
  38. return hmac(key, stringToSign).toString('hex');
  39. }
  40. // ========== 上传 ==========
  41. function uploadFile(filePath, s3Key) {
  42. const fileContent = readFileSync(filePath);
  43. const host = `${CONFIG.bucket}.s3.${CONFIG.region}.amazonaws.com`;
  44. const amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
  45. const dateStamp = amzDate.substring(0, 8);
  46. const payloadHash = sha256(fileContent);
  47. const headers = {
  48. 'Host': host,
  49. 'Content-Length': String(fileContent.length),
  50. 'Content-Type': 'application/octet-stream',
  51. 'x-amz-content-sha256': payloadHash,
  52. 'x-amz-date': amzDate,
  53. };
  54. const signedHeaders = Object.keys(headers)
  55. .sort()
  56. .map(k => k.toLowerCase())
  57. .join(';');
  58. // Canonical Request
  59. const canonicalRequest = [
  60. 'PUT',
  61. '/' + encodeURIComponent(s3Key).replace(/%2F/g, '/'),
  62. '',
  63. ...Object.keys(headers).sort().map(k => `${k.toLowerCase()}:${headers[k].trim()}`),
  64. '',
  65. signedHeaders,
  66. payloadHash,
  67. ].join('\n');
  68. // String to Sign
  69. const stringToSign = [
  70. ALGORITHM,
  71. amzDate,
  72. `${dateStamp}/${CONFIG.region}/${SERVICE}/aws4_request`,
  73. sha256(canonicalRequest),
  74. ].join('\n');
  75. const signingKey = getSigningKey(dateStamp);
  76. const signature = getSignature(signingKey, stringToSign);
  77. const authorization = `${ALGORITHM} Credential=${CONFIG.accessKeyId}/${dateStamp}/${CONFIG.region}/${SERVICE}/aws4_request,SignedHeaders=${signedHeaders},Signature=${signature}`;
  78. return new Promise((resolvePromise, reject) => {
  79. const req = request(
  80. {
  81. hostname: host,
  82. path: '/' + s3Key,
  83. method: 'PUT',
  84. headers: { ...headers, Authorization: authorization },
  85. },
  86. (res) => {
  87. let body = '';
  88. res.on('data', (chunk) => (body += chunk));
  89. res.on('end', () => {
  90. if (res.statusCode === 200) {
  91. resolvePromise(`https://${host}/${s3Key}`);
  92. } else {
  93. reject(new Error(`上传失败 HTTP ${res.statusCode}: ${body}`));
  94. }
  95. });
  96. }
  97. );
  98. req.on('error', reject);
  99. req.write(fileContent);
  100. req.end();
  101. });
  102. }
  103. // ========== 主函数 ==========
  104. (async () => {
  105. const args = process.argv.slice(2);
  106. if (args.length < 1) {
  107. console.error('用法: node upload-s3.mjs <文件路径> [S3目录前缀]');
  108. process.exit(1);
  109. }
  110. const filePath = resolve(args[0]);
  111. if (!existsSync(filePath)) {
  112. console.error(`文件不存在: ${filePath}`);
  113. process.exit(1);
  114. }
  115. const prefix = args[1] ? args[1].replace(/\/$/g, '') + '/' : '';
  116. const s3Key = prefix + basename(filePath);
  117. console.log(`上传: ${filePath} → s3://${CONFIG.bucket}/${s3Key}`);
  118. console.log(`大小: ${(readFileSync(filePath).length / 1024).toFixed(1)} KB`);
  119. try {
  120. const url = await uploadFile(filePath, s3Key);
  121. console.log(`\n✓ 上传成功`);
  122. console.log(`URL: ${url}`);
  123. } catch (e) {
  124. console.error(`\n✗ ${e.message}`);
  125. process.exit(1);
  126. }
  127. })();