123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- /**
- * 更新发布工具
- *
- * Copyright © 2025 imengyu.top imengyu-update-server
- */
- import { confirm , input } from '@inquirer/prompts';
- import { writeFile, readFile, access, unlink, readdir, stat, constants } from 'node:fs/promises';
- import { exec } from 'node:child_process';
- import fs from 'fs';
- import archiver from 'archiver';
- import path from 'node:path';
- import OSS from 'ali-oss';
- import cliProgress from 'cli-progress';
- import { selectVersion } from './index.mjs';
- import { config } from './postConfig.mjs';
- import { dirname } from 'node:path';
- import { fileURLToPath } from 'node:url';
- //基础配置
- //========================================
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = dirname(__filename);
- function readFileRange(file, start, length) {
- return new Promise((resolve, reject) => {
- fs.open(file, 'r', (err, fd) => {
- if (err) {
- reject('Error opening file:', er);
- return;
- }
- const buffer = Buffer.alloc(length);
- fs.read(fd, buffer, 0, length, start, (err, bytesRead, buffer) => {
- if (err) {
- reject('Error reading file:', err)
- return;
- }
- fs.close(fd, (err) => {
- if (err) {
- reject('Error closing file:', err)
- return ;
- }
- resolve(buffer);
- });
- });
- });
- })
-
- }
- export function pad(num, n) {
- var strNum = num.toString();
- var len = strNum.length;
- while (len < n) {
- strNum = "0" + strNum;
- len++;
- }
- return strNum;
- }
- async function getConfig() {
- let postConfig = {
- lastVersion: '',
- lastUpdateInfo: '',
- lastSubmitDay: '',
- lastTodaySubVersion: 0,
- };
- try {
- postConfig = JSON.parse(await readFile(path.resolve(__dirname, './_config.json')));
- } catch {
- //
- }
- if (postConfig.lastSubmitDay != new Date().getDate())
- postConfig.lastTodaySubVersion = 0;
- return postConfig;
- }
- async function saveConfig(postConfig) {
- postConfig.lastSubmitDay = new Date().getDate();
- await writeFile(path.resolve(__dirname, './_config.json'), JSON.stringify(postConfig));
- }
- //App更新与提交
- //========================================
- export async function postAppUpdate(axiosInstance, param) {
- const postConfig = await getConfig();
- const versionId = await selectVersion(false, postConfig.lastVersion);
- const updateInfo = await input({ message: '输入更新信息', default: postConfig.lastUpdateInfo });
- await axiosInstance.post('/update-post', { config: { type: 2, test: true, versionId, uploadWebConfig: config.uploadWebConfig, submitKey: config.submitKey } });
- postConfig.lastVersion = versionId;
- postConfig.lastUpdateInfo = updateInfo;
- postConfig.lastTodaySubVersion++;
- const updateSource = (await select({
- choices: [
- {
- name: '重新构建',
- value: 'rebuild',
- },
- {
- name: '已上传的阿里OSS文件路径',
- value: 'uploaded-alioss',
- },
- ],
- message: '选择上传来源',
- default: 'rebuild',
- }));
- const force = await confirm({ message: '强制更新?', default: false });
- const updateNext = hotfix ? false : await confirm({ message: '作为下个版本?', default: false });
- const versionCode = config.buildAppGetVersion();
- await saveConfig(postConfig);
- if (updateSource === 'rebuild')
- await config.buildAppCallback(param, versionCode, postConfig.lastTodaySubVersion);
- else if (updateSource === 'uploaded-alioss') {
- const fileName = await input({ message: '输入已上传的阿里OSS文件路径' });
- try {
- const result = (await axiosInstance.post('/update-post', {
- type: 2,
- ossConfig: {
- ossPath: fileName,
- ossPublic: '',
- },
- uploadAppConfig: {
- updateAsNext: updateNext,
- },
- versionId,
- updateInfo,
- versionCode: versionCode
- })).data;
- console.log('上传成功');
- console.log('新更新ID: ' + result.updateId);
- } catch (e) {
- console.error('上传失败', e);
- }
- return;
- }
- else {
- console.error('错误的选择');
- return;
- }
- const uploadFile = await config.buildAppGetUploadFile(param);
- try {
- await access(uploadFile, constants.R_OK)
- } catch {
- console.error(`Failed to access ${uploadFile}, did you created it?`);
- return;
- }
- console.log('开始上传');
- //小于8mb则小文件上传,否则使用阿里OSS上传
- const fileInfo = await stat(uploadFile);
- if (fileInfo.size < 8 * 1024 * 1024) {
- const appData = (await readFile(uploadFile));
- const formData = new FormData();
- formData.append("file", new Blob([ appData ], { type : 'application/zip' }));
- formData.append("type", 2);
- formData.append("versionId", versionId);
- formData.append("updateInfo", updateInfo);
- formData.append("uploadAppConfig", { updateAsNext: updateNext });
- formData.append("force", force);
- formData.append("versionCode", versionCode);
- try {
- const result = (await axiosInstance.post('/update-post', formData, {
- headers: { 'Content-Type': 'multipart/form-data' }
- })).data;
- console.log('上传成功');
- console.log('新更新ID: ' + result.updateId);
- console.log('删除构建文件');
- await unlink(uploadFile);
- } catch (e) {
- console.error('上传失败', e);
- }
- } else {
- //阿里OSS上传
- //请求STS进行临时授权
- const stsToken = (await axiosInstance.post('/update-ali-oss-sts')).data;
- const client = new OSS({
- region: stsToken.Region,
- accessKeyId: stsToken.AccessKeyId,
- accessKeySecret: stsToken.AccessKeySecret,
- stsToken: stsToken.SecurityToken,
- bucket: stsToken.Bucket,
- refreshSTSToken: async () => {
- const refreshToken = (await axiosInstance.get("/update-ali-oss-sts")).data;
- return {
- accessKeyId: refreshToken.AccessKeyId,
- accessKeySecret: refreshToken.AccessKeySecret,
- stsToken: refreshToken.SecurityToken,
- };
- },
- });
-
- //小于96mb则直接上传,否则分片上传
- const fileName = `/${await config.buildAppGetOSSFileName(param)}`;
- console.log('Start upload to ali oss');
- let returnUrl = '';
- if (fileInfo.size < 96 * 1024 * 1024) {
- const result = await client.put(fileName, uploadFile);
- returnUrl = result.url;
- } else {
- const bar1 = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
- bar1.start(100, 0);
- await aliOSSMultipartUpload(client, fileName, uploadFile, (p) => {
- bar1.update(p * 100);
- });
- bar1.stop();
- }
- console.log('Upload to ali oss done');
-
- try {
- const result = (await axiosInstance.post('/update-post', {
- type: 2,
- ossConfig: {
- ossPath: fileName,
- ossPublic: returnUrl,
- },
- uploadAppConfig: {
- updateAsNext: updateNext,
- },
- versionId,
- updateInfo,
- versionCode: versionCode
- })).data;
- console.log('上传成功');
- console.log('新更新ID: ' + result.updateId);
- } catch (e) {
- console.error('上传失败', e);
- }
- }
- }
- //Web更新与提交
- //========================================
- export async function postWebUpdate(axiosInstance, param) {
- const skipBuild = param.skip
- const noDelete = param.ndelete;
-
- const postConfig = await getConfig();
- const versionId = await selectVersion(false, postConfig.lastVersion);
- const updateInfo = await input({ message: '输入更新信息', default: postConfig.lastUpdateInfo });
- postConfig.lastVersion = versionId;
- postConfig.lastUpdateInfo = updateInfo;
- postConfig.lastTodaySubVersion++;
- await axiosInstance.post('/update-post', { config: { type: 1, test: true, versionId, uploadWebConfig: config.uploadWebConfig, submitKey: config.submitKey } });
- const now = new Date();
- const versionCode = await config.buildWebVersionGenerateCommand(now, postConfig.lastTodaySubVersion);
- if (config.buildWebOutVersionPath)
- await writeFile(path.resolve(__dirname, config.buildWebOutVersionPath), versionCode);
- await saveConfig(postConfig);
-
- if (!skipBuild && config.buildWebCommand) {
- console.log('正在执行构建...');
- await new Promise((resolve, reject) => {
- exec(config.buildWebCommand, function(err, stdout) {
- if (err)
- reject(err);
- else
- resolve();
- });
- });
- console.log('构建完成');
- }
- const distDir = path.resolve(__dirname, config.buildWebOutDir);
- try {
- await access(distDir, constants.R_OK)
- } catch {
- console.error(`Failed to access ${distDir}`);
- return;
- }
- const outputPath = __dirname + '/upload.zip';
- if (!skipBuild) {
- console.log('开始压缩zip...');
- const output = fs.createWriteStream(outputPath);
- const archive = archiver('zip', {
- zlib: { level: 9 } // Sets the compression level.
- });
- archive.pipe(output);
- const files = await readdir(distDir);
- for (const file of files) {
- const filestat = await stat(distDir + '/' + file);
- if (filestat.isDirectory()) {
- archive.directory(distDir + '/' + file, file);
- } else {
- archive.file(distDir + '/' + file, { name: file });
- }
- }
- console.log('等待压缩zip...');
- const waitArchive = new Promise((resolve, reject) => {
- output.on('close', function() {
- console.log(archive.pointer() + ' total bytes');
- console.log('archiver has been finalized and the output file descriptor has closed.');
- resolve();
- });
- archive.on('error', function(err) {
- reject(err);
- });
- })
- await archive.finalize();
- await waitArchive;
- }
- console.log('开始上传zip');
- let success = false;
- //小于8mb则小文件上传,否则分片上传
- const fileInfo = await stat(outputPath);
- const formData = new FormData();
- const submitConfig = {
- type: 1,
- versionId,
- updateInfo,
- versionCode,
- uploadWebConfig: config.uploadWebConfig,
- }
- if (fileInfo.size < 8 * 1024 * 1024) {
- const uploadZipContent = await readFile(outputPath);
- formData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
- } else {
- const multuploadInfo = (await axiosInstance.post('/update-large-token', {
- fileSize: fileInfo.size,
- fileName: path.basename(outputPath)
- })).data;
- const chunkSize = multuploadInfo.splitPartSize;
- const bar1 = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
- bar1.start(100, 0);
- for (let i = 0; i < multuploadInfo.allChunks; i++) {
- const start = i * chunkSize;
- const len = Math.min(start + chunkSize, fileInfo.size) - start;
- const uploadZipContent = await readFileRange(outputPath, start, len);
- const subFormData = new FormData();
- subFormData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
- subFormData.append("key", multuploadInfo.key);
- subFormData.append("info", JSON.stringify({}));
-
- (await axiosInstance.post('/update-large', subFormData, {
- headers: { 'Content-Type': 'multipart/form-data' }
- })).data;
-
- bar1.update(Math.floor(i / multuploadInfo.allChunks * 100));
- }
- bar1.stop();
- submitConfig.multuploadedKey = multuploadInfo.key;
- }
- try {
- formData.append("config", JSON.stringify(submitConfig));
- const result = (await axiosInstance.post('/update-post', formData, {
- headers: { 'Content-Type': 'multipart/form-data' }
- })).data;
- console.log('上传成功');
- console.log('新更新ID: ' + result.updateId);
- success = true;
- } catch (e) {
- console.error('上传失败', e);
- }
- if (!success || noDelete)
- return;
-
- console.log('删除zip');
- await unlink(outputPath);
- }
|