|
|
@@ -0,0 +1,423 @@
|
|
|
+/**
|
|
|
+ * 更新发布工具
|
|
|
+ *
|
|
|
+ * Copyright © 2025 imengyu.top imengyu-update-server
|
|
|
+ */
|
|
|
+
|
|
|
+import { confirm, select, input } from '@inquirer/prompts';
|
|
|
+import { writeFile, readFile, access, unlink, stat, constants } from 'node:fs/promises';
|
|
|
+import path from 'node:path';
|
|
|
+import OSS from 'ali-oss';
|
|
|
+import cliProgress from 'cli-progress';
|
|
|
+import { selectVersion } from './app.mjs';
|
|
|
+import { config } from './postConfig.mjs';
|
|
|
+import { readFileRange, compressZip, execAsync } from './utils.mjs';
|
|
|
+import { dirname } from 'node:path';
|
|
|
+import { fileURLToPath } from 'node:url';
|
|
|
+
|
|
|
+//基础配置
|
|
|
+//========================================
|
|
|
+
|
|
|
+const __filename = fileURLToPath(import.meta.url);
|
|
|
+const __dirname = dirname(__filename);
|
|
|
+
|
|
|
+const UPLOAD_APP_TYPE_LOCAL = 1;
|
|
|
+const UPLOAD_APP_TYPE_ALI_OSS = 2;
|
|
|
+
|
|
|
+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));
|
|
|
+}
|
|
|
+
|
|
|
+async function getUpdateInfo(postConfig) {
|
|
|
+ let lastUseGit = false;
|
|
|
+ let updateInfo = await input({ message: '输入更新信息', default: postConfig.lastUpdateInfo });
|
|
|
+ if (updateInfo === 'git') {
|
|
|
+ console.log('开始获取git提交信息');
|
|
|
+ lastUseGit = true;
|
|
|
+ updateInfo = await execAsync('git log -1 --pretty=format:"%h %s"');
|
|
|
+ console.log('使用git提交信息 "' + updateInfo + '" 作为更新信息');
|
|
|
+ }
|
|
|
+ return { updateInfo, lastUseGit };
|
|
|
+}
|
|
|
+export async function uploadMulitPartLarge(axiosInstance, fileInfo, filePath) {
|
|
|
+
|
|
|
+ console.log('开始分片上传');
|
|
|
+
|
|
|
+ const multuploadInfo = (await axiosInstance.post('/update/update-large-token', {
|
|
|
+ fileSize: fileInfo.size,
|
|
|
+ fileName: path.basename(filePath)
|
|
|
+ })).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(filePath, 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/update-large', subFormData, {
|
|
|
+ headers: { 'Content-Type': 'multipart/form-data' }
|
|
|
+ })).data;
|
|
|
+
|
|
|
+ bar1.update(Math.floor(i / multuploadInfo.allChunks * 100));
|
|
|
+ }
|
|
|
+
|
|
|
+ bar1.update(100);
|
|
|
+ bar1.stop();
|
|
|
+
|
|
|
+ return multuploadInfo.key;
|
|
|
+}
|
|
|
+async function aliOSSMultipartUpload(client, fileName, uploadFile, progressCallback) {
|
|
|
+
|
|
|
+ const fileInfo = await stat(uploadFile); // 获取文件信息
|
|
|
+ const partSize = 10 * 1024 * 1024; // 分片大小,这里使用10MB
|
|
|
+ const partCount = Math.ceil(fileInfo.size / partSize); // 计算总分片数
|
|
|
+
|
|
|
+ console.log(`开始分片上传到阿里OSS,文件大小: ${(fileInfo.size / 1024 / 1024).toFixed(2)}MB,分片数: ${partCount}`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 初始化分片上传
|
|
|
+ const multipartUpload = await client.initMultipartUpload(fileName);
|
|
|
+ const uploadId = multipartUpload.uploadId;
|
|
|
+
|
|
|
+ // 上传分片
|
|
|
+ const parts = [];
|
|
|
+ for (let i = 0; i < partCount; i++) {
|
|
|
+ const start = i * partSize;
|
|
|
+ const len = Math.min(partSize, fileInfo.size - start);
|
|
|
+
|
|
|
+ // 读取文件分片
|
|
|
+ const fileData = await readFileRange(uploadFile, start, len);
|
|
|
+
|
|
|
+ // 上传分片
|
|
|
+ const partResult = await client.uploadPart(fileName, fileData, uploadId, i + 1);
|
|
|
+ parts.push({
|
|
|
+ ETag: partResult.res.headers.etag,
|
|
|
+ PartNumber: i + 1
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算并回调进度
|
|
|
+ const progress = (i + 1) / partCount;
|
|
|
+ if (progressCallback) {
|
|
|
+ progressCallback(progress);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`已上传分片 ${i + 1}/${partCount} (${(progress * 100).toFixed(2)}%)`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 完成分片上传
|
|
|
+ await client.completeMultipartUpload(fileName, uploadId, parts);
|
|
|
+
|
|
|
+ console.log('阿里OSS分片上传完成');
|
|
|
+
|
|
|
+ // 获取上传后的文件URL
|
|
|
+ const url = await client.generateObjectUrl(fileName);
|
|
|
+ return url;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('阿里OSS分片上传失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+//App更新与提交
|
|
|
+//========================================
|
|
|
+
|
|
|
+export async function postAppUpdate(axiosInstance, param) {
|
|
|
+ const postConfig = await getConfig();
|
|
|
+ const { versionId, versionName } = await selectVersion(postConfig.lastVersion);
|
|
|
+ const { updateInfo, lastUseGit } = await getUpdateInfo(postConfig);
|
|
|
+
|
|
|
+ const serverConfig = await axiosInstance.post('/update/update-post', {
|
|
|
+ config: {
|
|
|
+ type: 2,
|
|
|
+ test: true,
|
|
|
+ versionId,
|
|
|
+ uploadAppConfig: {},
|
|
|
+ submitKey: config.submitKey
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ postConfig.lastVersion = versionId;
|
|
|
+ postConfig.lastUpdateInfo = lastUseGit ? 'git': 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 = await confirm({ message: '作为下个版本?', default: false });
|
|
|
+ const versionCode = await config.buildAppGetVersion(versionName);
|
|
|
+
|
|
|
+ await saveConfig(postConfig);
|
|
|
+
|
|
|
+ if (updateSource === 'rebuild')
|
|
|
+ await config.buildAppCallback(param, versionCode, versionName, postConfig.lastTodaySubVersion);
|
|
|
+ else if (updateSource === 'uploaded-alioss') {
|
|
|
+ const fileName = await input({ message: '输入已上传的阿里OSS文件路径' });
|
|
|
+ try {
|
|
|
+ const result = (await axiosInstance.post('/update/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, versionCode, versionName);
|
|
|
+ const fileName = `${await config.buildAppGetOSSFileName(param, versionCode, path.basename(uploadFile), uploadFile)}`;
|
|
|
+ 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("config", JSON.stringify({
|
|
|
+ type: 2,
|
|
|
+ versionId,
|
|
|
+ versionCode,
|
|
|
+ updateInfo,
|
|
|
+ updateCustomConfig: await config.getCustomConfig(param, true, versionName),
|
|
|
+ uploadAppConfig: { updateAsNext: updateNext },
|
|
|
+ force,
|
|
|
+ fileName,
|
|
|
+ }));
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = (await axiosInstance.post('/update/update-post', formData, {
|
|
|
+ headers: { 'Content-Type': 'multipart/form-data' }
|
|
|
+ })).data;
|
|
|
+ console.log('上传成功');
|
|
|
+ console.log('新更新ID: ' + result.updateId);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('上传失败', e);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+
|
|
|
+ let ossConfig;
|
|
|
+ let multuploadedKey;
|
|
|
+
|
|
|
+ if (serverConfig.uploadAppType === UPLOAD_APP_TYPE_LOCAL) {
|
|
|
+ //本地大文件上传
|
|
|
+ multuploadedKey = await uploadMulitPartLarge(axiosInstance, fileInfo, uploadFile);
|
|
|
+ } else if (serverConfig.uploadAppType === UPLOAD_APP_TYPE_ALI_OSS) {
|
|
|
+ //阿里OSS上传
|
|
|
+ //请求STS进行临时授权
|
|
|
+ const stsToken = (await axiosInstance.post('/update/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/update-ali-oss-sts")).data;
|
|
|
+ return {
|
|
|
+ accessKeyId: refreshToken.AccessKeyId,
|
|
|
+ accessKeySecret: refreshToken.AccessKeySecret,
|
|
|
+ stsToken: refreshToken.SecurityToken,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ //小于96mb则直接上传,否则分片上传
|
|
|
+
|
|
|
+ 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);
|
|
|
+ returnUrl = await aliOSSMultipartUpload(client, fileName, uploadFile, (p) => {
|
|
|
+ bar1.update(p * 100);
|
|
|
+ });
|
|
|
+ bar1.update(100);
|
|
|
+ bar1.stop();
|
|
|
+ }
|
|
|
+
|
|
|
+ ossConfig = {
|
|
|
+ ossPath: fileName,
|
|
|
+ ossPublic: returnUrl,
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('Upload to ali oss done');
|
|
|
+ } else {
|
|
|
+ console.error('错误的 uploadAppType 配置 ' + serverConfig.uploadAppType);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = (await axiosInstance.post('/update/update-post', {
|
|
|
+ config: {
|
|
|
+ type: 2,
|
|
|
+ ossConfig,
|
|
|
+ multuploadedKey,
|
|
|
+ uploadAppConfig: {
|
|
|
+ updateAsNext: updateNext,
|
|
|
+ },
|
|
|
+ updateCustomConfig: await config.getCustomConfig(param, true, versionName),
|
|
|
+ versionId,
|
|
|
+ updateInfo,
|
|
|
+ versionCode: versionCode,
|
|
|
+ fileName
|
|
|
+ },
|
|
|
+ })).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, versionName } = await selectVersion(postConfig.lastVersion);
|
|
|
+ const { updateInfo, lastUseGit } = await getUpdateInfo(postConfig);
|
|
|
+
|
|
|
+ postConfig.lastVersion = versionId;
|
|
|
+ postConfig.lastUpdateInfo = lastUseGit ? 'git': updateInfo;
|
|
|
+ postConfig.lastTodaySubVersion++;
|
|
|
+
|
|
|
+ await axiosInstance.post('/update/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 execAsync(config.buildWebCommand);
|
|
|
+ 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';
|
|
|
+ const skipFiles = config?.buildWebOptions?.skipFiles ?? [];
|
|
|
+
|
|
|
+ if (!skipBuild) {
|
|
|
+ console.log('开始压缩zip...');
|
|
|
+ await compressZip(distDir, outputPath, skipFiles);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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,
|
|
|
+ updateCustomConfig: await config.getCustomConfig(param, true, versionName),
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fileInfo.size < 8 * 1024 * 1024) {
|
|
|
+ const uploadZipContent = await readFile(outputPath);
|
|
|
+ formData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
|
|
|
+ } else {
|
|
|
+ submitConfig.multuploadedKey = await uploadMulitPartLarge(axiosInstance, fileInfo, outputPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ formData.append("config", JSON.stringify(submitConfig));
|
|
|
+ const result = (await axiosInstance.post('/update/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);
|
|
|
+}
|