electron-asar-updater-pro-new 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +34 -0
  3. package/readme.md +129 -0
  4. package/src/index.js +396 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 xianyunleo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "electron-asar-updater-pro-new",
3
+ "version": "2.5.4",
4
+ "description": "Handles Electron app.asar updates. Electron asar 热更新",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/xianyunleo/electron-asar-updater-pro.git"
12
+ },
13
+ "keywords": [
14
+ "electron",
15
+ "asar",
16
+ "update",
17
+ "updater"
18
+ ],
19
+ "author": "mojixiang1102@gmail.com",
20
+ "license": "MIT",
21
+ "bugs": {
22
+ "url": "https://github.com/xianyunleo/electron-asar-updater-pro/issues"
23
+ },
24
+ "homepage": "https://github.com/xianyunleo/electron-asar-updater-pro#readme",
25
+ "dependencies": {
26
+ "adm-zip": "^0.5.10",
27
+ "electron-fetch": "^1.9.1",
28
+ "electron-log": "^4.0.0",
29
+ "semver-diff": "^3.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "electron": ">=13.0.0"
33
+ }
34
+ }
package/readme.md ADDED
@@ -0,0 +1,129 @@
1
+ # electron-asar-updater-pro
2
+
3
+ 专业现代化的 electron asar文件更新。支持Windows、Mac 、Linux
4
+
5
+ 优点:Windows 无需额外的 exe,支持C盘Program Files目录下的更新。
6
+
7
+ 建议:因为开发模式下,路径不准确,仅测试代码跑通。完整流程,请将项目编译打包运行测试。
8
+
9
+ #### 安装
10
+ ```
11
+ npm i electron-asar-updater-pro
12
+ ```
13
+
14
+ #### 安装要求
15
+
16
+ ```
17
+ Electron >= 13
18
+ Node >= 14
19
+ ```
20
+
21
+ #### 示例
22
+
23
+ ```js
24
+ //Main Process
25
+ const Updater = require('electron-asar-updater-pro');
26
+ const options = {
27
+ api: {url: 'http://www.test.com/api'},
28
+ debug: true
29
+ }
30
+ const updater = new Updater(options);
31
+
32
+ ipcMain.handle('updater-check', async (event, data) => {
33
+ return await updater.check();
34
+ });
35
+
36
+ ipcMain.handle('updater-update', async (event, data) => {
37
+ updater.on('downloadProgress', progress => {
38
+ event.sender.send('updater-download-progress', progress)
39
+ });
40
+ await updater.update();
41
+ });
42
+
43
+ //Renderer Process
44
+ async function check() {
45
+ try {
46
+ const result = await ipcRenderer.invoke('updater-check');
47
+ if(result){
48
+ await update();
49
+ }
50
+ } catch (error) {
51
+ console.log('检查更新失败');
52
+ console.log(error);
53
+ }
54
+ };
55
+
56
+ async function update() {
57
+ try {
58
+ ipcRenderer.on('updater-download-progress', (event, message) => {
59
+ console.log(message)
60
+ })
61
+ await ipcRenderer.invoke('updater-update');
62
+ } catch (error) {
63
+ console.log('更新失败');
64
+ console.log(error);
65
+ }
66
+ };
67
+
68
+ ```
69
+
70
+ #### 服务端api json
71
+ ```
72
+ 远程asar文件名可以随意,sha256是指asar文件或者zip文件的hash
73
+
74
+ {
75
+ "version": "1.1.0",
76
+ "asar": "http://www.test.com/update.asar",
77
+ "sha256": "xxx"
78
+ }
79
+
80
+ 如果asar是zip文件,那么结构如下
81
+ ── update.zip
82
+ └── update.asar
83
+
84
+ ```
85
+
86
+ #### 构造方法
87
+
88
+ ```js
89
+ options = {
90
+ api: {
91
+ url: '', //
92
+ body: {}, //string或object,服务端可根据这个参数,返回不同的response json
93
+ method: 'POST|GET', //default POST
94
+ headers: {}
95
+ },
96
+ autoRestart: true,
97
+ debug: false,
98
+ };
99
+ const updater = new Updater(options);
100
+ ```
101
+
102
+ #### 方法
103
+
104
+ ```js
105
+ await check(); //检查是否有更新,本地版本号和远程版本号比较
106
+ await update(); //更新并重启软件,必须先执行check方法
107
+ stopDownload(); //停止下载,仅限node v15及以上版本。
108
+ ```
109
+
110
+ #### 事件
111
+ ```js
112
+ updater.on('downloadProgress', progress => {
113
+ //下载进度
114
+ });
115
+
116
+ updater.on('status', status => {
117
+ //Updater.EnumStatus,更新的状态,用作参考。
118
+ });
119
+ ```
120
+ #### 静态属性
121
+ ```js
122
+ Updater.EnumStatus; //更新的状态
123
+ ```
124
+
125
+ #### 其它:
126
+
127
+ 如果你使用了`electron-vite` 作为脚手架,那么你可能需要配置`build.rollupOptions.external: ["original-fs"],`,请参考 https://cn.electron-vite.org/config/#%E5%86%85%E7%BD%AE%E9%85%8D%E7%BD%AE
128
+
129
+ 如果你使用了`vue-cli-plugin-electron-builder` 作为脚手架,那么你可能需要配置`externals: ["electron-asar-updater-pro"],`,请参考 https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#native-modules
package/src/index.js ADDED
@@ -0,0 +1,396 @@
1
+ const {app,net} = require('electron');
2
+ const fs = require("original-fs");
3
+ const fsPromises = fs.promises;
4
+ const path = require("path");
5
+ const child_process = require("child_process");
6
+ const {EventEmitter} = require("events");
7
+ const electronLog = require("electron-log");
8
+ const semverDiff = require("semver-diff");
9
+ const Fetch = require('electron-fetch')
10
+ const AdmZip = require("adm-zip");
11
+
12
+ const exists = async (p) => {
13
+ return fsPromises.access(p).then(() => true).catch(() => false)
14
+ }
15
+
16
+ const Updater = class Updater extends EventEmitter {
17
+ _options = {
18
+ api: {
19
+ url: null,
20
+ body: '',
21
+ method: 'POST',
22
+ headers:{}
23
+ },
24
+ autoRestart: true,
25
+ debug: false,
26
+ };
27
+ _updateFileName = 'update.asar';
28
+ _downloadUrl = '';
29
+ _sha256 = '';
30
+ _downloadDir = '';
31
+ _downloadFilePath = '';
32
+ _status = 0;
33
+ _dlAbortController;
34
+ _isOldNode = false;
35
+ static EnumStatus = {
36
+ Ready: 0,
37
+ CheckError: 101,
38
+ Downloading: 102,
39
+ Downloaded: 103,
40
+ DownloadError: 104,
41
+ Extracting: 105,
42
+ Extracted: 106,
43
+ ExtractError: 107,
44
+ Moving: 108,
45
+ MoveError: 109,
46
+ HashNoMatch: 110,
47
+ Finish: 100,
48
+ Cancel: 200,
49
+ }
50
+
51
+ /**
52
+ * @param {Object} options
53
+ * @param {Object} options.api
54
+ * @param {string} options.api.url
55
+ * @param {string|Object} [options.api.body]
56
+ * @param {string} [options.api.method]
57
+ * @param {Object} [options.api.headers]
58
+ * @param {boolean} [options.autoRestart]
59
+ * @param {boolean} [options.debug]
60
+ */
61
+ constructor(options) {
62
+ super();
63
+ this._isOldNode = !!this._getNodeMajorVersion() < 15;
64
+ options.autoRestart = options.autoRestart ?? true
65
+ this._options = options;
66
+ this._downloadDir = app.getPath('userData');
67
+ if (!this._isOldNode) {
68
+ this._dlAbortController = new AbortController();
69
+ }
70
+ }
71
+
72
+ /**
73
+ *
74
+ * @returns {Promise<boolean>}
75
+ */
76
+ async check() {
77
+ const url = this._options.api.url;
78
+ if (!url) return false;
79
+
80
+ this._log(`AppDir: ${this.getAppDir()}`);
81
+ const appVersion = this.getAppVersion();
82
+ this._log(`Check:appVersion:${appVersion}`);
83
+ let respData;
84
+ const fetchOpts = {method: this._options.api.method ?? 'POST'};
85
+ try {
86
+ if (this._options.api.body) {
87
+ if (typeof this._options.api.body !== 'string') {
88
+ this._options.api.body = JSON.stringify(this._options.api.body);
89
+ }
90
+ fetchOpts.body = this._options.api.body;
91
+ }
92
+ if (this._options.api.headers) {
93
+ fetchOpts.headers = this._options.api.headers;
94
+ }
95
+ respData = await (await Fetch.default(url, fetchOpts)).json();
96
+ } catch (error) {
97
+ this._changeStatus(Updater.EnumStatus.Cancel, `Cannot connect to api url,${error.message}`);
98
+ throw new Error(`Cannot connect to api url,${error.message}`);
99
+ }
100
+
101
+ if (!respData.version || !respData.asar) {
102
+ this._changeStatus(Updater.EnumStatus.Cancel, 'Api url response not valid');
103
+ throw new Error('Api url response not valid');
104
+ }
105
+
106
+ if (!semverDiff(appVersion, respData.version)) {
107
+ this._changeStatus(Updater.EnumStatus.Cancel, 'No updates available');
108
+ return false;
109
+ }
110
+ this._downloadUrl = respData.asar;
111
+ this._sha256 = respData.sha256;
112
+ return true;
113
+ }
114
+
115
+ async update() {
116
+ try {
117
+ this._changeStatus(Updater.EnumStatus.Downloading);
118
+ await this._download();
119
+ } catch (error) {
120
+ if (error.name === 'AbortError') {
121
+ this._changeStatus(Updater.EnumStatus.Cancel, 'Download cancelled');
122
+ return;
123
+ } else {
124
+ this._changeStatus(Updater.EnumStatus.DownloadError, `Download Error,${error}`);
125
+ throw new Error(`Download Error,${error}`);
126
+ }
127
+ }
128
+
129
+ if (this._sha256) {
130
+ const fileBuffer = await fsPromises.readFile(this._downloadFilePath);
131
+ if (this.sha256(fileBuffer) !== this._sha256) {
132
+ this._changeStatus(Updater.EnumStatus.HashNoMatch, `File hash mismatch`);
133
+ throw new Error('File hash mismatch');
134
+ }
135
+ }
136
+
137
+ if (this._downloadFilePath.endsWith('.zip')) {
138
+ try {
139
+ this._changeStatus(Updater.EnumStatus.Extracting, 'Extracting');
140
+ await this._zipExtract();
141
+ this._changeStatus(Updater.EnumStatus.Extracted);
142
+ } catch (error) {
143
+ this._changeStatus(Updater.EnumStatus.ExtractError, 'Extract Error');
144
+ throw new Error('Extract Error');
145
+ }
146
+ }
147
+
148
+ try {
149
+ this._changeStatus(Updater.EnumStatus.Moving, 'Moving');
150
+ await this._move();
151
+ } catch (error) {
152
+ this._changeStatus(Updater.EnumStatus.MoveError, error.message);
153
+ throw new Error(`Move Error,${error.message}`);
154
+ }
155
+ this._changeStatus(Updater.EnumStatus.Finish);
156
+
157
+ if (this._options.autoRestart) {
158
+ app.relaunch();
159
+ app.quit();
160
+ }
161
+
162
+ }
163
+
164
+ async _download() {
165
+ let receivedBytes = 0;
166
+ const request = net.request({url:this._downloadUrl});
167
+ const responsePromise = this._getResponse(request);
168
+ request.end();
169
+ const response = await responsePromise;
170
+ const headers = response.headers;
171
+ const contentType = headers['content-type'];
172
+ const totalBytes = headers['content-length'] ? parseInt(headers['content-length']) : 0;
173
+ if (!await exists(this._downloadDir)) {
174
+ await fsPromises.mkdir(this._downloadDir, {recursive: true});
175
+ }
176
+
177
+ const isZipFromUrl = this._downloadUrl.toLowerCase().includes('.zip');
178
+ const isZipFromHeader = contentType && contentType.includes('zip');
179
+ let filePath = path.join(this._downloadDir, this._updateFileName);
180
+ if (isZipFromUrl || isZipFromHeader) {
181
+ let zipName = `${this._updateFileName}.zip`;
182
+ try {
183
+ const urlPathName = new URL(this._downloadUrl).pathname;
184
+ const basename = path.basename(urlPathName);
185
+ if (basename) {
186
+ zipName = basename;
187
+ }
188
+ } catch {
189
+ }
190
+ filePath = path.join(this._downloadDir, zipName);
191
+ }
192
+ this._downloadFilePath = filePath;
193
+ if (await exists(filePath)) {
194
+ await this.deleteFile(filePath);
195
+ }
196
+ const writeStream = fs.createWriteStream(filePath);
197
+
198
+ response.on('data', (buffer) => {
199
+ receivedBytes += buffer.length;
200
+ const progress = {
201
+ percent: totalBytes === 0 ? 0 : receivedBytes / totalBytes,
202
+ transferred: receivedBytes,
203
+ total: totalBytes,
204
+ }
205
+ this.emit('downloadProgress', progress)
206
+ })
207
+
208
+
209
+ if (this._isOldNode) {
210
+ const util = require('util');
211
+ const stream = require('stream');
212
+ const pipeline = util.promisify(stream.pipeline);
213
+ await pipeline(response, writeStream);
214
+ } else {
215
+ const {pipeline} = require('node:stream/promises');
216
+ await pipeline(response, writeStream, {signal: this._dlAbortController.signal});
217
+ }
218
+
219
+ this._changeStatus(Updater.EnumStatus.Downloaded);
220
+ }
221
+
222
+ async deleteFile($p) {
223
+ try {
224
+ await fsPromises.unlink($p);
225
+ if (await exists($p)) {
226
+ await fsPromises.rm($p, {force: true, recursive: true});
227
+ }
228
+ } catch {
229
+ }
230
+ }
231
+
232
+ async _zipExtract() {
233
+ let zip = new AdmZip(this._downloadFilePath);
234
+ zip.extractAllTo(this._downloadDir, true);
235
+ }
236
+
237
+ stopDownload() {
238
+ if (this._isOldNode) {
239
+ throw new Error('This method only supports node v15 and later');
240
+ } else {
241
+ this._dlAbortController.abort();
242
+ }
243
+ }
244
+
245
+ async _move() {
246
+ const resourcesDir = this.getResourcesDir();
247
+ const updateAsarPath = path.join(this._downloadDir,this._updateFileName);
248
+ const appAsarPath = path.join(resourcesDir, 'app.asar');
249
+
250
+ if (!this.isDev()) {
251
+ try {
252
+ const bakAsarPath = path.join(this._downloadDir, 'app.bak.asar');
253
+ await this.deleteFile(bakAsarPath); //如果已有bak文件是只读,那么必须要先删除才能copy overwrite
254
+ await fsPromises.copyFile(appAsarPath, bakAsarPath);
255
+ } catch (e) {
256
+ this._log(`Backup app.bak.asar error.${e.message}`);
257
+ }
258
+ }
259
+
260
+ const canWriteResources = await this._checkWritePermission(appAsarPath);
261
+ this._log(`CanWriteResources ${canWriteResources}`);
262
+ if (canWriteResources) {
263
+ this._log(`Copy ${updateAsarPath} to ${appAsarPath}`);
264
+ await fsPromises.chmod(appAsarPath, 0o666)
265
+ await fsPromises.copyFile(updateAsarPath, appAsarPath);
266
+ } else {
267
+ if (process.platform !== 'win32') {
268
+ throw new Error('app.asar access denied');
269
+ }
270
+
271
+ const shell = 'powershell';
272
+ const options = {shell: shell, stdio: 'ignore'};
273
+ const command = `Start-Process`;
274
+ const updateAsarPathArg = this._getArg(updateAsarPath, true);
275
+ const appAsarPathArg = this._getArg(appAsarPath, true);
276
+ const args = [
277
+ '-WindowStyle', 'hidden',
278
+ '-FilePath', 'cmd',
279
+ '-ArgumentList', `"/c attrib -r ${appAsarPathArg} & copy /y ${updateAsarPathArg} ${appAsarPathArg}"`,
280
+ '-Verb', 'RunAs'
281
+ ];
282
+
283
+ this._log(`Update start shell process. Command:${command}, Args:${args.join(' ')}`);
284
+ try {
285
+ const childProcess = child_process.spawn(command, args, options);
286
+ await new Promise((resolve, reject) => {
287
+ childProcess.on('exit', (code) => {
288
+ if (code == 1) {
289
+ reject(new Error('The operation has been canceled by the user'));
290
+ } else {
291
+ resolve();
292
+ }
293
+ });
294
+ childProcess.on('error', (error) => {
295
+ reject(error);
296
+ });
297
+ });
298
+ } catch (error) {
299
+ throw new Error('Start shell process Error: ' + error);
300
+ }
301
+ }
302
+ }
303
+
304
+ async _getResponse(request) {
305
+ return new Promise((resolve, reject) => {
306
+ request.on('response', response => {
307
+ resolve(response);
308
+ });
309
+ request.on('error', error => {
310
+ reject(error);
311
+ });
312
+ });
313
+ }
314
+
315
+ _changeStatus(status, logText = '') {
316
+ this._status = status;
317
+ this.emit('status', status);
318
+ if (logText) {
319
+ this._log(logText);
320
+ }
321
+ }
322
+
323
+ _log(text) {
324
+ if (this._options.debug) {
325
+ console.log('Updater: ', text)
326
+ }
327
+ electronLog.info('[ electron-asar-updater-pro ]', text)
328
+ }
329
+
330
+ _getNodeMajorVersion(version) {
331
+ return parseInt(process.versions.node.split('.')[0]);
332
+ }
333
+
334
+ getStatus() {
335
+ return this._status;
336
+ }
337
+
338
+ getAppVersion() {
339
+ return app.getVersion();
340
+ }
341
+
342
+ getAppDir() {
343
+ if (this.isDev()) {
344
+ return app.getAppPath();
345
+ } else {
346
+ return path.dirname(this.getExePath());
347
+ }
348
+ }
349
+
350
+ getResourcesDir() {
351
+ if (this.isDev()) {
352
+ return app.getAppPath();
353
+ } else {
354
+ return path.dirname(app.getAppPath());
355
+ }
356
+ }
357
+
358
+ getExePath() {
359
+ //可执行文件路径,Mac返回路径为 AppName.app/Contents/MacOS/AppName。dev返回node_modules\electron\dist\updater.exe
360
+ return app.getPath('exe');
361
+ }
362
+
363
+ isDev() {
364
+ return !app.isPackaged;
365
+ }
366
+
367
+ _getArg(str, isPowershell = false) {
368
+ if (isPowershell) {
369
+ return `\`"${str}\`"`;
370
+ }
371
+ return `"${str}"`;
372
+ }
373
+
374
+ async _checkWritePermission(path) {
375
+ try {
376
+ if (process.platform === 'win32') {
377
+ const fileHandle = await fsPromises.open(path, "w");
378
+ await fileHandle?.close();
379
+ } else {
380
+ await fsPromises.access(path, fs.constants.W_OK);
381
+ }
382
+ return true;
383
+ } catch (err) {
384
+ return false;
385
+ }
386
+ }
387
+
388
+ sha256(data) {
389
+ const crypto = require('crypto');
390
+ const hash = crypto.createHash('sha256');
391
+ hash.update(data);
392
+ return hash.digest('hex');
393
+ }
394
+ }
395
+
396
+ module.exports = Updater;