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; });