UNPKG

electron-auto-updater-enhanced

Version:

Enhanced auto-updater for Electron applications

604 lines (554 loc) 20.5 kB
const { initAutoUpdater } = require('./autoUpdater'); const { app, dialog,net } = require('electron'); const path = require('path'); const fs = require('fs'); const fse = require('fs-extra'); const crypto = require('crypto'); const decompress = require('decompress'); const { EventEmitter } = require('events'); class ElectronAutoUpdater { constructor(options = {}) { this.mainWindow = options.mainWindow; this.logPath = options.logPath; this.md5Value = ""; this.isSilent = false; this.silentZipPath = ""; this.updateInProgress = false; this.emitter = new EventEmitter(); // 初始化autoUpdater this.checkForUpdates = initAutoUpdater(this.mainWindow).checkForUpdates; } // 检查PC版本更新 checkForUpdateVersion(arg) { try { if (!fse || !arg) { this.checkForUpdates(); return; } if (!fse.pathExistsSync(this.logPath)) { this.emitter.emit('auto-update-log', { type: "error", message:"Log path not found: " + this.logPath }); this.checkForUpdates(); return; } this.requestPcVersion(arg); } catch (error) { this.emitter.emit('auto-update-log', { type: "error", message:"Error in checkForUpdateVersion:" + error }); this.checkForUpdates(); } } // 请求PC版本信息 requestPcVersion(arg) { const baseUrl = arg.endsWith('/') ? arg : arg + '/'; const updateUrl = new URL('pcVersion.json', baseUrl).href; const request = net.request(updateUrl); request.on('response', (response) => { let data = ''; response.on('data', (chunk) => { data += chunk; }); response.on('end', () => { try { const versionData = JSON.parse(data); const unpackedZipPath = path.join(app.getPath("userData"), "unpacked.zip"); this.handleVersionResponse(versionData, unpackedZipPath, arg); } catch (err) { this.emitter.emit('auto-update-log', { type: "error", message: "Failed to parse JSON: " + err.message }); this.checkForUpdates(); } }); }); request.on('error', (error) => { this.emitter.emit('auto-update-log', { type: "error", message: "Request failed: " + error.message }); }); request.end(); } // 处理版本响应 handleVersionResponse(data, unpackedZipPath, updatePath) { const remoteVersion = data.version.split("."); const localVersion = app.getVersion().toString().split("."); const num = Math.random(); if (this.isNewMajorVersion(remoteVersion, localVersion)) { if (data.percentage && num <= data.percentage) { this.emitter.emit('auto-update-log', { type: "info", message:"autoUpdaterBigVersionStart" }); this.checkForUpdates(); } return; } if ( this.isNewMinorVersion(remoteVersion, localVersion) && data.percentage && num <= data.percentage ) { this.handleMinorUpdate(data, unpackedZipPath, updatePath); } } // 检查是否是新的主版本 isNewMajorVersion(remoteVersion, localVersion) { try { const remote0 = parseInt(remoteVersion[0]) || 0; const remote1 = parseInt(remoteVersion[1]) || 0; const local0 = parseInt(localVersion[0]) || 0; const local1 = parseInt(localVersion[1]) || 0; return remote0 > local0 || (remote0 === local0 && remote1 > local1); } catch (error) { this.emitter.emit('auto-update-log', { type: "error", message:"Error in isNewMajorVersion comparison: " + error }); return false; } } // 检查是否是新的次要版本 isNewMinorVersion(remoteVersion, localVersion) { try { const remote0 = parseInt(remoteVersion[0]) || 0; const remote1 = parseInt(remoteVersion[1]) || 0; const remote2 = parseInt(remoteVersion[2]) || 0; const local0 = parseInt(localVersion[0]) || 0; const local1 = parseInt(localVersion[1]) || 0; const local2 = parseInt(localVersion[2]) || 0; return remote0 === local0 && remote1 === local1 && remote2 > local2; } catch (error) { this.emitter.emit('auto-update-log', { type: "error", message:"Error in isNewMinorVersion comparison: " + error }); return false; } } // 处理小版本更新 handleMinorUpdate(data, unpackedZipPath, updatePath) { let fileContent = fs.readFileSync(this.logPath, "utf8"); if ( fileContent.split("autoUpdaterSmallVersion" + data.version).length > 3 ) { this.emitter.emit('auto-update-log', { type: "info", message:"autoUpdaterSmallVersionNumber:" + fileContent.split("autoUpdaterSmallVersion" + data.version).length }); this.checkForUpdates(); return; } this.emitter.emit('auto-update-log', { type: "info", message:"autoUpdaterSmallVersion" + data.version }); this.md5Value = data.md5; if (data.isSilent === true) { this.emitter.emit('auto-update-log', { type: "info", message:"autoUpdaterSmallVersionSilent" }); this.isSilent = true; } if (fse.pathExistsSync(unpackedZipPath)) { this.removeFile(unpackedZipPath); } this.autoUpdaterSmallVersion(data.updatePath, updatePath); } // 小版本更新处理 autoUpdaterSmallVersion(customPath, updatePath) { const unpackedZipPath = path.join( app.getPath("userData"), "./unpacked.zip" ); if (!fse.pathExistsSync(unpackedZipPath)) { const url = customPath ? customPath : updatePath + "/unpacked.zip"; this.emitter.emit('auto-update-log', { type: "info", message:"unpackedZipPath:" + url }); this.downloadFile({ url, targetPath: app.getPath("userData"), }) .then((filePath) => this.handleDownloadComplete(filePath)) .catch((err) => { this.emitter.emit('auto-update-log', { type: "error", message:"downloadFileErr:" + err }); this.checkForUpdates(); }); } } // 处理下载完成 handleDownloadComplete(filePath) { this.calculateMD5(filePath) .then((md5ValueLocal) => { if (!this.md5Value || this.md5Value !== md5ValueLocal) { this.emitter.emit('auto-update-log', { type: "info", message:"MD5 check failed: " + this.md5Value + " != " + md5ValueLocal }); this.removeFile(filePath); this.checkForUpdates(); } else { return this.handleValidMD5(filePath); } }) .catch((md5Error) => { this.emitter.emit('auto-update-log', { type: "error", message:"MD5 calculation failed: " + md5Error.message }); this.removeFile(filePath); this.checkForUpdates(); }); } // 处理有效的MD5 handleValidMD5(filePath) { if (!this.isSilent) { this.sendUpdateMessage({ cmd: "small-update-downloaded", message: "", }); return this.showUpdateDialog(filePath); } this.silentZipPath = filePath; } // 显示更新对话框 showUpdateDialog(filePath) { return dialog .showMessageBox(this.mainWindow, { title: "提示", message: "点击确定自动完成更新", buttons: ["确定"], }) .then(() => { this.emitter.emit('auto-update-log', { type: "info", message:"updateDownloadFileStart" }); return this.updateDownloadFile(filePath); }) .then(() => { this.reLoad(true); }) .catch((err) => { this.emitter.emit('auto-update-log', { type: "error", message:"Failed to update:"+err.message }); if (err.appPath) { this.reduction(err.appPath); } }); } // 更新下载文件 updateDownloadFile(filePath) { this.emitter.emit('auto-update-log', { type: "info", message:"downloadFilePath:" + filePath }); return new Promise((resolve, reject) => { const resourcesPath = process.resourcesPath; const appPath = path.join(resourcesPath, "./app.asar.unpacked"); try { this.backupCurrentVersion(appPath); this.extractAndCleanup(filePath, appPath, resolve, reject); } catch (e) { reject({ appPath: appPath, message: e.message || "", }); } }); } // 备份当前版本 backupCurrentVersion(appPath) { if (fse.pathExistsSync(appPath + ".back")) { fse.removeSync(appPath + ".back"); } if (fse.pathExistsSync(appPath)) { this.emitter.emit('auto-update-log', { type: "info", message:"backupsPath" + appPath }); fse.moveSync(appPath, appPath + ".back"); } } // 解压并清理 extractAndCleanup(filePath, appPath, resolve, reject) { decompress(filePath, appPath) .then(() => { this.emitter.emit('auto-update-log', { type: "info", message:"Files extracted successfully to: " + appPath }); return fse.remove(filePath); }) .then(() => { this.emitter.emit('auto-update-log', { type: "info", message:"Old ZIP file removed: " + filePath }); resolve(); }) .catch((err) => { this.emitter.emit('auto-update-log', { type: "error", message:"extractAllToAsync:" + err }); this.sendUpdateMessage({ cmd: "error", message: "Failed to extract files: " + err.message, }); reject({ appPath: appPath, message: err.message || "", }); }); } // 计算MD5值 calculateMD5(filePath) { return new Promise((resolve, reject) => { const md5 = crypto.createHash("md5"); const stream = fs.createReadStream(filePath); stream.on("data", (chunk) => md5.update(chunk)); stream.on("end", () => { const hex = md5.digest("hex"); resolve(hex); }); stream.on("error", (err) => { reject(err); }); }); } // 版本回退 reduction(targetPath) { this.emitter.emit('auto-update-log', { type: "info", message:"reductionPath:" + targetPath }); try { if (fse.pathExistsSync(targetPath + ".back")) { fse.moveSync(targetPath + ".back", targetPath, { overwrite: true }); } else { throw new Error("backPath not find"); } this.reLoad(false); } catch (error) { dialog .showMessageBox(this.mainWindow, { title: "提示", message: "当前电脑无可回退版本", buttons: ["确定"], }) .then((index) => { }); } } // 重新加载应用 reLoad(close) { this.emitter.emit('auto-update-log', { type: "info", message:"reLoad:" + close }); if (close) { app.relaunch(); app.exit(0); } else { this.mainWindow.webContents.reloadIgnoringCache(); } } // 下载文件 downloadFile({ url, targetPath, folder = "./" }) { if (!targetPath || !url) { throw new Error("targetPath or url is nofind"); } try { if (!this.isSilent) { this.sendUpdateMessage({ cmd: "update-available", message: "下载中,请不要关闭系统", }); } fse.ensureDirSync(path.join(targetPath, folder)); return this.startDownload(url, targetPath, folder); } catch (error) { throw new Error(error); } } // 开始下载 startDownload(url, targetPath, folder) { return new Promise((resolve, reject) => { const name = url.split("/").pop(); const filePath = path.join(targetPath, folder, name); this.download(url, filePath, (status, result) => { this.handleDownloadStatus(status, result, filePath, resolve, reject); }); }); } // 处理下载状态 handleDownloadStatus(status, result, filePath, resolve, reject) { switch (status) { case "completed": resolve(filePath); break; case "error": this.emitter.emit('auto-update-log', { type: "error", message:"netWork error2" }); reject(result); break; case "progressing": if (!this.isSilent) { this.sendUpdateMessage({ cmd: "download-progress", message: { percent: result, }, }); } break; } } // 下载实现 download(url, targetPath, cb = () => { }) { let status; let len = 0; let cur = 0; try { const request = net.request(encodeURI(url)); const stream = fs.createWriteStream(targetPath); // 管道输出到文件 request.on('response', (response) => { // 获取 content-length len = parseInt(response.headers['content-length'] || 0, 10); // 开始传输数据 response.on('data', (chunk) => { cur += chunk.length; const progress = ((100 * cur) / len).toFixed(2); status = 'progressing'; cb(status, progress); }); // 数据传输结束 response.on('end', () => { stream.close(); // 调用你原有的结束处理函数 this.handleDownloadEnd(request, len, cur, stream, targetPath, status, cb); }); // 管道数据到文件流 response.pipe(stream); }); // 请求级别错误(如 DNS、连接失败) request.on('error', (e) => { // 调用你原有的错误处理函数 this.handleDownloadError(e, stream, targetPath, len, cur, status, cb); }); // 发起请求 request.end(); } catch (error) { this.emitter.emit('auto-update-log', { type: "error", message: "Download error2:" + error.message }); } } // 处理下载结束 handleDownloadEnd(req, len, cur, stream, targetPath, status, cb) { if (req.response.statusCode === 200) { if (len === cur) { this.emitter.emit('auto-update-log', { type: "info", message:"Download complete:" + targetPath }); status = "completed"; cb(status, 100); } else { stream.end(); this.removeFile(targetPath); status = "error"; cb(status, "网络波动,下载文件不全"); } } else { this.emitter.emit('auto-update-log', { type: "info", message:"Download error0:" + req.response.statusMessage }); stream.end(); this.removeFile(targetPath); status = "error"; cb(status, req.response.statusMessage); } } // 处理下载错误 handleDownloadError(e, stream, targetPath, len, cur, status, cb) { this.emitter.emit('auto-update-log', { type: "info", message:"Download error1:" + e }); try { stream.end(); stream.destroy(); } catch (streamError) { this.emitter.emit('auto-update-log', { type: "error", message:"Error closing stream: " + streamError }); } try { this.removeFile(targetPath); } catch (removeError) { this.emitter.emit('auto-update-log', { type: "error", message:"Error removing file: " + removeError }); } if (len !== cur) { status = "error"; cb(status, "网络波动,下载失败"); } else { status = "error"; cb(status, e); } } // 删除文件 removeFile(targetPath) { if (!targetPath) { this.emitter.emit('auto-update-log', { type: "info", message:"removeFile: No target path provided" }); return; } this.emitter.emit('auto-update-log', { type: "info", message:"removeFile:" + targetPath }); try { if (fse.pathExistsSync(targetPath)) { fse.removeSync(targetPath); } } catch (error) { this.emitter.emit('auto-update-log', { type: "error", message:"removeSync error:" + error }); throw error; // 向上传播错误,让调用者处理 } } // 辅助方法 sendUpdateMessage(message) { if (this.mainWindow && this.mainWindow.webContents) { this.mainWindow.webContents.send("message", message); } } } module.exports = ElectronAutoUpdater;