YD_Event.Web/scripts/build-and-upload-oss.cjs
2026-07-14 10:05:12 +08:00

155 lines
5.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const crypto = require('crypto');
const fs = require('fs');
const https = require('https');
const path = require('path');
const { spawnSync } = require('child_process');
const bucket = 'yuedong-wechatapplet';
const endpoint = 'oss-cn-shanghai.aliyuncs.com';
const uploadPrefix = 'static/event_web';
const publicRoot = `https://${bucket}.${endpoint}/${uploadPrefix}`;
const concurrency = 8;
const pad = (value) => value.toString().padStart(2, '0');
const now = new Date();
const buildDirectory = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(
now.getMinutes()
)}`;
const outputPath = path.resolve(process.cwd(), buildDirectory);
const publicPath = `${publicRoot}/${buildDirectory}/`;
const dryRun = process.env.OSS_DRY_RUN === '1';
const accessKeyId = 'LTAI5t79WkXht8yy8aAAZ5qz';
const accessKeySecret = 'RpJ5miIeGARvfy2G1P8HtWvdR4j6rq';
const securityToken = process.env.ALIYUN_OSS_SECURITY_TOKEN || process.env.OSS_SECURITY_TOKEN;
if (!dryRun && (!accessKeyId || !accessKeySecret)) {
console.error('缺少阿里云 OSS 凭证,请设置 ALIYUN_OSS_ACCESS_KEY_ID 和 ALIYUN_OSS_ACCESS_KEY_SECRET。');
process.exit(1);
}
console.log(`开始打包:${buildDirectory}`);
console.log(`资源地址:${publicPath}`);
const viteCli = path.resolve(process.cwd(), 'node_modules/vite/bin/vite.js');
const buildEnvironment = { ...process.env };
[
'ALIYUN_OSS_ACCESS_KEY_ID',
'ALIYUN_OSS_ACCESS_KEY_SECRET',
'ALIYUN_OSS_SECURITY_TOKEN',
'OSS_ACCESS_KEY_ID',
'OSS_ACCESS_KEY_SECRET',
'OSS_SECURITY_TOKEN',
].forEach((key) => delete buildEnvironment[key]);
const buildResult = spawnSync(process.execPath, [viteCli, 'build'], {
stdio: 'inherit',
env: {
...buildEnvironment,
BUILD_OUT_DIR: buildDirectory,
BUILD_PUBLIC_PATH: publicPath,
},
});
if (buildResult.error) throw buildResult.error;
if (buildResult.status !== 0) process.exit(buildResult.status || 1);
const getFiles = (directory) =>
fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const fullPath = path.join(directory, entry.name);
return entry.isDirectory() ? getFiles(fullPath) : [fullPath];
});
const contentTypes = {
'.css': 'text/css; charset=utf-8',
'.eot': 'application/vnd.ms-fontobject',
'.gif': 'image/gif',
'.html': 'text/html; charset=utf-8',
'.ico': 'image/x-icon',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.map': 'application/json; charset=utf-8',
'.mp3': 'audio/mpeg',
'.mp4': 'video/mp4',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.ttf': 'font/ttf',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
};
const encodeObjectKey = (objectKey) => objectKey.split('/').map(encodeURIComponent).join('/');
const uploadFile = (filePath, objectKey, attempt = 1) =>
new Promise((resolve, reject) => {
const date = new Date().toUTCString();
const contentType = contentTypes[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
const canonicalHeaders = securityToken ? `x-oss-security-token:${securityToken}\n` : '';
const canonicalResource = `/${bucket}/${objectKey}`;
const stringToSign = `PUT\n\n${contentType}\n${date}\n${canonicalHeaders}${canonicalResource}`;
const signature = crypto.createHmac('sha1', accessKeySecret).update(stringToSign).digest('base64');
const headers = {
Authorization: `OSS ${accessKeyId}:${signature}`,
'Content-Length': fs.statSync(filePath).size,
'Content-Type': contentType,
Date: date,
};
if (securityToken) headers['x-oss-security-token'] = securityToken;
const request = https.request(
{
host: `${bucket}.${endpoint}`,
path: `/${encodeObjectKey(objectKey)}`,
method: 'PUT',
headers,
},
(response) => {
let responseBody = '';
response.setEncoding('utf8');
response.on('data', (chunk) => (responseBody += chunk));
response.on('end', () => {
if (response.statusCode >= 200 && response.statusCode < 300) return resolve();
const error = new Error(`OSS 上传失败 (${response.statusCode}):${objectKey}\n${responseBody.slice(0, 2000)}`);
if (response.statusCode >= 500 && attempt < 3) return setTimeout(() => resolve(uploadFile(filePath, objectKey, attempt + 1)), 500 * attempt);
reject(error);
});
}
);
request.on('error', (error) => {
if (attempt < 3) return setTimeout(() => resolve(uploadFile(filePath, objectKey, attempt + 1)), 500 * attempt);
reject(error);
});
fs.createReadStream(filePath).pipe(request);
});
const files = getFiles(outputPath).sort();
if (dryRun) {
console.log(`[DRY RUN] 已生成 ${files.length} 个文件,将上传到 ${publicPath}`);
process.exit(0);
}
let nextIndex = 0;
let uploaded = 0;
const worker = async () => {
while (nextIndex < files.length) {
const filePath = files[nextIndex++];
const relativePath = path.relative(outputPath, filePath).split(path.sep).join('/');
const objectKey = `${uploadPrefix}/${buildDirectory}/${relativePath}`;
await uploadFile(filePath, objectKey);
uploaded += 1;
if (uploaded % 50 === 0 || uploaded === files.length) console.log(`上传进度:${uploaded}/${files.length}`);
}
};
Promise.all(Array.from({ length: Math.min(concurrency, files.length) }, worker))
.then(() => console.log(`部署完成:${publicPath}`))
.catch((error) => {
console.error(error.message);
process.exitCode = 1;
});