electron-auto-updater-enhanced
Version:
Enhanced auto-updater for Electron applications
604 lines (554 loc) • 20.5 kB
JavaScript
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;