| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- /**
- * 更新发布工具
- *
- * 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 './index.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 updateInfo = await input({ message: '输入更新信息', default: postConfig.lastUpdateInfo });
- if (updateInfo === 'git') {
- console.log('开始获取git提交信息');
- updateInfo = await execAsync('git log -1 --pretty=format:"%h %s"');
- console.log('使用git提交信息 "' + updateInfo + '" 作为更新信息');
- }
- return updateInfo;
- }
- async function uploadMulitPartLarge(axiosInstance, fileInfo, filePath) {
- console.log('开始分片上传');
- const multuploadInfo = (await axiosInstance.post('/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-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(false, postConfig.lastVersion);
- const updateInfo = await getUpdateInfo(postConfig);
- const serverConfig = await axiosInstance.post('/update-post', {
- config: {
- type: 2,
- test: true,
- versionId,
- uploadAppConfig: {},
- 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 = 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-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-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-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则直接上传,否则分片上传
- 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-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(false, postConfig.lastVersion);
- const updateInfo = await getUpdateInfo(postConfig);
- 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 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-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);
- }
|