electron-incremental-update 1.3.0 → 2.0.0-beta.10

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.
package/dist/index.js CHANGED
@@ -1,291 +1,167 @@
1
- import {
2
- __require,
3
- getPathFromAppNameAsar,
4
- getVersions,
5
- is,
6
- isUpdateJSON,
7
- parseVersion,
8
- restartApp,
9
- unzipFile,
10
- waitAppReady
11
- } from "./chunk-SBPTSLG7.js";
1
+ import { isDev, getEntryVersion, getAppVersion, getPathFromAppNameAsar, restartApp } from './chunk-4MH6ZXCY.js';
2
+ import { isUpdateJSON, __require } from './chunk-72ZAJ7AF.js';
3
+ import fs2 from 'node:fs';
4
+ import { EventEmitter } from 'node:events';
5
+ import { app } from 'electron';
6
+ import path from 'node:path';
12
7
 
13
- // src/index.ts
14
- import { resolve } from "node:path";
15
- import { existsSync as existsSync2, renameSync } from "node:fs";
16
- import { app as app2 } from "electron";
17
-
18
- // src/updater/core.ts
19
- import { existsSync, rmSync, writeFileSync } from "node:fs";
20
- import { app } from "electron";
21
-
22
- // src/crypto/dec.ts
23
- import { createDecipheriv, createVerify } from "node:crypto";
24
-
25
- // src/crypto/utils.ts
26
- import { createHash } from "node:crypto";
27
- function hashString(data, length) {
28
- const hash = createHash("SHA256").update(data).digest("binary");
29
- return Buffer.from(hash).subarray(0, length);
30
- }
31
-
32
- // src/crypto/dec.ts
33
- function decrypt(encryptedText, key, iv) {
34
- const decipher = createDecipheriv("aes-256-cbc", key, iv);
35
- let decrypted = decipher.update(encryptedText, "base64url", "utf8");
36
- decrypted += decipher.final("utf8");
37
- return decrypted;
38
- }
39
- var verify = (buffer, signature, cert) => {
40
- try {
41
- const [sig, version] = decrypt(signature, hashString(cert, 32), hashString(buffer, 16)).split("%");
42
- const result = createVerify("RSA-SHA256").update(buffer).verify(cert, sig, "base64");
43
- return result ? version : false;
44
- } catch (error) {
45
- return false;
46
- }
47
- };
48
-
49
- // src/updater/types.ts
8
+ // src/entry/types.ts
50
9
  var ErrorInfo = {
51
- downlaod: "Download failed",
52
- validate: "Validate failed",
53
- param: "Missing params",
54
- version: "Unsatisfied version"
10
+ download: "Download Failed",
11
+ validate: "Validate Failed",
12
+ param: "Missing Params",
13
+ network: "Network Error"
55
14
  };
56
15
  var UpdaterError = class extends Error {
16
+ code;
57
17
  constructor(msg, info) {
58
- super(msg + ": " + info);
59
- }
60
- };
61
-
62
- // src/updater/defaultFunctions/download.ts
63
- import { net } from "electron";
64
- async function downlaodFn(url, headers, onResponse) {
65
- await waitAppReady();
66
- return new Promise((resolve2, reject) => {
67
- const request = net.request({ url, method: "GET", redirect: "follow" });
68
- Object.keys(headers).forEach((key) => request.setHeader(key, headers[key]));
69
- request.on("response", (res) => onResponse(res, resolve2, reject));
70
- request.on("error", reject);
71
- request.end();
72
- });
73
- }
74
- var downloadJSONDefault = async (url, headers) => {
75
- return await downlaodFn(url, headers, (resp, resolve2, reject) => {
76
- let data = "";
77
- resp.on("data", (chunk) => data += chunk);
78
- resp.on("end", () => {
79
- try {
80
- const json = JSON.parse(data);
81
- if (isUpdateJSON(json)) {
82
- resolve2(json);
83
- } else {
84
- throw Error;
85
- }
86
- } catch (ignore) {
87
- reject(new Error("invalid update json"));
88
- }
89
- });
90
- resp.on("aborted", () => reject(new Error("aborted")));
91
- resp.on("error", () => reject(new Error("download error")));
92
- });
93
- };
94
- var downloadBufferDefault = async (url, headers, total, onDownloading) => {
95
- let current = 0;
96
- return await downlaodFn(url, headers, (resp, resolve2, reject) => {
97
- let data = [];
98
- resp.on("data", (chunk) => {
99
- current += chunk.length;
100
- onDownloading?.({ percent: `${+(current / total).toFixed(2) * 100}%`, total, current });
101
- data.push(chunk);
102
- });
103
- resp.on("end", () => resolve2(Buffer.concat(data)));
104
- resp.on("aborted", () => reject(new Error("aborted")));
105
- resp.on("error", () => reject(new Error("download error")));
106
- });
107
- };
108
-
109
- // src/updater/defaultFunctions/compareVersion.ts
110
- var isLowerVersionDefault = (version1, version2) => {
111
- const oldV = parseVersion(version1);
112
- const newV = parseVersion(version2);
113
- function compareStrings(str1, str2) {
114
- if (str1 === "") {
115
- return str2 !== "";
116
- } else if (str2 === "") {
117
- return true;
118
- }
119
- return str1 < str2;
18
+ super("[" + ErrorInfo[msg] + "] " + info);
19
+ this.code = msg;
120
20
  }
121
- for (let key of Object.keys(oldV)) {
122
- if (key === "stage" && compareStrings(oldV[key], newV[key])) {
123
- return true;
124
- } else if (oldV[key] !== newV[key]) {
125
- return oldV[key] < newV[key];
126
- }
127
- }
128
- return false;
129
21
  };
130
22
 
131
- // src/updater/core.ts
132
- var Updater = class {
133
- CERT = __SIGNATURE_CERT__;
23
+ // src/entry/updater.ts
24
+ var Updater = class extends EventEmitter {
25
+ CERT = __EIU_SIGNATURE_CERT__;
134
26
  info;
135
- option;
136
- asarPath;
137
- gzipPath;
138
- tmpFilePath;
27
+ provider;
139
28
  /**
140
- * updater logger
29
+ * Updater logger
141
30
  */
142
31
  logger;
143
32
  /**
144
- * downloading progress hook
145
- * @param progress download progress
146
- * @example
147
- * updater.onDownloading = ({ percent, total, current }) => {
148
- * console.log(`download progress: ${percent}, total: ${total}, current: ${current}`)
149
- * }
33
+ * Whether to receive beta update
150
34
  */
151
- onDownloading;
35
+ receiveBeta;
152
36
  /**
153
- * whether receive beta version
37
+ * Whether force update in DEV
154
38
  */
155
- get receiveBeta() {
156
- return !!this.option.receiveBeta;
157
- }
158
- set receiveBeta(receiveBeta) {
159
- this.option.receiveBeta = receiveBeta;
160
- }
39
+ forceUpdate;
161
40
  /**
162
- * initialize incremental updater
163
- * @param option UpdaterOption
41
+ * Initialize incremental updater
42
+ * @param options UpdaterOption
164
43
  */
165
- constructor(option = {}) {
166
- this.option = option;
167
- if (option.SIGNATURE_CERT) {
168
- this.CERT = option.SIGNATURE_CERT;
44
+ constructor(options = {}) {
45
+ super();
46
+ this.provider = options.provider;
47
+ this.receiveBeta = options.receiveBeta;
48
+ this.CERT = options.SIGNATURE_CERT || __EIU_SIGNATURE_CERT__;
49
+ this.logger = options.logger;
50
+ if (isDev && !this.logger) {
51
+ this.logger = {
52
+ info: (...args) => console.log("[EIU-INFO ]", ...args),
53
+ debug: (...args) => console.log("[EIU-DEBUG]", ...args),
54
+ warn: (...args) => console.log("[EIU-WARN ]", ...args),
55
+ error: (...args) => console.error("[EIU-ERROR]", ...args)
56
+ };
57
+ this.logger.info("no logger set, enable dev-only logger");
169
58
  }
170
- this.asarPath = getPathFromAppNameAsar();
171
- this.gzipPath = `${this.asarPath}.gz`;
172
- this.tmpFilePath = `${this.asarPath}.tmp`;
173
- }
174
- async needUpdate(version, minVersion) {
175
- const isLowerVersion = this.option.overrideFunctions?.isLowerVersion ?? isLowerVersionDefault;
176
- const { appVersion, entryVersion } = getVersions();
177
- if (await isLowerVersion(entryVersion, minVersion)) {
178
- throw new UpdaterError(ErrorInfo.version, `entry version (${entryVersion}) < minimumVersion (${minVersion})`);
59
+ if (!this.provider) {
60
+ this.logger?.debug("No update provider, please setup provider before checking update");
179
61
  }
180
- this.logger?.info(`check update: current version is ${appVersion}, new version is ${version}`);
181
- return await isLowerVersion(appVersion, version);
182
62
  }
183
- async parseData(format, data) {
184
- if (existsSync(this.tmpFilePath)) {
185
- this.logger?.warn(`remove tmp file: ${this.tmpFilePath}`);
186
- rmSync(this.tmpFilePath);
187
- }
188
- if (existsSync(this.gzipPath)) {
189
- this.logger?.warn(`remove .gz file: ${this.gzipPath}`);
190
- rmSync(this.gzipPath);
63
+ checkProvider() {
64
+ if (!this.provider) {
65
+ throw new UpdaterError("param", "missing update provider");
191
66
  }
67
+ }
68
+ async fetch(format, data) {
192
69
  if (typeof data === "object") {
193
70
  if (format === "json" && isUpdateJSON(data) || format === "buffer" && Buffer.isBuffer(data)) {
194
71
  return data;
195
72
  } else {
196
- throw new UpdaterError(ErrorInfo.param, `invalid type at format '${format}': ${JSON.stringify(data)}`);
73
+ this.err("invalid type", "param", `invalid type at format '${format}': ${JSON.stringify(data)}`);
74
+ return;
197
75
  }
198
76
  }
199
- const ua = this.option.downloadConfig?.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36";
200
- const headers = {
201
- Accept: `application/${format === "json" ? "json" : "octet-stream"}`,
202
- UserAgent: ua,
203
- ...this.option.downloadConfig?.extraHeader
204
- };
205
- this.logger?.debug(`download headers: ${JSON.stringify(headers)}`);
206
- const config = format === "json" ? {
207
- name: "updateJsonURL",
208
- url: this.option.updateJsonURL,
209
- repoFallback: `${this.option.repository?.replace("github.com", "raw.githubusercontent.com")}/master/version.json`,
210
- fn: this.option.overrideFunctions?.downloadJSON ?? downloadJSONDefault
211
- } : {
212
- name: "releaseAsarURL",
213
- url: this.option.releaseAsarURL,
214
- repoFallback: `${this.option.repository}/releases/download/v${this.info?.version}/${app.name}-${this.info?.version}.asar.gz`,
215
- fn: this.option.overrideFunctions?.downloadBuffer ?? downloadBufferDefault
216
- };
217
- data ??= config.url;
218
- if (!data) {
219
- this.logger?.debug(`no ${config.name}, fallback to use repository`);
220
- if (!this.option.repository) {
221
- throw new UpdaterError(ErrorInfo.param, `${config.name} or repository is not set`);
222
- }
223
- if (format === "buffer" && !this.info?.version) {
224
- throw new UpdaterError(ErrorInfo.param, "version is not set");
225
- }
226
- data = config.repoFallback;
227
- }
228
- this.logger?.debug(`download ${format} from ${data}`);
77
+ this.logger?.debug(`download from ${this.provider.name}`);
229
78
  try {
230
- const ret = format === "json" ? await config.fn(data, headers) : await config.fn(data, headers, this.info.size, this.onDownloading);
231
- this.logger?.debug(`download ${format} success${format === "buffer" ? `, file size: ${ret.length}` : ""}`);
232
- return ret;
79
+ const result = format === "json" ? await this.provider.downloadJSON(data ?? __EIU_VERSION_PATH__) : await this.provider.downloadAsar(app.name, this.info, (data2) => this.emit("download-progress", data2));
80
+ this.logger?.debug(`download ${format} success${format === "buffer" ? `, file size: ${result.length}` : ""}`);
81
+ return result;
233
82
  } catch (e) {
234
- throw new UpdaterError(ErrorInfo.downlaod, e.toString());
83
+ this.err(`fetch ${format} failed`, "network", `download ${format} failed: ${e}`);
235
84
  }
236
85
  }
86
+ /**
87
+ * Handle error message and emit error event
88
+ */
89
+ err(msg, code, errorInfo) {
90
+ const err = new UpdaterError(code, errorInfo);
91
+ this.logger?.error(msg, err);
92
+ this.emit("error", err);
93
+ }
237
94
  async checkUpdate(data) {
95
+ this.checkProvider();
96
+ const emitUnavailable = (msg) => {
97
+ this.logger?.info(msg);
98
+ this.emit("update-unavailable", msg);
99
+ return false;
100
+ };
101
+ const _data = await this.fetch("json", data);
102
+ if (!_data) {
103
+ return emitUnavailable("failed to get update info");
104
+ }
105
+ let { signature, version, minimumVersion, beta } = _data;
106
+ if (this.receiveBeta) {
107
+ version = beta.version;
108
+ signature = beta.signature;
109
+ minimumVersion = beta.minimumVersion;
110
+ }
111
+ this.logger?.debug(`checked update, version: ${version}, signature: ${signature}`);
112
+ if (isDev && !this.forceUpdate && !data) {
113
+ return emitUnavailable("skip check update in dev mode, to force update, set `updater.forceUpdate` to true or call checkUpdate with UpdateJSON");
114
+ }
115
+ const isLowerVersion = this.provider.isLowerVersion;
116
+ const entryVersion = getEntryVersion();
117
+ const appVersion = getAppVersion();
238
118
  try {
239
- let { signature, size, version, minimumVersion, beta } = await this.parseData("json", data);
240
- if (this.receiveBeta) {
241
- version = beta.version;
242
- signature = beta.signature;
243
- minimumVersion = beta.minimumVersion;
244
- size = beta.size;
119
+ if (isLowerVersion(entryVersion, minimumVersion)) {
120
+ return emitUnavailable(`entry version (${entryVersion}) < minimumVersion (${minimumVersion})`);
245
121
  }
246
- this.logger?.debug(`checked version: ${version}, size: ${size}, signature: ${signature}`);
247
- if (!await this.needUpdate(version, minimumVersion)) {
248
- this.logger?.info(`update unavailable: ${version} is the latest version`);
249
- return void 0;
250
- } else {
251
- this.logger?.info(`update available: ${version}`);
252
- this.info = {
253
- signature,
254
- minimumVersion,
255
- version,
256
- size
257
- };
258
- return this.info;
122
+ this.logger?.info(`check update: current version is ${appVersion}, new version is ${version}`);
123
+ if (!isLowerVersion(appVersion, version)) {
124
+ return emitUnavailable(`current version (${appVersion}) < new version (${version})`);
259
125
  }
260
- } catch (error) {
261
- this.logger?.error("check update failed", error);
262
- return error;
126
+ this.logger?.info(`update available: ${version}`);
127
+ this.info = { signature, minimumVersion, version };
128
+ this.emit("update-available", this.info);
129
+ return true;
130
+ } catch {
131
+ this.err("Fail to parse version", "validate", "fail to parse version string");
132
+ return false;
263
133
  }
264
134
  }
265
- async download(data, sig) {
135
+ async downloadUpdate(data, info) {
136
+ this.checkProvider();
137
+ const _sig = info?.signature ?? this.info?.signature;
138
+ const _version = info?.version ?? this.info?.version;
139
+ if (!_sig || !_version) {
140
+ this.err("download failed", "param", "no update signature, please call `checkUpdate` first or manually setup params");
141
+ return false;
142
+ }
143
+ const buffer = await this.fetch("buffer", data ? Buffer.from(data) : void 0);
144
+ if (!buffer) {
145
+ this.err("download failed", "param", "no update asar file buffer");
146
+ return false;
147
+ }
148
+ this.logger?.debug("verify start");
149
+ if (!await this.provider.verifySignaure(buffer, _version, _sig, this.CERT)) {
150
+ this.err("download failed", "validate", "invalid update asar file");
151
+ return false;
152
+ }
153
+ this.logger?.debug("verify success");
266
154
  try {
267
- const _sig = sig ?? this.info?.signature;
268
- if (!_sig) {
269
- throw new UpdaterError(ErrorInfo.param, "signature is empty");
270
- }
271
- const buffer = await this.parseData("buffer", data);
272
- this.logger?.debug("verify start");
273
- const _verify = this.option.overrideFunctions?.verifySignaure ?? verify;
274
- const _ver = await _verify(buffer, _sig, this.CERT);
275
- if (!_ver) {
276
- throw new UpdaterError(ErrorInfo.validate, "invalid signature or certificate");
277
- }
278
- this.logger?.debug("verify success");
279
- this.logger?.debug(`write to ${this.gzipPath}`);
280
- writeFileSync(this.gzipPath, buffer);
281
- this.logger?.debug(`extract to ${this.tmpFilePath}`);
282
- await unzipFile(this.gzipPath, this.tmpFilePath);
283
- this.logger?.info(`download success, version: ${_ver}`);
155
+ const tmpFilePath = getPathFromAppNameAsar() + ".tmp";
156
+ this.logger?.debug(`install to ${tmpFilePath}`);
157
+ fs2.writeFileSync(tmpFilePath, await this.provider.unzipFile(buffer));
158
+ this.logger?.info(`download success, version: ${_version}`);
284
159
  this.info = void 0;
160
+ this.emit("update-downloaded");
285
161
  return true;
286
162
  } catch (error) {
287
- this.logger?.error("download asar failed", error);
288
- return error;
163
+ this.err("download failed", "download", `fail to unwrap asar file, ${error}`);
164
+ return false;
289
165
  }
290
166
  }
291
167
  /**
@@ -296,13 +172,11 @@ var Updater = class {
296
172
  restartApp();
297
173
  }
298
174
  };
299
-
300
- // src/updater/index.ts
301
- function createUpdater(option) {
302
- return new Updater(option);
175
+ async function autoUpdate(updater) {
176
+ if (await updater.checkUpdate() && await updater.downloadUpdate()) {
177
+ updater.quitAndInstall();
178
+ }
303
179
  }
304
-
305
- // src/index.ts
306
180
  function startupWithUpdater(fn) {
307
181
  return fn;
308
182
  }
@@ -313,23 +187,13 @@ var defaultOnInstall = (install, _, __, logger) => {
313
187
  async function initApp(appOptions = {}) {
314
188
  const {
315
189
  updater,
316
- electronDevDistPath = "../dist-electron",
317
- mainPath = "main/index.js",
318
- hooks
319
- } = appOptions || {};
320
- const {
321
190
  onInstall = defaultOnInstall,
322
191
  beforeStart,
323
192
  onStartError
324
- } = hooks || {};
325
- function handleError(err, logger2) {
326
- console.error(err);
327
- onStartError?.(err, logger2);
328
- app2.quit();
329
- }
193
+ } = appOptions;
330
194
  let updaterInstance;
331
195
  if (typeof updater === "object" || !updater) {
332
- updaterInstance = createUpdater(updater);
196
+ updaterInstance = new Updater(updater);
333
197
  } else {
334
198
  updaterInstance = await updater();
335
199
  }
@@ -337,26 +201,22 @@ async function initApp(appOptions = {}) {
337
201
  try {
338
202
  const appNameAsarPath = getPathFromAppNameAsar();
339
203
  const tempAsarPath = `${appNameAsarPath}.tmp`;
340
- if (existsSync2(tempAsarPath)) {
204
+ if (fs2.existsSync(tempAsarPath)) {
341
205
  logger?.info(`installing new asar: ${tempAsarPath}`);
342
- await onInstall(() => renameSync(tempAsarPath, appNameAsarPath), tempAsarPath, appNameAsarPath, logger);
206
+ await onInstall(() => fs2.renameSync(tempAsarPath, appNameAsarPath), tempAsarPath, appNameAsarPath, logger);
343
207
  }
344
- const mainDir = is.dev ? electronDevDistPath : appNameAsarPath;
345
- const entry = resolve(__dirname, mainDir, mainPath);
346
- await beforeStart?.(entry, logger);
347
- __require(entry)(updaterInstance);
208
+ const mainFilePath = path.join(
209
+ isDev ? path.join(app.getAppPath(), __EIU_MAIN_DEV_DIR__) : appNameAsarPath,
210
+ "main",
211
+ __EIU_MAIN_FILE__
212
+ );
213
+ await beforeStart?.(mainFilePath, logger);
214
+ __require(mainFilePath)(updaterInstance);
348
215
  } catch (error) {
349
- handleError(error, logger);
216
+ logger?.error("startup error", error);
217
+ onStartError?.(error, logger);
218
+ app.quit();
350
219
  }
351
220
  }
352
- export {
353
- ErrorInfo,
354
- Updater,
355
- UpdaterError,
356
- createUpdater,
357
- downloadBufferDefault,
358
- downloadJSONDefault,
359
- initApp,
360
- isLowerVersionDefault,
361
- startupWithUpdater
362
- };
221
+
222
+ export { ErrorInfo, Updater, UpdaterError, autoUpdate, initApp, startupWithUpdater };