electron-updator 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -8,21 +8,20 @@
8
8
 
9
9
  [npm-image]: https://img.shields.io/npm/v/electron-updator.svg
10
10
  [npm-url]: https://npmjs.org/package/electron-updator
11
- [ci-image]: https://github.com/xudafeng/electron-updator/actions/workflows/ci.yml/badge.svg
12
- [ci-url]: https://github.com/xudafeng/electron-updator/actions/workflows/ci.yml
13
- [codecov-image]: https://img.shields.io/codecov/c/github/xudafeng/electron-updator.svg?logo=codecov
14
- [codecov-url]: https://codecov.io/gh/xudafeng/electron-updator
11
+ [ci-image]: https://github.com/electron-modules/electron-updator/actions/workflows/ci.yml/badge.svg
12
+ [ci-url]: https://github.com/electron-modules/electron-updator/actions/workflows/ci.yml
13
+ [codecov-image]: https://img.shields.io/codecov/c/github/electron-modules/electron-updator.svg?logo=codecov
14
+ [codecov-url]: https://codecov.io/gh/electron-modules/electron-updator
15
15
  [node-image]: https://img.shields.io/badge/node.js-%3E=_16-green.svg
16
16
  [node-url]: http://nodejs.org/download/
17
17
  [download-image]: https://img.shields.io/npm/dm/electron-updator.svg
18
18
  [download-url]: https://npmjs.org/package/electron-updator
19
19
 
20
- > electron-updator
21
-
20
+ > electron updator
22
21
  ## Installment
23
22
 
24
23
  ```bash
25
- $ npm i electron-updator --save-dev
24
+ $ npm i electron-updator --save
26
25
  ```
27
26
 
28
27
  ## License
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppUpdator = void 0;
4
+ const eventemitter3_1 = require("eventemitter3");
5
+ const constants_1 = require("./common/constants");
6
+ const download_file_1 = require("./utils/download-file");
7
+ const utils_1 = require("./utils");
8
+ const elelctron_app_adapter_1 = require("./elelctron-app-adapter");
9
+ class AppUpdator extends eventemitter3_1.EventEmitter {
10
+ constructor(options, app) {
11
+ super();
12
+ this.state = "idle" /* StateType.Idle */;
13
+ this.options = options;
14
+ this.logger = this._wrapLogger(options.logger);
15
+ this.app = app || new elelctron_app_adapter_1.ElectronAppAdapter();
16
+ this.startTimeStamp = new Date().getTime();
17
+ this.logger.info('AppUpdator#constructor');
18
+ this.availableUpdate = {
19
+ resourcePath: '',
20
+ latestAsarPath: '',
21
+ downloadTargetDir: '',
22
+ };
23
+ }
24
+ _wrapLogger(logger) {
25
+ if (!logger) {
26
+ logger = console;
27
+ }
28
+ const _logger = { ...logger };
29
+ const _wrap = (message, callback) => {
30
+ callback(`ElectronUpdator(${this.startTimeStamp})${message}`);
31
+ };
32
+ _logger.error = (message) => {
33
+ _wrap(message, logger.error);
34
+ };
35
+ _logger.info = (message) => {
36
+ _wrap(message, logger.info);
37
+ };
38
+ _logger.warn = (message) => {
39
+ _wrap(message, logger.warn);
40
+ };
41
+ return _logger;
42
+ }
43
+ setState(state) {
44
+ this.logger.info(`AppUpdator#setState${state}`);
45
+ this.state = state;
46
+ }
47
+ setFeedUrl(url) {
48
+ this.logger.info(`AppUpdator#setFeedUrl:url is ${url}`);
49
+ if (url && this.options) {
50
+ this.options.url = url;
51
+ }
52
+ }
53
+ async checkForUpdates(executeType = constants_1.ExecuteType.Auto) {
54
+ this.logger.info(`AppUpdator#checkForUpdates:state is ${this.state}`);
55
+ if (executeType === constants_1.ExecuteType.User) {
56
+ if (this.state === "downloaded" /* StateType.Downloaded */) {
57
+ this.logger.info(`AppUpdator#checkForUpdates:UPDATE_DOWNLOADED`);
58
+ this.emit(constants_1.EventType.UPDATE_DOWNLOADED, {
59
+ executeType,
60
+ });
61
+ }
62
+ else {
63
+ this.logger.info(`AppUpdator#checkForUpdates:UPDATE_NOT_AVAILABLE`);
64
+ this.emit(constants_1.EventType.UPDATE_NOT_AVAILABLE, { updateInfo: this.updateInfo });
65
+ }
66
+ return;
67
+ }
68
+ this.setState("idle" /* StateType.Idle */);
69
+ try {
70
+ // 新一轮更新流程,更新 TimeStamp
71
+ this.startTimeStamp = new Date().getTime();
72
+ this.setState("checking-for-update" /* StateType.CheckingForUpdate */);
73
+ this.emit(constants_1.EventType.CHECKING_FOR_UPDATE);
74
+ const updateInfoResponse = await (0, utils_1.requestUpdateInfo)(this.options);
75
+ this.updateInfo = (this.options?.responseFormatter ? this.options?.responseFormatter(updateInfoResponse) : updateInfoResponse);
76
+ const needUpdate = this.options?.needUpdate(updateInfoResponse);
77
+ if (!needUpdate) {
78
+ this.logger.info(`updateInfo is ${JSON.stringify(this.updateInfo)},needUpdate is false`);
79
+ this.emit(constants_1.EventType.UPDATE_NOT_AVAILABLE, { updateInfo: this.updateInfo });
80
+ this.setState("idle" /* StateType.Idle */);
81
+ return;
82
+ }
83
+ this.logger.info(`AppUpdator#checkForUpdates:needUpdate is true`);
84
+ this.availableUpdate = this.doGetAvailableUpdateInfo(this.updateInfo);
85
+ if (!this.options?.autoDownload) {
86
+ this.logger.info(`AppUpdator#checkForUpdates:emit UPDATE_AVAILABLE`);
87
+ this.emit(constants_1.EventType.UPDATE_AVAILABLE, {
88
+ executeType,
89
+ updateInfo: this.updateInfo,
90
+ });
91
+ return;
92
+ }
93
+ this.downloadUpdate(executeType);
94
+ }
95
+ catch (e) {
96
+ e.customMessage = e.customMessage ? e.customMessage : `${constants_1.InstallResultType.CheckForUpdatesError}_${e.message}`;
97
+ this.logError(e);
98
+ this.setState("idle" /* StateType.Idle */);
99
+ }
100
+ }
101
+ async downloadUpdate(executeType = constants_1.ExecuteType.User) {
102
+ this.logger.info(`AppUpdator#downloadUpdate:executeType is ${executeType}`);
103
+ await this.downloadUpdateFile(this.updateInfo);
104
+ const result = await this.preCheck();
105
+ if (result.success) {
106
+ this.logger.info(`AppUpdator#downloadUpdate:emit UPDATE_DOWNLOADED`);
107
+ this.emit(constants_1.EventType.UPDATE_DOWNLOADED, {
108
+ executeType,
109
+ });
110
+ }
111
+ else {
112
+ this.logError(result.error);
113
+ this.setState("idle" /* StateType.Idle */);
114
+ }
115
+ }
116
+ async quitAndInstall() {
117
+ this.logger.info(`AppUpdator#quitAndInstall:state is ${this.state}`);
118
+ if (this.state !== "downloaded" /* StateType.Downloaded */) {
119
+ return;
120
+ }
121
+ this.setState("idle" /* StateType.Idle */);
122
+ try {
123
+ let result = { success: false };
124
+ if (this.updateInfo?.updateType === constants_1.UpdateType.Package) {
125
+ result = await this.doQuitAndInstallPackage();
126
+ }
127
+ else {
128
+ result = await this.doQuitAndInstallAsar();
129
+ }
130
+ if (result.success) {
131
+ this.logger.warn(`AppUpdator#quitAndInstall:install success`);
132
+ this.emit(constants_1.EventType.BEFORE_QUIT_FOR_UPDATE);
133
+ }
134
+ else {
135
+ result.message = `error: ${result.error?.message}`;
136
+ this.dispatchError(result.error);
137
+ }
138
+ }
139
+ catch (e) {
140
+ this.dispatchError(e);
141
+ }
142
+ }
143
+ async preCheckForAsar() {
144
+ this.logger.info(`AppUpdator#preCheckForAsar`);
145
+ return await this.unzip();
146
+ }
147
+ async preCheck() {
148
+ this.logger.info(`AppUpdator#preCheck`);
149
+ const { resourcePath } = this.availableUpdate;
150
+ if (this.state !== "downloaded" /* StateType.Downloaded */) {
151
+ return {
152
+ success: false,
153
+ error: new Error(`AppUpdator#preCheck:update status(${this.state}) error`),
154
+ };
155
+ }
156
+ // 清理老包
157
+ try {
158
+ this.logger.info(`AppUpdator#preCheck:cleanOldArchive`);
159
+ await (0, utils_1.cleanOldArchive)(resourcePath);
160
+ }
161
+ catch (e) {
162
+ this.logError(e);
163
+ }
164
+ let result = { success: true };
165
+ const { downloadTargetDir } = this.availableUpdate;
166
+ try {
167
+ const hasLatestFile = await (0, utils_1.existsAsync)(downloadTargetDir);
168
+ // 下载失败返回提示
169
+ if (!hasLatestFile) {
170
+ return {
171
+ success: false,
172
+ error: new Error('file is notfound'),
173
+ };
174
+ }
175
+ }
176
+ catch (e) {
177
+ return {
178
+ success: false,
179
+ error: e,
180
+ };
181
+ }
182
+ if (this.updateInfo?.updateType === constants_1.UpdateType.Package) {
183
+ result = await this.doPreCheckForPackage();
184
+ }
185
+ else {
186
+ result = await this.preCheckForAsar();
187
+ }
188
+ return result;
189
+ }
190
+ async downloadUpdateFile(updateInfo) {
191
+ if (this.state !== "checking-for-update" /* StateType.CheckingForUpdate */) {
192
+ throw new Error(`AppUpdator#downloadUpdateFile:update status(${this.state}) error`);
193
+ }
194
+ const { url, signature } = updateInfo.files[0];
195
+ const { downloadTargetDir } = this.availableUpdate;
196
+ this.setState("downloading" /* StateType.Downloading */);
197
+ try {
198
+ await (0, download_file_1.downloadFile)({
199
+ logger: this.logger,
200
+ url,
201
+ signature,
202
+ targetDir: downloadTargetDir,
203
+ emit: this.emit,
204
+ progressHandle: (data) => {
205
+ this.emit(constants_1.EventType.UPDATE_DOWNLOAD_PROGRESS, data);
206
+ },
207
+ });
208
+ this.logger.info(`AppUpdator#downloadUpdateFile:Downloaded`);
209
+ this.setState("downloaded" /* StateType.Downloaded */);
210
+ }
211
+ catch (e) {
212
+ this.setState("idle" /* StateType.Idle */);
213
+ e.customMessage = `${constants_1.InstallResultType.DownloadError}_${e.message}`;
214
+ this.logError(e);
215
+ }
216
+ }
217
+ async unzip() {
218
+ this.logger.info(`AppUpdator#unzip:start`);
219
+ try {
220
+ const result = await this.doUnzip();
221
+ if (!result.success) {
222
+ return result;
223
+ }
224
+ const { latestAsarPath } = this.availableUpdate;
225
+ const exist = await (0, utils_1.existFile)(latestAsarPath);
226
+ return Promise.resolve({ success: exist });
227
+ }
228
+ catch (error) {
229
+ error.customMessage = constants_1.InstallResultType.UpdateUnzipError;
230
+ return Promise.resolve({
231
+ success: false,
232
+ error,
233
+ });
234
+ }
235
+ }
236
+ dispatchError(e) {
237
+ this.logError(e);
238
+ this.emit(constants_1.EventType.ERROR, e.message);
239
+ }
240
+ logError(e) {
241
+ const message = (e.stack || e).toString();
242
+ this.logger.error(message);
243
+ }
244
+ }
245
+ exports.AppUpdator = AppUpdator;
246
+ //# sourceMappingURL=app-updator.js.map
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventType = exports.DownloadProgressStatus = exports.ExecuteType = exports.UpdateType = exports.InstallResultType = exports.OldArchivePrefix = void 0;
4
+ /**
5
+ * 更新包备份前缀
6
+ */
7
+ exports.OldArchivePrefix = 'old-';
8
+ /**
9
+ * 安装结果类型
10
+ */
11
+ var InstallResultType;
12
+ (function (InstallResultType) {
13
+ InstallResultType["UpdateUnzipError"] = "update-unzip-error";
14
+ InstallResultType["DownloadError"] = "download-error";
15
+ InstallResultType["UpdateSuccess"] = "update-success";
16
+ InstallResultType["CheckForUpdatesError"] = "check-for-updates-error";
17
+ })(InstallResultType = exports.InstallResultType || (exports.InstallResultType = {}));
18
+ /**
19
+ * 更新类型
20
+ */
21
+ var UpdateType;
22
+ (function (UpdateType) {
23
+ /** 全量更新 */
24
+ UpdateType["Package"] = "package";
25
+ /** 动态更新 */
26
+ UpdateType["Asar"] = "asar";
27
+ /** 更新包更新 */
28
+ UpdateType["Assets"] = "assets";
29
+ })(UpdateType = exports.UpdateType || (exports.UpdateType = {}));
30
+ /**
31
+ * 执行类型
32
+ */
33
+ var ExecuteType;
34
+ (function (ExecuteType) {
35
+ /**
36
+ * 自动触发
37
+ */
38
+ ExecuteType["User"] = "user";
39
+ /**
40
+ * 手动触发
41
+ */
42
+ ExecuteType["Auto"] = "auto";
43
+ })(ExecuteType = exports.ExecuteType || (exports.ExecuteType = {}));
44
+ /**
45
+ * 执行类型
46
+ */
47
+ var DownloadProgressStatus;
48
+ (function (DownloadProgressStatus) {
49
+ /**
50
+ * 开始
51
+ */
52
+ DownloadProgressStatus["Begin"] = "begin";
53
+ /**
54
+ * 下载中
55
+ */
56
+ DownloadProgressStatus["Downloading"] = "downloading";
57
+ /**
58
+ * 结束
59
+ */
60
+ DownloadProgressStatus["End"] = "end";
61
+ })(DownloadProgressStatus = exports.DownloadProgressStatus || (exports.DownloadProgressStatus = {}));
62
+ /**
63
+ * 事件类型
64
+ */
65
+ var EventType;
66
+ (function (EventType) {
67
+ /**
68
+ * 下载完成
69
+ */
70
+ EventType["UPDATE_DOWNLOADED"] = "update-downloaded";
71
+ /**
72
+ * 下载进度
73
+ */
74
+ EventType["UPDATE_DOWNLOAD_PROGRESS"] = "update-download-progress";
75
+ /**
76
+ * 用户调用 quitAndInstall 之后发出,可在次事件后完成关闭窗口、重启等操作
77
+ */
78
+ EventType["BEFORE_QUIT_FOR_UPDATE"] = "before-quit-for-update";
79
+ /**
80
+ * 有可用更新
81
+ */
82
+ EventType["UPDATE_AVAILABLE"] = "update-available";
83
+ /**
84
+ * 开始检查更新
85
+ */
86
+ EventType["CHECKING_FOR_UPDATE"] = "checking-for-update";
87
+ /**
88
+ * 无可用更新
89
+ */
90
+ EventType["UPDATE_NOT_AVAILABLE"] = "update-not-available";
91
+ /**
92
+ * 错误
93
+ */
94
+ EventType["ERROR"] = "error";
95
+ })(EventType = exports.EventType || (exports.EventType = {}));
96
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ElectronAppAdapter = void 0;
4
+ const electron_1 = require("electron");
5
+ class ElectronAppAdapter {
6
+ constructor(app = electron_1.app) {
7
+ this.app = app;
8
+ }
9
+ get name() {
10
+ return this.app.getName();
11
+ }
12
+ get isPackaged() {
13
+ return this.app.isPackaged === true;
14
+ }
15
+ get userDataPath() {
16
+ return this.app.getPath('userData');
17
+ }
18
+ get exePath() {
19
+ return this.app.getPath('exe');
20
+ }
21
+ exit(code) {
22
+ this.app.exit(code);
23
+ }
24
+ relaunch(code) {
25
+ this.app.relaunch();
26
+ this.app.exit(code);
27
+ }
28
+ quit() {
29
+ this.app.quit();
30
+ }
31
+ }
32
+ exports.ElectronAppAdapter = ElectronAppAdapter;
33
+ //# sourceMappingURL=elelctron-app-adapter.js.map
package/build/index.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const constants_1 = require("./common/constants");
4
+ const mac_updator_1 = require("./mac-updator");
5
+ const windows_updator_1 = require("./windows-updator");
6
+ exports.default = {
7
+ UpdateType: constants_1.UpdateType, EventType: constants_1.EventType, ExecuteType: constants_1.ExecuteType,
8
+ MacUpdator: mac_updator_1.MacUpdator, WindowsUpdator: windows_updator_1.WindowsUpdator, DownloadProgressStatus: constants_1.DownloadProgressStatus,
9
+ };
10
+ //# sourceMappingURL=index.js.map
Binary file
Binary file
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MacUpdator = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const app_updator_1 = require("./app-updator");
9
+ const constants_1 = require("./common/constants");
10
+ const install_macos_dmg_1 = __importDefault(require("./utils/install-macos-dmg"));
11
+ const utils_1 = require("./utils");
12
+ class MacUpdator extends app_updator_1.AppUpdator {
13
+ constructor(options) {
14
+ super(options);
15
+ }
16
+ doGetAvailableUpdateInfo(updateInfo) {
17
+ this.logger.info('MacUpdator#doGetAvailableUpdateInfo:start');
18
+ const exePath = this.app.exePath;
19
+ const resourcePath = path_1.default.resolve(exePath, '..', '..', 'Resources');
20
+ const latestAsarPath = path_1.default.resolve(resourcePath, 'latest.asar');
21
+ const latestAppPath = path_1.default.resolve(resourcePath, 'latest');
22
+ let downloadTargetDir = `${latestAsarPath}.zip`;
23
+ if (updateInfo.updateType === constants_1.UpdateType.Package) {
24
+ downloadTargetDir = `${latestAppPath}.dmg`;
25
+ }
26
+ return {
27
+ resourcePath,
28
+ downloadTargetDir,
29
+ latestAsarPath,
30
+ };
31
+ }
32
+ async doPreCheckForPackage() {
33
+ this.logger.info('MacUpdator#doPreCheckForPackage:start');
34
+ // Mac 全量安装前,先进行 dmg 安装检查
35
+ return await (0, install_macos_dmg_1.default)(this.options, this.logger, this.availableUpdate, this.updateInfo, true);
36
+ }
37
+ /**
38
+ * 资源解压
39
+ * @return
40
+ */
41
+ async doUnzip() {
42
+ this.logger.info('MacUpdator#doUnzip:start');
43
+ const { resourcePath, downloadTargetDir } = this.availableUpdate;
44
+ try {
45
+ // 直接解压
46
+ await (0, utils_1.execAsync)(`unzip -o ${downloadTargetDir}`, {
47
+ cwd: resourcePath,
48
+ maxBuffer: 2 ** 28,
49
+ });
50
+ return {
51
+ success: true,
52
+ };
53
+ }
54
+ catch (error) {
55
+ return {
56
+ success: false,
57
+ error,
58
+ };
59
+ }
60
+ }
61
+ async doQuitAndInstallAsar() {
62
+ this.logger.info('MacUpdator#doQuitAndInstallAsar:start');
63
+ if (!this.availableUpdate) {
64
+ this.logger.error('MacUpdator#doQuitAndInstallAsar:not availableUpdate');
65
+ return Promise.resolve({ success: false });
66
+ }
67
+ const { resourcePath, latestAsarPath } = this.availableUpdate;
68
+ const oldAsarPath = path_1.default.resolve(resourcePath, `${constants_1.OldArchivePrefix}${new Date().getTime()}.asar`);
69
+ const currentAsarPath = path_1.default.resolve(resourcePath, 'app.asar');
70
+ try {
71
+ // 将老包改名 app.asar => old-xxxx.asar
72
+ if (await (0, utils_1.existsAsync)(currentAsarPath)) {
73
+ await (0, utils_1.renameAsync)(currentAsarPath, oldAsarPath);
74
+ }
75
+ }
76
+ catch (error) {
77
+ return {
78
+ success: false,
79
+ error,
80
+ };
81
+ }
82
+ try {
83
+ // 新包替换
84
+ await (0, utils_1.renameAsync)(latestAsarPath, currentAsarPath);
85
+ }
86
+ catch (error) {
87
+ // 替换出错,需要把老包还原
88
+ await (0, utils_1.renameAsync)(oldAsarPath, currentAsarPath);
89
+ return {
90
+ success: false,
91
+ error,
92
+ };
93
+ }
94
+ this.logger.warn('AppUpdator#quitAndInstall:install success');
95
+ this.emit(constants_1.EventType.BEFORE_QUIT_FOR_UPDATE);
96
+ // 重启应用
97
+ this.app.relaunch();
98
+ return {
99
+ success: true,
100
+ };
101
+ }
102
+ async doQuitAndInstallPackage() {
103
+ this.logger.info('AppUpdator#doQuitAndInstallPackage:start');
104
+ return await (0, install_macos_dmg_1.default)(this.options, this.logger, this.availableUpdate, this.updateInfo);
105
+ }
106
+ }
107
+ exports.MacUpdator = MacUpdator;
108
+ //# sourceMappingURL=mac-updator.js.map
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.downloadFile = void 0;
7
+ const urllib_1 = __importDefault(require("urllib"));
8
+ const constants_1 = require("../common/constants");
9
+ const _1 = require(".");
10
+ /**
11
+ * 文件下载
12
+ * @param updator 更新实例
13
+ * @param url 下载地址
14
+ * @param signature 下载签名
15
+ * @param targeDir 下载目录
16
+ * @param progressHandle 下载状态回调
17
+ * @return
18
+ */
19
+ const downloadFile = async ({ logger, url, signature, targetDir, progressHandle }) => {
20
+ logger.info('downloadFile#downloadFile (start)');
21
+ const writeStream = (0, _1.createWriteStream)(targetDir);
22
+ let currentLength = 0;
23
+ progressHandle({
24
+ status: constants_1.DownloadProgressStatus.Begin,
25
+ });
26
+ return new Promise((resolve, reject) => {
27
+ urllib_1.default
28
+ .request(url, {
29
+ streaming: true,
30
+ followRedirect: true,
31
+ timeout: 10 * 60 * 1000,
32
+ })
33
+ .then((res) => {
34
+ const totalLength = res.headers['content-length'];
35
+ logger.info(`downloadFile#downloadFile (then),totalLength is ${totalLength}`);
36
+ res.res.on('data', (data) => {
37
+ try {
38
+ currentLength += data.length;
39
+ const progress = (currentLength / totalLength) * 100;
40
+ progressHandle({
41
+ status: constants_1.DownloadProgressStatus.Downloading,
42
+ progress,
43
+ url,
44
+ signature,
45
+ data,
46
+ });
47
+ writeStream.write(data);
48
+ }
49
+ catch (e) {
50
+ reject(e);
51
+ }
52
+ });
53
+ res.res.on('end', () => {
54
+ // 推迟调用 end(): https://stackoverflow.com/a/53878933
55
+ process.nextTick(() => writeStream.end());
56
+ try {
57
+ progressHandle({
58
+ status: constants_1.DownloadProgressStatus.End,
59
+ url,
60
+ signature,
61
+ });
62
+ logger.info('download file success, url:%s, to %s', url, targetDir);
63
+ resolve();
64
+ }
65
+ catch (e) {
66
+ reject(e);
67
+ }
68
+ });
69
+ res.res.on('error', reject);
70
+ });
71
+ });
72
+ };
73
+ exports.downloadFile = downloadFile;
74
+ //# sourceMappingURL=download-file.js.map
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getExecuteFile = exports.requestUpdateInfo = exports.existFile = exports.cleanOldArchive = exports.spawnAsync = exports.waitUntil = exports.sleep = exports.getMacOSAppPath = exports.sudoPrompt = exports.existsSync = exports.createWriteStream = exports.isMac = exports.isWin = exports.execAsync = exports.rimrafAsync = exports.readdirAsync = exports.existsAsync = exports.renameAsync = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const child_process_1 = require("child_process");
9
+ const util_1 = __importDefault(require("util"));
10
+ const urllib_1 = __importDefault(require("urllib"));
11
+ const sudo_prompt_alt_1 = __importDefault(require("sudo-prompt-alt"));
12
+ exports.sudoPrompt = sudo_prompt_alt_1.default;
13
+ const rimraf_alt_1 = __importDefault(require("rimraf-alt"));
14
+ const original_fs_1 = require("original-fs");
15
+ Object.defineProperty(exports, "existsSync", { enumerable: true, get: function () { return original_fs_1.existsSync; } });
16
+ Object.defineProperty(exports, "createWriteStream", { enumerable: true, get: function () { return original_fs_1.createWriteStream; } });
17
+ const constants_1 = require("../common/constants");
18
+ exports.renameAsync = util_1.default.promisify(original_fs_1.rename);
19
+ exports.existsAsync = util_1.default.promisify(original_fs_1.exists);
20
+ exports.readdirAsync = util_1.default.promisify(original_fs_1.readdir);
21
+ exports.rimrafAsync = util_1.default.promisify(rimraf_alt_1.default);
22
+ exports.execAsync = util_1.default.promisify(child_process_1.exec);
23
+ exports.isWin = process.platform === 'win32';
24
+ exports.isMac = process.platform === 'darwin';
25
+ const getMacOSAppPath = () => {
26
+ const sep = path_1.default.sep;
27
+ const execPath = process.execPath;
28
+ const contentPath = ['', 'Contents', 'MacOS'].join(sep);
29
+ return execPath.split(contentPath)[0];
30
+ };
31
+ exports.getMacOSAppPath = getMacOSAppPath;
32
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
33
+ exports.sleep = sleep;
34
+ const waitUntil = async (handle, options = { retryTime: 10, ms: 1000 }) => {
35
+ let retryTime = 0;
36
+ const p = async () => {
37
+ const isOk = handle();
38
+ if (!isOk) {
39
+ if (retryTime === options.retryTime) {
40
+ return false;
41
+ }
42
+ retryTime++;
43
+ await (0, exports.sleep)(options.ms);
44
+ return await p();
45
+ }
46
+ return true;
47
+ };
48
+ return await p();
49
+ };
50
+ exports.waitUntil = waitUntil;
51
+ const spawnAsync = (command, options) => {
52
+ let stdout = '';
53
+ let stderr = '';
54
+ const child = (0, child_process_1.spawn)(command, options);
55
+ child.stdout.on('data', (data) => {
56
+ stdout += data;
57
+ });
58
+ child.stderr.on('data', (data) => {
59
+ stderr += data;
60
+ });
61
+ return new Promise((resolve, reject) => {
62
+ child.on('error', reject);
63
+ child.on('close', (code) => {
64
+ if (code === 0) {
65
+ return resolve(stdout.toString());
66
+ }
67
+ const err = new Error(`child exited with code ${code}`);
68
+ err.code = code;
69
+ err.stderr = stderr.toString();
70
+ reject(err);
71
+ });
72
+ });
73
+ };
74
+ exports.spawnAsync = spawnAsync;
75
+ const cleanOldArchive = async (resourcePath) => {
76
+ const resourceArray = await (0, exports.readdirAsync)(resourcePath);
77
+ if (!resourceArray || !resourceArray.length) {
78
+ return;
79
+ }
80
+ const oldArchiveSource = resourceArray.filter((item) => item.startsWith(constants_1.OldArchivePrefix));
81
+ return await Promise.all(oldArchiveSource.map(async (file) => {
82
+ const filePath = path_1.default.join(resourcePath, file);
83
+ if (await (0, exports.existsAsync)(filePath)) {
84
+ await (0, exports.rimrafAsync)(filePath);
85
+ }
86
+ }));
87
+ };
88
+ exports.cleanOldArchive = cleanOldArchive;
89
+ const existFile = async (path) => {
90
+ const _existFile = await (0, exports.waitUntil)(() => (0, original_fs_1.existsSync)(path), {
91
+ ms: 1000,
92
+ retryTime: 30,
93
+ });
94
+ return _existFile;
95
+ };
96
+ exports.existFile = existFile;
97
+ const requestUpdateInfo = async (options) => {
98
+ const { url } = options;
99
+ if (!url) {
100
+ throw new Error('request url can\'t be empty');
101
+ }
102
+ let res = null;
103
+ try {
104
+ res = await urllib_1.default.request(url, {
105
+ dataType: 'json',
106
+ timeout: 10 * 1000,
107
+ });
108
+ }
109
+ catch (e) {
110
+ throw e;
111
+ }
112
+ if (!res || res.status !== 200) {
113
+ throw new Error(`request failure,status is ${res.status}`);
114
+ }
115
+ return res.data;
116
+ };
117
+ exports.requestUpdateInfo = requestUpdateInfo;
118
+ const getExecuteFile = (file) => {
119
+ return path_1.default.join(__dirname, '..', 'libs', file);
120
+ };
121
+ exports.getExecuteFile = getExecuteFile;
122
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const os_1 = __importDefault(require("os"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const _1 = require(".");
9
+ const index_1 = require("./index");
10
+ const _log = (logger, e) => {
11
+ const message = (e.stack || e).toString();
12
+ logger.error?.(message);
13
+ };
14
+ exports.default = async (updatorOptions, logger, availableUpdate, updateInfo, preCheck = false) => {
15
+ const { productName } = updatorOptions || {};
16
+ const { downloadTargetDir } = availableUpdate || {};
17
+ const appPath = (0, _1.getMacOSAppPath)();
18
+ const tmpPath = path_1.default.join(os_1.default.tmpdir(), String(new Date().getTime())); // 本地临时文件使用的目录
19
+ const volumesPath = path_1.default.join('/Volumes', updatorOptions.dmgTitleFormatter ? updatorOptions.dmgTitleFormatter(updatorOptions, updateInfo) : productName);
20
+ const volumesAppPath = path_1.default.join(volumesPath, `${productName}.app`);
21
+ // step 1: 检测是否支持 hdiutil;
22
+ try {
23
+ const res = (await (0, index_1.spawnAsync)('which', ['hdiutil']));
24
+ if (!res.includes('/bin/hdiutil')) {
25
+ throw new Error('hdiutil not found');
26
+ }
27
+ }
28
+ catch (error) {
29
+ if (preCheck) {
30
+ _log(logger, error);
31
+ }
32
+ return {
33
+ success: false,
34
+ error,
35
+ };
36
+ }
37
+ // step 2: eject 一次 volume 下的 app,兜底,避免上一次结束时执行失败
38
+ try {
39
+ await (0, index_1.spawnAsync)('hdiutil', ['eject', volumesPath]);
40
+ }
41
+ catch (e) {
42
+ // 如果前一次更新时 eject 成功,这里文件不存在会报错,是个正常的 case,不用处理上报
43
+ }
44
+ finally {
45
+ const volumeAppNotExist = await (0, _1.waitUntil)(() => !(0, index_1.existsSync)(volumesAppPath), {
46
+ ms: 300,
47
+ retryTime: 5,
48
+ });
49
+ if (!volumeAppNotExist) {
50
+ const error = new Error('volume not exists');
51
+ if (preCheck) {
52
+ _log(logger, error);
53
+ }
54
+ return {
55
+ success: false,
56
+ error,
57
+ };
58
+ }
59
+ }
60
+ // step 3: 执行 hdiutil attach,挂载 dmg
61
+ try {
62
+ await (0, index_1.spawnAsync)('hdiutil', ['attach', downloadTargetDir]);
63
+ }
64
+ catch (error) {
65
+ _log(logger, error);
66
+ }
67
+ finally {
68
+ // 判断有没有新的 dmg 文件,没有的话提示用户重新下载
69
+ const volumeAppExist = await (0, _1.waitUntil)(() => (0, index_1.existsSync)(volumesAppPath), {
70
+ ms: 300,
71
+ retryTime: 5,
72
+ });
73
+ if (!volumeAppExist) {
74
+ const error = new Error('attach fail');
75
+ if (preCheck) {
76
+ _log(logger, error);
77
+ }
78
+ return {
79
+ success: false,
80
+ error,
81
+ };
82
+ }
83
+ }
84
+ if (preCheck) {
85
+ // 如果是预检查,直接返回
86
+ try {
87
+ await (0, index_1.spawnAsync)('hdiutil', ['eject', volumesPath]);
88
+ }
89
+ catch (error) {
90
+ _log(logger, error);
91
+ }
92
+ return {
93
+ success: true,
94
+ };
95
+ }
96
+ // step 4: 将当前目录下的 app 移到临时目录,如果后续操作失败了兜底用
97
+ try {
98
+ await (0, index_1.spawnAsync)('mv', [appPath, tmpPath]);
99
+ }
100
+ catch (error) {
101
+ error.customMessage = 'step4 mv to tmp path error';
102
+ _log(logger, error);
103
+ }
104
+ finally {
105
+ // 看临时目录文件是否移动成功
106
+ const tmpPathExist = await (0, _1.waitUntil)(() => (0, index_1.existsSync)(tmpPath), {
107
+ ms: 300,
108
+ retryTime: 5,
109
+ });
110
+ if (!tmpPathExist) {
111
+ const error = new Error('cp to tmp path fail');
112
+ return {
113
+ success: false,
114
+ error,
115
+ };
116
+ }
117
+ }
118
+ // step 5: 将新的 app 文件移动到 Applications 目录下,如果失败 or 查不到移入文件,将临时目录下的文件移动回来
119
+ try {
120
+ await (0, index_1.spawnAsync)('cp', ['-R', volumesAppPath, appPath]);
121
+ }
122
+ catch (error) {
123
+ _log(logger, error);
124
+ }
125
+ finally {
126
+ const appExist = await (0, _1.waitUntil)(() => (0, index_1.existsSync)(appPath), {
127
+ ms: 300,
128
+ retryTime: 5,
129
+ });
130
+ // 查不到新移入的 dmg,将原 dmg 再移动回来
131
+ if (!appExist) {
132
+ const error = new Error('cp to app fail');
133
+ await (0, index_1.spawnAsync)('mv', [tmpPath, appPath]);
134
+ return {
135
+ success: false,
136
+ error,
137
+ };
138
+ }
139
+ }
140
+ // step 6: 执行 hdiutil eject,推出
141
+ try {
142
+ await (0, index_1.spawnAsync)('hdiutil', ['eject', volumesPath]);
143
+ }
144
+ catch (error) {
145
+ _log(logger, error);
146
+ }
147
+ return {
148
+ success: true,
149
+ };
150
+ };
151
+ //# sourceMappingURL=install-macos-dmg.js.map
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sudoPromptExec = void 0;
4
+ const index_1 = require("./index");
5
+ function sudoPromptExec(appUpdatorOptions, logger, shell) {
6
+ const options = {
7
+ name: appUpdatorOptions.productName,
8
+ };
9
+ return new Promise((resolve, reject) => {
10
+ logger.warn(`update#sudoPromptExec_shell_${shell}`);
11
+ index_1.sudoPrompt.exec(shell, options, (error, stdout, _) => {
12
+ if (error) {
13
+ reject(error);
14
+ logger.error(`update#sudoPromptExec_error_${error}`);
15
+ return;
16
+ }
17
+ resolve(stdout);
18
+ logger.warn(`update#sudoPromptExec_stdout_${stdout}`);
19
+ });
20
+ });
21
+ }
22
+ exports.sudoPromptExec = sudoPromptExec;
23
+ //# sourceMappingURL=sudo-prompt-exec.js.map
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WindowsUpdator = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const electron_1 = require("electron");
9
+ const constants_1 = require("./common/constants");
10
+ const sudo_prompt_exec_1 = require("./utils/sudo-prompt-exec");
11
+ const app_updator_1 = require("./app-updator");
12
+ const utils_1 = require("./utils");
13
+ class WindowsUpdator extends app_updator_1.AppUpdator {
14
+ doGetAvailableUpdateInfo(updateInfo) {
15
+ this.logger.info(`WindowsUpdator#doGetAvailableUpdateInfo:start`);
16
+ let resourcePath = path_1.default.resolve(this.app.userDataPath);
17
+ const latestAsarPath = path_1.default.resolve(resourcePath, 'latest.asar');
18
+ const latestAppPath = path_1.default.resolve(resourcePath, 'latest');
19
+ let downloadTargetDir = `${latestAsarPath}.zip`;
20
+ if (updateInfo.updateType === constants_1.UpdateType.Package) {
21
+ downloadTargetDir = `${latestAppPath}.exe`;
22
+ }
23
+ return {
24
+ resourcePath,
25
+ downloadTargetDir,
26
+ latestAsarPath,
27
+ };
28
+ }
29
+ async doPreCheckForPackage() {
30
+ this.logger.info(`WindowsUpdator#doPreCheckForPackage:start`);
31
+ // Windows 全量安装默认预检正常
32
+ return Promise.resolve({ success: true });
33
+ }
34
+ async doUnzip() {
35
+ this.logger.info(`WindowsUpdator#doUnzip:start`);
36
+ try {
37
+ const { downloadTargetDir, resourcePath } = this.availableUpdate;
38
+ const unzipExe = (0, utils_1.getExecuteFile)('unzip.exe');
39
+ const executeCommand = `"${unzipExe}" -o "${downloadTargetDir}" -d "${resourcePath}"`;
40
+ await (0, utils_1.execAsync)(executeCommand);
41
+ }
42
+ catch (error) {
43
+ return {
44
+ success: false,
45
+ error,
46
+ };
47
+ }
48
+ this.logger.info(`WindowsUpdator#doUnzip:success`);
49
+ return {
50
+ success: true,
51
+ };
52
+ }
53
+ async doQuitAndInstallPackage() {
54
+ this.logger.info(`WindowsUpdator#doQuitAndInstallPackage:success`);
55
+ const { downloadTargetDir } = this.availableUpdate;
56
+ try {
57
+ // Windows 全量安装
58
+ const shellOpen = electron_1.shell.openItem || electron_1.shell.openPath;
59
+ shellOpen(downloadTargetDir);
60
+ setTimeout(() => {
61
+ this.app.exit(0);
62
+ }, 30);
63
+ return Promise.resolve({ success: true });
64
+ }
65
+ catch (error) {
66
+ return Promise.resolve({ success: false, error });
67
+ }
68
+ }
69
+ async doQuitAndInstallAsar() {
70
+ this.logger.info(`WindowsUpdator#doQuitAndInstallAsar:start`);
71
+ const productName = this.options?.productName;
72
+ const { resourcePath } = this.availableUpdate;
73
+ const exePath = this.app.exePath;
74
+ const updateExePath = (0, utils_1.getExecuteFile)('installer.exe');
75
+ const targetPath = path_1.default.resolve(exePath, '..', 'resources');
76
+ const executeCommand = `"${updateExePath}" "${targetPath}" "${resourcePath}" "${productName}.exe" "${exePath}"`;
77
+ try {
78
+ await (0, sudo_prompt_exec_1.sudoPromptExec)(this.options, this.logger, executeCommand);
79
+ return Promise.resolve({ success: true });
80
+ }
81
+ catch (error) {
82
+ return Promise.resolve({ success: false, error });
83
+ }
84
+ }
85
+ }
86
+ exports.WindowsUpdator = WindowsUpdator;
87
+ //# sourceMappingURL=windows-updator.js.map
package/package.json CHANGED
@@ -1,34 +1,64 @@
1
1
  {
2
2
  "name": "electron-updator",
3
- "version": "0.1.0",
4
- "description": "electron-updator",
5
- "keywords": ["electron-updator"],
6
- "bin": {
7
- "electron-updator": "./bin/electron-updator.js"
3
+ "version": "0.1.2",
4
+ "description": "electron-updator is a software updator management solution for Electron applications, It is convenient to complete full software update and dynamic update.",
5
+ "scripts": {
6
+ "build": "sh ./build.sh",
7
+ "test": "egg-bin test test/**/index.test.ts",
8
+ "lint": "eslint --fix --quiet --ext .js,.jsx ./src",
9
+ "prepublishOnly": "npm run build",
10
+ "contributor": "git-contributor",
11
+ "dev": "ttsc -p tsconfig.json -watch"
8
12
  },
13
+ "main": "./build",
14
+ "keywords": [
15
+ "electron",
16
+ "updator",
17
+ "auto-updator"
18
+ ],
9
19
  "files": [
10
- "bin/**/*.js",
11
- "lib/**/*.js"
20
+ "build/**/*.js",
21
+ "build/**/*.exe"
12
22
  ],
13
- "main": "index.js",
14
23
  "repository": {
15
24
  "type": "git",
16
- "url": "git://github.com/xudafeng/electron-updator.git"
25
+ "url": "https://github.com/electron-modules/electron-updator"
17
26
  },
18
27
  "dependencies": {
28
+ "lodash": "4.17.21",
29
+ "moment": "2.29.4",
30
+ "eventemitter3": "^4.0.0",
31
+ "rimraf-alt": "0.2.0",
32
+ "urllib": "2.34.1"
19
33
  },
20
34
  "devDependencies": {
21
- "eslint": "*",
22
- "eslint-plugin-mocha": "^4.11.0",
23
- "git-contributor": "1",
24
- "husky": "*",
25
- "mocha": "*",
26
- "nyc": "*"
27
- },
28
- "scripts": {
29
- "test": "nyc --reporter=lcov --reporter=text mocha",
30
- "lint": "eslint . --fix",
31
- "contributor": "git-contributor"
35
+ "@applint/spec": "^1.2.0",
36
+ "@types/lodash": "^4.14.181",
37
+ "@types/mocha": "^10.0.1",
38
+ "@types/mock-require": "^2.0.1",
39
+ "@types/react-dom": "^17.0.16",
40
+ "@typescript-eslint/parser": "^5.19.0",
41
+ "babel-plugin-module-resolver": "^4.1.0",
42
+ "concurrently": "^5.3.0",
43
+ "copyfiles": "^2.4.1",
44
+ "cross-env": "^7.0.3",
45
+ "detect-port": "1",
46
+ "egg-bin": "^5.9.0",
47
+ "electron": "18",
48
+ "electron-windows": "18",
49
+ "eslint": "^7.32.0",
50
+ "git-contributor": "*",
51
+ "husky": "^7.0.4",
52
+ "mm": "^3.0.2",
53
+ "mock-require": "^3.0.3",
54
+ "prettier": "^2.6.2",
55
+ "prop-types": "^15.7.2",
56
+ "style-loader": "^1.1.2",
57
+ "ts-loader": "^9.2.8",
58
+ "ts-node": "^10.7.0",
59
+ "ttypescript": "^1.5.15",
60
+ "typescript": "^4.6.3",
61
+ "typescript-transform-paths": "^3.3.1"
32
62
  },
33
63
  "husky": {
34
64
  "hooks": {
@@ -36,4 +66,4 @@
36
66
  }
37
67
  },
38
68
  "license": "MIT"
39
- }
69
+ }
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- 'use strict';
package/index.js DELETED
@@ -1,3 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = require('./lib/electron-updator');
@@ -1 +0,0 @@
1
- 'use strict';