postUpdate.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. /**
  2. * 更新发布工具
  3. *
  4. * Copyright © 2025 imengyu.top imengyu-update-server
  5. */
  6. import { confirm , input } from '@inquirer/prompts';
  7. import { writeFile, readFile, access, unlink, readdir, stat, constants } from 'node:fs/promises';
  8. import { exec } from 'node:child_process';
  9. import fs from 'fs';
  10. import archiver from 'archiver';
  11. import path, { resolve } from 'node:path';
  12. import OSS from 'ali-oss';
  13. import cliProgress from 'cli-progress';
  14. import { selectVersion } from './index.mjs';
  15. import { config } from './postConfig.mjs';
  16. import { dirname } from 'node:path';
  17. import { fileURLToPath } from 'node:url';
  18. //基础配置
  19. //========================================
  20. const __filename = fileURLToPath(import.meta.url);
  21. const __dirname = dirname(__filename);
  22. function readFileRange(file, start, length) {
  23. return new Promise((resolve, reject) => {
  24. fs.open(file, 'r', (err, fd) => {
  25. if (err) {
  26. reject('Error opening file:', er);
  27. return;
  28. }
  29. const buffer = Buffer.alloc(length);
  30. fs.read(fd, buffer, 0, length, start, (err, bytesRead, buffer) => {
  31. if (err) {
  32. reject('Error reading file:', err)
  33. return;
  34. }
  35. fs.close(fd, (err) => {
  36. if (err) {
  37. reject('Error closing file:', err)
  38. return ;
  39. }
  40. resolve(buffer);
  41. });
  42. });
  43. });
  44. })
  45. }
  46. export function pad(num, n) {
  47. var strNum = num.toString();
  48. var len = strNum.length;
  49. while (len < n) {
  50. strNum = "0" + strNum;
  51. len++;
  52. }
  53. return strNum;
  54. }
  55. async function getConfig() {
  56. let postConfig = {
  57. lastVersion: '',
  58. lastUpdateInfo: '',
  59. lastSubmitDay: '',
  60. lastTodaySubVersion: 0,
  61. };
  62. try {
  63. postConfig = JSON.parse(await readFile(path.resolve(__dirname, './_config.json')));
  64. } catch {
  65. //
  66. }
  67. if (postConfig.lastSubmitDay != new Date().getDate())
  68. postConfig.lastTodaySubVersion = 0;
  69. return postConfig;
  70. }
  71. async function saveConfig(postConfig) {
  72. postConfig.lastSubmitDay = new Date().getDate();
  73. await writeFile(path.resolve(__dirname, './_config.json'), JSON.stringify(postConfig));
  74. }
  75. async function getUpdateInfo(postConfig) {
  76. let updateInfo = await input({ message: '输入更新信息', default: postConfig.lastUpdateInfo });
  77. if (updateInfo === 'git') {
  78. console.log('开始获取git提交信息');
  79. updateInfo = await new Promise((resolve, reject) => {
  80. exec('git log -1 --pretty=format:"%h %s"', (error, stdout, stderr) => {
  81. if (error) {
  82. reject(error);
  83. return;
  84. }
  85. resolve(stdout);
  86. });
  87. });
  88. console.log('使用git提交信息作为更新信息');
  89. }
  90. return updateInfo;
  91. }
  92. //App更新与提交
  93. //========================================
  94. export async function postAppUpdate(axiosInstance, param) {
  95. const postConfig = await getConfig();
  96. const versionId = await selectVersion(false, postConfig.lastVersion);
  97. const updateInfo = await getUpdateInfo(postConfig);
  98. await axiosInstance.post('/update-post', { config: { type: 2, test: true, versionId, uploadWebConfig: config.uploadWebConfig, submitKey: config.submitKey } });
  99. postConfig.lastVersion = versionId;
  100. postConfig.lastUpdateInfo = updateInfo;
  101. postConfig.lastTodaySubVersion++;
  102. const updateSource = (await select({
  103. choices: [
  104. {
  105. name: '重新构建',
  106. value: 'rebuild',
  107. },
  108. {
  109. name: '已上传的阿里OSS文件路径',
  110. value: 'uploaded-alioss',
  111. },
  112. ],
  113. message: '选择上传来源',
  114. default: 'rebuild',
  115. }));
  116. const force = await confirm({ message: '强制更新?', default: false });
  117. const updateNext = hotfix ? false : await confirm({ message: '作为下个版本?', default: false });
  118. const versionCode = config.buildAppGetVersion();
  119. await saveConfig(postConfig);
  120. if (updateSource === 'rebuild')
  121. await config.buildAppCallback(param, versionCode, postConfig.lastTodaySubVersion);
  122. else if (updateSource === 'uploaded-alioss') {
  123. const fileName = await input({ message: '输入已上传的阿里OSS文件路径' });
  124. try {
  125. const result = (await axiosInstance.post('/update-post', {
  126. type: 2,
  127. ossConfig: {
  128. ossPath: fileName,
  129. ossPublic: '',
  130. },
  131. uploadAppConfig: {
  132. updateAsNext: updateNext,
  133. },
  134. versionId,
  135. updateInfo,
  136. versionCode: versionCode
  137. })).data;
  138. console.log('上传成功');
  139. console.log('新更新ID: ' + result.updateId);
  140. } catch (e) {
  141. console.error('上传失败', e);
  142. }
  143. return;
  144. }
  145. else {
  146. console.error('错误的选择');
  147. return;
  148. }
  149. const uploadFile = await config.buildAppGetUploadFile(param);
  150. try {
  151. await access(uploadFile, constants.R_OK)
  152. } catch {
  153. console.error(`Failed to access ${uploadFile}, did you created it?`);
  154. return;
  155. }
  156. console.log('开始上传');
  157. //小于8mb则小文件上传,否则使用阿里OSS上传
  158. const fileInfo = await stat(uploadFile);
  159. if (fileInfo.size < 8 * 1024 * 1024) {
  160. const appData = (await readFile(uploadFile));
  161. const formData = new FormData();
  162. formData.append("file", new Blob([ appData ], { type : 'application/zip' }));
  163. formData.append("type", 2);
  164. formData.append("versionId", versionId);
  165. formData.append("updateInfo", updateInfo);
  166. formData.append("uploadAppConfig", { updateAsNext: updateNext });
  167. formData.append("force", force);
  168. formData.append("versionCode", versionCode);
  169. try {
  170. const result = (await axiosInstance.post('/update-post', formData, {
  171. headers: { 'Content-Type': 'multipart/form-data' }
  172. })).data;
  173. console.log('上传成功');
  174. console.log('新更新ID: ' + result.updateId);
  175. console.log('删除构建文件');
  176. await unlink(uploadFile);
  177. } catch (e) {
  178. console.error('上传失败', e);
  179. }
  180. } else {
  181. //阿里OSS上传
  182. //请求STS进行临时授权
  183. const stsToken = (await axiosInstance.post('/update-ali-oss-sts')).data;
  184. const client = new OSS({
  185. region: stsToken.Region,
  186. accessKeyId: stsToken.AccessKeyId,
  187. accessKeySecret: stsToken.AccessKeySecret,
  188. stsToken: stsToken.SecurityToken,
  189. bucket: stsToken.Bucket,
  190. refreshSTSToken: async () => {
  191. const refreshToken = (await axiosInstance.get("/update-ali-oss-sts")).data;
  192. return {
  193. accessKeyId: refreshToken.AccessKeyId,
  194. accessKeySecret: refreshToken.AccessKeySecret,
  195. stsToken: refreshToken.SecurityToken,
  196. };
  197. },
  198. });
  199. //小于96mb则直接上传,否则分片上传
  200. const fileName = `/${await config.buildAppGetOSSFileName(param)}`;
  201. console.log('Start upload to ali oss');
  202. let returnUrl = '';
  203. if (fileInfo.size < 96 * 1024 * 1024) {
  204. const result = await client.put(fileName, uploadFile);
  205. returnUrl = result.url;
  206. } else {
  207. const bar1 = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
  208. bar1.start(100, 0);
  209. await aliOSSMultipartUpload(client, fileName, uploadFile, (p) => {
  210. bar1.update(p * 100);
  211. });
  212. bar1.stop();
  213. }
  214. console.log('Upload to ali oss done');
  215. try {
  216. const result = (await axiosInstance.post('/update-post', {
  217. type: 2,
  218. ossConfig: {
  219. ossPath: fileName,
  220. ossPublic: returnUrl,
  221. },
  222. uploadAppConfig: {
  223. updateAsNext: updateNext,
  224. },
  225. versionId,
  226. updateInfo,
  227. versionCode: versionCode
  228. })).data;
  229. console.log('上传成功');
  230. console.log('新更新ID: ' + result.updateId);
  231. } catch (e) {
  232. console.error('上传失败', e);
  233. }
  234. }
  235. }
  236. //Web更新与提交
  237. //========================================
  238. export async function postWebUpdate(axiosInstance, param) {
  239. const skipBuild = param.skip
  240. const noDelete = param.ndelete;
  241. const postConfig = await getConfig();
  242. const versionId = await selectVersion(false, postConfig.lastVersion);
  243. const updateInfo = await getUpdateInfo(postConfig);
  244. postConfig.lastVersion = versionId;
  245. postConfig.lastUpdateInfo = updateInfo;
  246. postConfig.lastTodaySubVersion++;
  247. await axiosInstance.post('/update-post', { config: { type: 1, test: true, versionId, uploadWebConfig: config.uploadWebConfig, submitKey: config.submitKey } });
  248. const now = new Date();
  249. const versionCode = await config.buildWebVersionGenerateCommand(now, postConfig.lastTodaySubVersion);
  250. if (config.buildWebOutVersionPath)
  251. await writeFile(path.resolve(__dirname, config.buildWebOutVersionPath), versionCode);
  252. await saveConfig(postConfig);
  253. if (!skipBuild && config.buildWebCommand) {
  254. console.log('正在执行构建...');
  255. await new Promise((resolve, reject) => {
  256. exec(config.buildWebCommand, function(err, stdout) {
  257. if (err)
  258. reject(err);
  259. else
  260. resolve();
  261. });
  262. });
  263. console.log('构建完成');
  264. }
  265. const distDir = path.resolve(__dirname, config.buildWebOutDir);
  266. try {
  267. await access(distDir, constants.R_OK)
  268. } catch {
  269. console.error(`Failed to access ${distDir}`);
  270. return;
  271. }
  272. const outputPath = __dirname + '/upload.zip';
  273. if (!skipBuild) {
  274. console.log('开始压缩zip...');
  275. const output = fs.createWriteStream(outputPath);
  276. const archive = archiver('zip', {
  277. zlib: { level: 9 } // Sets the compression level.
  278. });
  279. archive.pipe(output);
  280. const files = await readdir(distDir);
  281. for (const file of files) {
  282. const filestat = await stat(distDir + '/' + file);
  283. if (filestat.isDirectory()) {
  284. archive.directory(distDir + '/' + file, file);
  285. } else {
  286. archive.file(distDir + '/' + file, { name: file });
  287. }
  288. }
  289. console.log('等待压缩zip...');
  290. const waitArchive = new Promise((resolve, reject) => {
  291. output.on('close', function() {
  292. console.log(archive.pointer() + ' total bytes');
  293. console.log('archiver has been finalized and the output file descriptor has closed.');
  294. resolve();
  295. });
  296. archive.on('error', function(err) {
  297. reject(err);
  298. });
  299. })
  300. await archive.finalize();
  301. await waitArchive;
  302. }
  303. console.log('开始上传zip');
  304. let success = false;
  305. //小于8mb则小文件上传,否则分片上传
  306. const fileInfo = await stat(outputPath);
  307. const formData = new FormData();
  308. const submitConfig = {
  309. type: 1,
  310. versionId,
  311. updateInfo,
  312. versionCode,
  313. uploadWebConfig: config.uploadWebConfig,
  314. }
  315. if (fileInfo.size < 8 * 1024 * 1024) {
  316. const uploadZipContent = await readFile(outputPath);
  317. formData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
  318. } else {
  319. const multuploadInfo = (await axiosInstance.post('/update-large-token', {
  320. fileSize: fileInfo.size,
  321. fileName: path.basename(outputPath)
  322. })).data;
  323. const chunkSize = multuploadInfo.splitPartSize;
  324. const bar1 = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
  325. bar1.start(100, 0);
  326. for (let i = 0; i < multuploadInfo.allChunks; i++) {
  327. const start = i * chunkSize;
  328. const len = Math.min(start + chunkSize, fileInfo.size) - start;
  329. const uploadZipContent = await readFileRange(outputPath, start, len);
  330. const subFormData = new FormData();
  331. subFormData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
  332. subFormData.append("key", multuploadInfo.key);
  333. subFormData.append("info", JSON.stringify({}));
  334. (await axiosInstance.post('/update-large', subFormData, {
  335. headers: { 'Content-Type': 'multipart/form-data' }
  336. })).data;
  337. bar1.update(Math.floor(i / multuploadInfo.allChunks * 100));
  338. }
  339. bar1.stop();
  340. submitConfig.multuploadedKey = multuploadInfo.key;
  341. }
  342. try {
  343. formData.append("config", JSON.stringify(submitConfig));
  344. const result = (await axiosInstance.post('/update-post', formData, {
  345. headers: { 'Content-Type': 'multipart/form-data' }
  346. })).data;
  347. console.log('上传成功');
  348. console.log('新更新ID: ' + result.updateId);
  349. success = true;
  350. } catch (e) {
  351. console.error('上传失败', e);
  352. }
  353. if (!success || noDelete)
  354. return;
  355. console.log('删除zip');
  356. await unlink(outputPath);
  357. }