"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const node_child_process = require("node:child_process"); const node_process = require("node:process"); const node_path = require("node:path"); const node_util = require("node:util"); const node_stream = require("node:stream"); const node_zlib = require("node:zlib"); const node_https = require("node:https"); const node_http = require("node:http"); const node_crypto = require("node:crypto"); const originalFs = require("original-fs"); const rmPromisify = node_util.promisify(originalFs.rm); const renamePromisify = node_util.promisify(originalFs.rename); const accessPromisify = node_util.promisify(originalFs.access); const writeFilePromisify = node_util.promisify(originalFs.writeFile); const readFilePromisify = node_util.promisify(originalFs.readFile); const asarSwapFileName = "swap.asar"; const asarNextFileName = "next.asar"; const asarVersionFileName = "version.json"; const asarTargetFileName = "app.asar"; async function exists(p, mode) { try { await accessPromisify(p, mode); return true; } catch (err) { return false; } } class HashTransform extends node_stream.Transform { #hash; #result = null; constructor(algorithm, options) { super(); this.#hash = node_crypto.createHash(algorithm, options); } _transform(chunk, _, next) { this.#hash.update(chunk); next(null, chunk); } _flush(done) { this.#result = this.#hash.digest("hex").toLowerCase(); done(); } get hash() { return this.#result; } } function parserVersion(ver) { let build = 0; if (ver.includes("-")) { let buildstr = "0"; [ver, buildstr] = ver.split("-"); build = parseInt(buildstr, 10); } const [major, minor, patch = 0] = ver.split(".").map((item) => parseInt(item, 10)); return [major, minor, patch, build]; } function compareVersions(updateVersionStr, currentVersionStr) { const appVersion = parserVersion(currentVersionStr); const updateVersion = parserVersion(updateVersionStr); for (let i = 0; i < 4; i++) { if (updateVersion[i] !== appVersion[i]) return updateVersion[i] > appVersion[i]; } return false; } function pipe(stream, ...streams) { return new Promise(function(resolve2, reject) { stream.on("error", reject); for (const nextStream of streams) { stream = stream.pipe(nextStream); stream.on("error", reject); } stream.on("finish", resolve2); }); } const headers = { "user-agent": "@zeromake/electron-asar-updater/1.0.0", accept: "*/*" }; function http_get_json(url) { return new Promise((resolve2, reject) => { const req = (url.startsWith("https://") ? node_https.request : node_http.request)( url, { headers: { ...headers, accept: "application/json" }, timeout: 5e3, method: "GET" }, (res) => { if (res.statusCode !== 200) { res.resume(); return reject(new Error(`Failed to fetch ${url}: ${res.statusCode}`)); } res.setEncoding("utf8"); const data = []; res.on("data", (chunk) => { data.push(chunk); }); res.on("end", () => { try { resolve2(JSON.parse(data.join())); } catch (err) { reject(err); } }); } ).on("error", reject); req.end(); }); } function http_get_stream(url) { return new Promise((resolve2, reject) => { const req = (url.startsWith("https://") ? node_https.request : node_http.request)( url, { headers: { ...headers, accept: "application/octet-stream" }, method: "GET" }, (res) => { if (res.statusCode !== 200) { res.resume(); return reject(new Error(`Failed to fetch ${url}: ${res.statusCode}`)); } resolve2(res); } ).on("error", reject); req.end(); }); } async function generateWinScript(resourcePath, scriptPath, execPath) { let relaunch = false; if (execPath && await exists(node_path.join(".", execPath))) { relaunch = true; } await writeFilePromisify( scriptPath, ` On Error Resume Next Dim relaunch relaunch = wscript.arguments(0) Set wshShell = WScript.CreateObject("WScript.Shell") Set fsObject = WScript.CreateObject("Scripting.FileSystemObject") updaterPath = "${node_path.join(resourcePath, asarNextFileName)}" destPath = "${node_path.join(resourcePath, asarTargetFileName)}" Do While fsObject.FileExists(destPath) fsObject.DeleteFile destPath WScript.Sleep 250 Loop WScript.Sleep 250 fsObject.MoveFile updaterPath,destPath WScript.Sleep 250 If relaunch = "1" Then ${relaunch ? 'wshShell.Run ".\\' + execPath + '"' : ""} End If `, { encoding: "utf8" } ); } class AsarUpdater { /** * 检查版本号的接口地址 */ checkVersionUrl; /** * 检查版本号的接口地址 */ downloadHost; appPathFolder; asarNextPath; asarSwapPath; asarTargetPath; asarVersionPath; resourcePath; execPath; /** * @param options 选项 */ constructor(options) { this.checkVersionUrl = options.version_url; this.downloadHost = new URL(options.version_url).origin; const resource_path = options.resource_path.endsWith(asarTargetFileName) ? options.resource_path.slice(0, -asarTargetFileName.length) : options.resource_path; this.appPathFolder = resource_path.endsWith(node_path.sep) ? resource_path.slice(0, -1) : resource_path; this.asarNextPath = node_path.join(this.appPathFolder, asarNextFileName); this.asarSwapPath = node_path.join(this.appPathFolder, asarSwapFileName); this.asarTargetPath = node_path.join(this.appPathFolder, asarTargetFileName); this.asarVersionPath = node_path.join(this.appPathFolder, asarVersionFileName); this.resourcePath = node_path.relative(node_path.resolve("."), this.appPathFolder); if (options.exec_path) { this.execPath = node_path.relative(node_path.resolve("."), options.exec_path); } } /** * 检查是否有更新 * @param curentVersion 当前软件版本号 * @returns [是否需要更新,文件信息] */ async check(curentVersion) { const data = await http_get_json(this.checkVersionUrl); let isUpdater = compareVersions(data.version, curentVersion); let downloadUrl = ""; if (!data.download_url.startsWith("http://") && !data.download_url.startsWith("https://")) { if (!data.download_url.startsWith("/")) { downloadUrl = `${this.checkVersionUrl.slice(0, this.checkVersionUrl.lastIndexOf("/"))}/${data.download_url}`; } else { downloadUrl = `${this.downloadHost}${data.download_url}`; } } else { downloadUrl = data.download_url; } if (await exists(this.asarVersionPath)) { const nextUpdaterInfo = JSON.parse( await readFilePromisify(this.asarVersionPath, { encoding: "utf8" }) ); if (nextUpdaterInfo.checksum && nextUpdaterInfo.checksum === data.checksum) { isUpdater = false; } } return [ isUpdater, { ...data, download_url: downloadUrl } ]; } /** * 下载asar * @param asarInfo asar信息 */ async download(asarInfo) { for (const file of [this.asarNextPath, this.asarSwapPath, this.asarVersionPath]) { if (await exists(file)) { await rmPromisify(file); } } const { download_url, checksum } = asarInfo; const inStream = await http_get_stream(download_url); const isGzip = download_url.endsWith(".gz") || download_url.endsWith(".gzip"); const outStream = originalFs.createWriteStream(this.asarSwapPath, { encoding: "binary", flags: "w" }); const outputStreams = []; let hashTransform = null; if (checksum) { hashTransform = new HashTransform(checksum.length === 32 ? "md5" : "sha256"); outputStreams.push(hashTransform); } if (isGzip) { outputStreams.push(node_zlib.createGunzip()); } outputStreams.push(outStream); await pipe(inStream, ...outputStreams); if (hashTransform) { const _checksum = checksum.toLowerCase(); if (hashTransform.hash !== _checksum) { throw new Error(`checksum remote(${checksum}) != local(${hashTransform.hash})`); } } await renamePromisify(this.asarSwapPath, this.asarNextPath); await writeFilePromisify(this.asarVersionPath, JSON.stringify(asarInfo, null, 2), { encoding: "utf8" }); return true; } /** * 软件当前是否已经有 asar 的更新文件 */ async hasUpgraded() { if (await exists(this.asarNextPath) && await exists(this.asarVersionPath)) { return JSON.parse( await readFilePromisify(this.asarVersionPath, { encoding: "utf8" }) ); } return false; } /** * 替换 asar 文件 * @param execPath win32 传入可以触发重启(其它平台请自行使用 app.relaunch) */ async upgrade(relaunch = false) { if (node_process.platform === "win32") { const updaterScriptPath = node_path.join(this.resourcePath, "updater.vbs"); await generateWinScript(this.resourcePath, updaterScriptPath, this.execPath); node_child_process.spawn("cmd", ["/s", "/c", "wscript", `"${updaterScriptPath}"`, `"${relaunch ? "1" : "0"}"`], { detached: true, windowsVerbatimArguments: true, stdio: "ignore", windowsHide: true }); } else { await rmPromisify(this.asarTargetPath, { force: true }); await renamePromisify(this.asarNextPath, this.asarTargetPath); } } } exports.AsarUpdater = AsarUpdater;