electron-smallest-updater 0.0.2
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/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/builder.d.ts +3 -0
- package/dist/builder.js +69 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +2 -0
- package/dist/updater.d.ts +49 -0
- package/dist/updater.js +278 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +23 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 haiweilian<https://github.com/haiweilian>
|
|
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/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# electron-smallest-updater
|
|
2
|
+
|
|
3
|
+
用于最小更新 Electron Resources 的更新器。
|
|
4
|
+
|
|
5
|
+
- 与 electron-updater 有相近的配置和用法。
|
|
6
|
+
- 可手动指定更新 Resources 目录下的任意资源。
|
|
7
|
+
- 自动生成更新所需的资源压缩包和元数据文件。
|
|
8
|
+
- 支持 Windows 任意目录下的更新无需额外依赖。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install electron-smallest-updater
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 构建
|
|
17
|
+
|
|
18
|
+
利用 `electron-builder` 的 `afterPack` 钩子自动生成更新所需的资源压缩包和元数据文件。
|
|
19
|
+
|
|
20
|
+
```yml
|
|
21
|
+
# electron-builder.yml
|
|
22
|
+
afterPack: ./afterPack.js
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// afterPack.js
|
|
27
|
+
const { smallestBuilder } = require('electron-smallest-updater')
|
|
28
|
+
|
|
29
|
+
exports.default = async (context) => {
|
|
30
|
+
return smallestBuilder(context, {
|
|
31
|
+
// options
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
以上配置打包后会在指定的输出目录生成 `{productName}-{version}-smallest.zip`(资源压缩包) 和 `latest-smallest.json`(更新频道文件)。然后你可以把生成内容放到文件服务器上。
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
// latest-smallest.json
|
|
40
|
+
{
|
|
41
|
+
"version": "1.1.0",
|
|
42
|
+
"releaseFile": {
|
|
43
|
+
"url": "electron-updater-example-1.1.0-smallest.zip",
|
|
44
|
+
"size": 9334322,
|
|
45
|
+
"sha512": "28d21a3e1d15e5c5b7bbc7ee4298df66e160cd5330cdb0a135a614d5077..."
|
|
46
|
+
},
|
|
47
|
+
"releaseDate": "2024-04-08T02:23:43.648Z",
|
|
48
|
+
"releaseName": "Update 1.1.0",
|
|
49
|
+
"releaseNotes": "Update for version 1.1.0 is available"
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 配置
|
|
54
|
+
|
|
55
|
+
| 名称 | 描述 | 默认值 |
|
|
56
|
+
| --------- | ---------------- | ------------------------------------------------ |
|
|
57
|
+
| channel | 更新频道名称 | latest-smallest.json |
|
|
58
|
+
| resources | 生成压缩包的资源 | \['app.asar', 'app/**', 'app.asar.unpacked/**'\] |
|
|
59
|
+
| urlPrefix | 文件资源路径前缀 | |
|
|
60
|
+
|
|
61
|
+
## 使用
|
|
62
|
+
|
|
63
|
+
在主进程中接入更新逻辑,以下是一个可直接使用的示例。
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { dialog, BrowserWindow } from 'electron'
|
|
67
|
+
import logger from 'electron-log'
|
|
68
|
+
import ProgressBar from 'electron-progressbar'
|
|
69
|
+
import { SmallestUpdater } from 'electron-smallest-updater'
|
|
70
|
+
|
|
71
|
+
export function initSmallestUpdater(mainWindow: BrowserWindow): SmallestUpdater {
|
|
72
|
+
// 下载进度条
|
|
73
|
+
const progressBar = new ProgressBar({
|
|
74
|
+
title: '更新',
|
|
75
|
+
text: '下载更新',
|
|
76
|
+
detail: '等待下载',
|
|
77
|
+
indeterminate: false,
|
|
78
|
+
closeOnComplete: true,
|
|
79
|
+
browserWindow: {
|
|
80
|
+
show: false,
|
|
81
|
+
modal: true,
|
|
82
|
+
parent: mainWindow,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
// @ts-expect-error
|
|
86
|
+
progressBar._window.hide()
|
|
87
|
+
// @ts-expect-error
|
|
88
|
+
progressBar._window.setProgressBar(-1)
|
|
89
|
+
|
|
90
|
+
// 创建实例
|
|
91
|
+
const smallestUpdater = new SmallestUpdater({
|
|
92
|
+
logger,
|
|
93
|
+
publish: {
|
|
94
|
+
url: 'http://localhost:3000/smallest-updates',
|
|
95
|
+
},
|
|
96
|
+
autoDownload: false,
|
|
97
|
+
forceDevUpdateConfig: true,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// 更新可用
|
|
101
|
+
smallestUpdater.on('update-available', (info) => {
|
|
102
|
+
console.log('[electron-updater]', '更新可用', info)
|
|
103
|
+
const version = info.version
|
|
104
|
+
const releaseNotes = info.releaseNotes
|
|
105
|
+
dialog
|
|
106
|
+
.showMessageBox(mainWindow, {
|
|
107
|
+
title: '版本更新',
|
|
108
|
+
message: `发现新版本${version},是否更新\n\n${releaseNotes}`,
|
|
109
|
+
type: 'info',
|
|
110
|
+
buttons: ['稍后更新', '立即更新'],
|
|
111
|
+
})
|
|
112
|
+
.then(({ response }) => {
|
|
113
|
+
if (response === 1) {
|
|
114
|
+
// @ts-expect-error
|
|
115
|
+
progressBar._window.show()
|
|
116
|
+
// @ts-expect-error
|
|
117
|
+
progressBar._window.setProgressBar(0)
|
|
118
|
+
smallestUpdater.downloadUpdate() // 手动下载更新
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// 更新不可用
|
|
124
|
+
smallestUpdater.on('update-not-available', () => {
|
|
125
|
+
progressBar.close()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// 下载进度
|
|
129
|
+
smallestUpdater.on('download-progress', (progress) => {
|
|
130
|
+
console.log('[electron-updater]', '下载进度', progress)
|
|
131
|
+
progressBar.value = progress.percent * 100
|
|
132
|
+
progressBar.detail = `下载中 ${(progress.transferred / 1000 / 1000).toFixed(2)}/${(progress.total / 1000 / 1000).toFixed(2)}`
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// 下载完成
|
|
136
|
+
smallestUpdater.on('update-downloaded', (info) => {
|
|
137
|
+
console.log('[electron-updater]', '下载完成', info)
|
|
138
|
+
dialog
|
|
139
|
+
.showMessageBox(mainWindow, {
|
|
140
|
+
title: '下载完成',
|
|
141
|
+
message: `重启可应用新版本`,
|
|
142
|
+
type: 'info',
|
|
143
|
+
buttons: ['稍后重启', '立即重启'],
|
|
144
|
+
})
|
|
145
|
+
.then(({ response }) => {
|
|
146
|
+
if (response === 1) {
|
|
147
|
+
smallestUpdater.quitAndInstall() // 退出并安装重启
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// 检查更新
|
|
153
|
+
smallestUpdater.checkForUpdates()
|
|
154
|
+
|
|
155
|
+
return smallestUpdater
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 配置
|
|
160
|
+
|
|
161
|
+
| 名称 | 描述 | 默认值 |
|
|
162
|
+
| -------------------- | -------------------- | -------------------------------------- |
|
|
163
|
+
| logger | 自定义日志 | console |
|
|
164
|
+
| channel | 更新频道名称 | latest-smallest.json |
|
|
165
|
+
| publish | 发布配置 | { url: string, options?: Got.Options } |
|
|
166
|
+
| autoDownload | 是否自动下载 | true |
|
|
167
|
+
| autoInstallOnAppQuit | 是否退出时自动安装 | true |
|
|
168
|
+
| forceDevUpdateConfig | 是否允许开发环境更新 | false |
|
|
169
|
+
|
|
170
|
+
### 事件
|
|
171
|
+
|
|
172
|
+
| 名称 | 描述 | 回调 |
|
|
173
|
+
| -------------------- | ------------ | ---------------------------------------- |
|
|
174
|
+
| error | 更新错误 | (error: Error, message?: string) => void |
|
|
175
|
+
| checking-for-update | 检查更新 | () => void |
|
|
176
|
+
| update-available | 更新可用 | (info: UpdateInfo) => void |
|
|
177
|
+
| update-not-available | 更新不可用 | (info: UpdateInfo) => void |
|
|
178
|
+
| update-downloaded | 更新下载完成 | (event: UpdateDownloadedInfo) => void |
|
|
179
|
+
| download-progress | 更新下载进度 | (info: ProgressInfo) => void |
|
|
180
|
+
| update-cancelled | 更新取消 | (info: UpdateInfo) => void |
|
|
181
|
+
|
|
182
|
+
**UpdateInfo**
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
interface UpdateInfo {
|
|
186
|
+
version: string
|
|
187
|
+
releaseFile: {
|
|
188
|
+
url: string
|
|
189
|
+
size: number
|
|
190
|
+
sha512: string
|
|
191
|
+
}
|
|
192
|
+
releaseDate: string
|
|
193
|
+
releaseName?: string
|
|
194
|
+
releaseNotes?: string
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**ProgressInfo**
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
export interface ProgressInfo {
|
|
202
|
+
total: number
|
|
203
|
+
percent: number
|
|
204
|
+
transferred: number
|
|
205
|
+
}
|
|
206
|
+
```
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.smallestBuilder = void 0;
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
18
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
19
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
20
|
+
const utils_1 = require("./utils");
|
|
21
|
+
const defaultChannel = 'latest-smallest.json';
|
|
22
|
+
const defaultResources = ['app.asar', 'app/**', 'app.asar.unpacked/**'];
|
|
23
|
+
function smallestBuilder(context, options) {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
const appInfo = context.packager.appInfo;
|
|
26
|
+
const platform = context.electronPlatformName;
|
|
27
|
+
const resources = (options === null || options === void 0 ? void 0 : options.resources) || defaultResources;
|
|
28
|
+
const outChannelName = (options === null || options === void 0 ? void 0 : options.channel) || defaultChannel;
|
|
29
|
+
const outChannelPath = path_1.default.join(context.outDir, outChannelName);
|
|
30
|
+
const outResourceFileName = `${appInfo.productName}-${appInfo.version}-smallest.zip`;
|
|
31
|
+
const outResourceFilePath = path_1.default.join(context.outDir, outResourceFileName);
|
|
32
|
+
// find resources
|
|
33
|
+
let resourcesPath;
|
|
34
|
+
if (platform === 'win32') {
|
|
35
|
+
resourcesPath = path_1.default.join(context.appOutDir, 'resources');
|
|
36
|
+
}
|
|
37
|
+
else if (platform === 'darwin') {
|
|
38
|
+
resourcesPath = path_1.default.join(context.appOutDir, `${appInfo.productName}.app`, 'Contents', 'Resources');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw new Error('todo ....');
|
|
42
|
+
}
|
|
43
|
+
// write zip file
|
|
44
|
+
const zip = new adm_zip_1.default();
|
|
45
|
+
for (const resource of resources) {
|
|
46
|
+
const files = yield (0, fast_glob_1.default)(resource, {
|
|
47
|
+
cwd: resourcesPath,
|
|
48
|
+
});
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
zip.addLocalFile(path_1.default.join(resourcesPath, file), path_1.default.dirname(file));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
zip.writeZip(outResourceFilePath);
|
|
54
|
+
// write publish json
|
|
55
|
+
const publishJSON = {
|
|
56
|
+
version: appInfo.version,
|
|
57
|
+
releaseFile: {
|
|
58
|
+
url: (options === null || options === void 0 ? void 0 : options.urlPrefix) ? `${options === null || options === void 0 ? void 0 : options.urlPrefix}/${outResourceFileName}` : outResourceFileName,
|
|
59
|
+
size: (yield fs_extra_1.default.stat(outResourceFilePath)).size,
|
|
60
|
+
sha512: yield (0, utils_1.calcSha512)(outResourceFilePath),
|
|
61
|
+
},
|
|
62
|
+
releaseDate: new Date().toISOString(),
|
|
63
|
+
releaseName: `Update ${appInfo.version}`,
|
|
64
|
+
releaseNotes: `Update for version ${appInfo.version} is available`,
|
|
65
|
+
};
|
|
66
|
+
yield fs_extra_1.default.writeJSON(outChannelPath, publishJSON, { spaces: 2 });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
exports.smallestBuilder = smallestBuilder;
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/core.js
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./builder"), exports);
|
|
18
|
+
__exportStar(require("./updater"), exports);
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
20
|
+
__exportStar(require("./utils"), exports);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Options } from 'got';
|
|
2
|
+
export interface UpdateInfo {
|
|
3
|
+
version: string;
|
|
4
|
+
releaseFile: {
|
|
5
|
+
url: string;
|
|
6
|
+
size: number;
|
|
7
|
+
sha512: string;
|
|
8
|
+
};
|
|
9
|
+
releaseDate: string;
|
|
10
|
+
releaseName?: string;
|
|
11
|
+
releaseNotes?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ProgressInfo {
|
|
14
|
+
total: number;
|
|
15
|
+
percent: number;
|
|
16
|
+
transferred: number;
|
|
17
|
+
}
|
|
18
|
+
export interface UpdateDownloadedInfo extends UpdateInfo {
|
|
19
|
+
downloadFilePath: string;
|
|
20
|
+
downloadUnzipPath: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Logger {
|
|
23
|
+
info(message?: any): void;
|
|
24
|
+
warn(message?: any): void;
|
|
25
|
+
error(message?: any): void;
|
|
26
|
+
debug?(message: string): void;
|
|
27
|
+
}
|
|
28
|
+
export interface Publish {
|
|
29
|
+
url: string;
|
|
30
|
+
options?: Options;
|
|
31
|
+
provider?: 'generic';
|
|
32
|
+
}
|
|
33
|
+
export interface SmallestUpdaterOptions {
|
|
34
|
+
logger?: Logger;
|
|
35
|
+
channel?: string;
|
|
36
|
+
publish: Publish;
|
|
37
|
+
autoDownload?: boolean;
|
|
38
|
+
autoInstallOnAppQuit?: boolean;
|
|
39
|
+
forceDevUpdateConfig?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface SmallestUpdaterEvents {
|
|
42
|
+
error: (error: Error, message?: string) => void;
|
|
43
|
+
'checking-for-update': () => void;
|
|
44
|
+
'update-not-available': (info: UpdateInfo) => void;
|
|
45
|
+
'update-available': (info: UpdateInfo) => void;
|
|
46
|
+
'update-downloaded': (event: UpdateDownloadedInfo) => void;
|
|
47
|
+
'download-progress': (info: ProgressInfo) => void;
|
|
48
|
+
'update-cancelled': (info: UpdateInfo) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface SmallestBuilderOptions {
|
|
51
|
+
channel?: string;
|
|
52
|
+
resources?: string[];
|
|
53
|
+
urlPrefix?: string;
|
|
54
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TypedEmitter } from 'tiny-typed-emitter';
|
|
2
|
+
import type { SmallestUpdaterOptions, SmallestUpdaterEvents } from './types';
|
|
3
|
+
export declare class SmallestUpdater extends TypedEmitter<SmallestUpdaterEvents> {
|
|
4
|
+
private logger;
|
|
5
|
+
private channel;
|
|
6
|
+
private publish;
|
|
7
|
+
private updateInfo;
|
|
8
|
+
private downloadUrl;
|
|
9
|
+
private downloadDir;
|
|
10
|
+
private downloadFileName;
|
|
11
|
+
private downloadFilePath;
|
|
12
|
+
private downloadUnzipPath;
|
|
13
|
+
private resourcesPath;
|
|
14
|
+
private quitAndInstallCalled;
|
|
15
|
+
private autoDownload;
|
|
16
|
+
private autoInstallOnAppQuit;
|
|
17
|
+
private forceDevUpdateConfig;
|
|
18
|
+
constructor(options: SmallestUpdaterOptions);
|
|
19
|
+
/**
|
|
20
|
+
* Asks the server whether there is an update.
|
|
21
|
+
*/
|
|
22
|
+
checkForUpdates(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Start downloading update manually.
|
|
25
|
+
*/
|
|
26
|
+
downloadUpdate(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Restarts the app and installs the update after it has been downloaded.
|
|
29
|
+
* It should only be called after `update-downloaded` has been emitted.
|
|
30
|
+
*/
|
|
31
|
+
quitAndInstall(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Install on exit when quitAndInstall is not called.
|
|
34
|
+
*/
|
|
35
|
+
private addQuitHandler;
|
|
36
|
+
/**
|
|
37
|
+
* Copy download resources to application resources.
|
|
38
|
+
*/
|
|
39
|
+
private updateResources;
|
|
40
|
+
/**
|
|
41
|
+
* Format URL and add prefix.
|
|
42
|
+
*/
|
|
43
|
+
private formatDownloadUrl;
|
|
44
|
+
/**
|
|
45
|
+
* Check for write permission by writing files to the folder.
|
|
46
|
+
* Why not fs.access: https://github.com/nodejs/node/issues/34395.
|
|
47
|
+
*/
|
|
48
|
+
private checkFolderWritePermission;
|
|
49
|
+
}
|
package/dist/updater.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SmallestUpdater = void 0;
|
|
16
|
+
const node_child_process_1 = require("node:child_process");
|
|
17
|
+
const node_fs_1 = require("node:fs");
|
|
18
|
+
const node_path_1 = require("node:path");
|
|
19
|
+
const promises_1 = require("node:stream/promises");
|
|
20
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
21
|
+
const electron_1 = require("electron");
|
|
22
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
23
|
+
const got_1 = __importDefault(require("got"));
|
|
24
|
+
const semver_1 = __importDefault(require("semver"));
|
|
25
|
+
const tiny_typed_emitter_1 = require("tiny-typed-emitter");
|
|
26
|
+
const utils_1 = require("./utils");
|
|
27
|
+
class SmallestUpdater extends tiny_typed_emitter_1.TypedEmitter {
|
|
28
|
+
constructor(options) {
|
|
29
|
+
var _a, _b, _c, _d, _e;
|
|
30
|
+
super();
|
|
31
|
+
this.downloadUrl = '';
|
|
32
|
+
this.downloadDir = '';
|
|
33
|
+
this.downloadFileName = 'latest-smallest.zip';
|
|
34
|
+
this.downloadFilePath = '';
|
|
35
|
+
this.downloadUnzipPath = '';
|
|
36
|
+
this.resourcesPath = '';
|
|
37
|
+
this.quitAndInstallCalled = false;
|
|
38
|
+
this.autoDownload = true;
|
|
39
|
+
this.autoInstallOnAppQuit = true;
|
|
40
|
+
this.forceDevUpdateConfig = false;
|
|
41
|
+
this.logger = (_a = options.logger) !== null && _a !== void 0 ? _a : console;
|
|
42
|
+
this.channel = (_b = options.channel) !== null && _b !== void 0 ? _b : 'latest-smallest.json';
|
|
43
|
+
this.publish = options.publish;
|
|
44
|
+
this.autoDownload = (_c = options.autoDownload) !== null && _c !== void 0 ? _c : true;
|
|
45
|
+
this.autoInstallOnAppQuit = (_d = options.autoInstallOnAppQuit) !== null && _d !== void 0 ? _d : true;
|
|
46
|
+
this.forceDevUpdateConfig = (_e = options.forceDevUpdateConfig) !== null && _e !== void 0 ? _e : false;
|
|
47
|
+
this.downloadDir = electron_1.app.getPath('userData');
|
|
48
|
+
this.resourcesPath = process.resourcesPath;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Asks the server whether there is an update.
|
|
52
|
+
*/
|
|
53
|
+
checkForUpdates() {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
if (!electron_1.app.isPackaged) {
|
|
56
|
+
if (!this.forceDevUpdateConfig) {
|
|
57
|
+
this.logger.info('Skip checkForUpdates because application is not packed and dev update config is not forced');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// To avoid affecting application parsing errors
|
|
61
|
+
// Update development environment resources to virtual directory(electron-smallest-updater-dev)
|
|
62
|
+
this.resourcesPath = (0, node_path_1.join)(this.resourcesPath, 'electron-smallest-updater-dev');
|
|
63
|
+
}
|
|
64
|
+
const url = this.publish.url;
|
|
65
|
+
if (!url)
|
|
66
|
+
return;
|
|
67
|
+
this.logger.info('Checking for update');
|
|
68
|
+
// check
|
|
69
|
+
let updateInfo;
|
|
70
|
+
try {
|
|
71
|
+
this.emit('checking-for-update');
|
|
72
|
+
updateInfo = yield (0, got_1.default)(Object.assign(Object.assign({}, this.publish), { url: `${url}/${this.channel}` })).json();
|
|
73
|
+
if (!(updateInfo === null || updateInfo === void 0 ? void 0 : updateInfo.version) || !updateInfo.releaseFile) {
|
|
74
|
+
throw new Error('Invalid response content');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
this.emit('error', error, `Cannot check for updates: ${error.message}`);
|
|
79
|
+
this.logger.error(error.stack || error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
// diff
|
|
83
|
+
const latestVersion = updateInfo.version;
|
|
84
|
+
const currentVersion = electron_1.app.getVersion();
|
|
85
|
+
if (semver_1.default.gt(latestVersion, currentVersion)) {
|
|
86
|
+
this.emit('update-available', updateInfo);
|
|
87
|
+
this.logger.info(`Update for version ${currentVersion} is available (latest version: ${latestVersion}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
this.emit('update-not-available', updateInfo);
|
|
91
|
+
this.logger.info(`Update for version ${currentVersion} is not available (latest version: ${latestVersion}`);
|
|
92
|
+
}
|
|
93
|
+
this.updateInfo = updateInfo;
|
|
94
|
+
this.downloadUrl = this.formatDownloadUrl(updateInfo.releaseFile.url);
|
|
95
|
+
if (this.autoDownload) {
|
|
96
|
+
this.logger.info('Download triggered by autoDownload');
|
|
97
|
+
yield this.downloadUpdate();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Start downloading update manually.
|
|
103
|
+
*/
|
|
104
|
+
downloadUpdate() {
|
|
105
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
var _a;
|
|
107
|
+
const downloadUrl = this.downloadUrl;
|
|
108
|
+
if (!downloadUrl)
|
|
109
|
+
return;
|
|
110
|
+
this.logger.info(`Downloading update from ${downloadUrl}`);
|
|
111
|
+
// clean
|
|
112
|
+
yield fs_extra_1.default.ensureDir(this.downloadDir);
|
|
113
|
+
const downloadFilePath = (0, node_path_1.join)(this.downloadDir, this.downloadFileName);
|
|
114
|
+
if (yield fs_extra_1.default.pathExists(downloadFilePath)) {
|
|
115
|
+
yield fs_extra_1.default.remove(downloadFilePath);
|
|
116
|
+
}
|
|
117
|
+
const downloadUnzipPath = (0, node_path_1.join)(this.downloadDir, (0, node_path_1.basename)(this.downloadFileName, '.zip'));
|
|
118
|
+
if (yield fs_extra_1.default.pathExists(downloadUnzipPath)) {
|
|
119
|
+
// https://github.com/isaacs/rimraf/issues/203
|
|
120
|
+
// https://www.electronjs.org/docs/latest/tutorial/asar-archives#treating-an-asar-archive-as-a-normal-file
|
|
121
|
+
process.noAsar = true;
|
|
122
|
+
yield fs_extra_1.default.remove(downloadUnzipPath);
|
|
123
|
+
process.noAsar = false;
|
|
124
|
+
}
|
|
125
|
+
// download
|
|
126
|
+
try {
|
|
127
|
+
this.logger.info(`Download to ${downloadFilePath}`);
|
|
128
|
+
const resStream = got_1.default.stream(downloadUrl);
|
|
129
|
+
const writeStream = (0, node_fs_1.createWriteStream)(downloadFilePath);
|
|
130
|
+
resStream.on('downloadProgress', (progress) => {
|
|
131
|
+
this.emit('download-progress', progress);
|
|
132
|
+
});
|
|
133
|
+
yield (0, promises_1.pipeline)(resStream, writeStream);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.emit('error', error, `Download error: ${error.message}`);
|
|
137
|
+
this.logger.error(error.stack || error);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
// validate
|
|
141
|
+
try {
|
|
142
|
+
const sha512 = (_a = this.updateInfo) === null || _a === void 0 ? void 0 : _a.releaseFile.sha512;
|
|
143
|
+
this.logger.info(`Validate sha512 ${sha512}`);
|
|
144
|
+
const result = yield (0, utils_1.calcSha512)(downloadFilePath);
|
|
145
|
+
if (sha512 !== result) {
|
|
146
|
+
throw new Error('Verification of sha512 failed, file changed');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
this.emit('error', error, `Validate error: ${error.message}`);
|
|
151
|
+
this.logger.error(error.stack || error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
// unzip
|
|
155
|
+
try {
|
|
156
|
+
this.logger.info(`Extract to ${downloadUnzipPath}`);
|
|
157
|
+
const zip = new adm_zip_1.default(downloadFilePath);
|
|
158
|
+
zip.extractAllTo(downloadUnzipPath, true);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
this.emit('error', error, `Extract error: ${error.message}`);
|
|
162
|
+
this.logger.error(error.stack || error);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
this.downloadFilePath = downloadFilePath;
|
|
166
|
+
this.downloadUnzipPath = downloadUnzipPath;
|
|
167
|
+
this.emit('update-downloaded', Object.assign(Object.assign({}, this.updateInfo), { downloadFilePath: this.downloadFilePath, downloadUnzipPath: this.downloadUnzipPath }));
|
|
168
|
+
this.addQuitHandler();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Restarts the app and installs the update after it has been downloaded.
|
|
173
|
+
* It should only be called after `update-downloaded` has been emitted.
|
|
174
|
+
*/
|
|
175
|
+
quitAndInstall() {
|
|
176
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
177
|
+
try {
|
|
178
|
+
this.logger.info('Auto install update on call quitAndInstall');
|
|
179
|
+
this.updateResources();
|
|
180
|
+
this.quitAndInstallCalled = true;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.emit('error', error, `Install error: ${error.message}`);
|
|
184
|
+
this.logger.error(error.stack || error);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
// quit and relaunch
|
|
188
|
+
electron_1.app.relaunch();
|
|
189
|
+
electron_1.app.quit();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Install on exit when quitAndInstall is not called.
|
|
194
|
+
*/
|
|
195
|
+
addQuitHandler() {
|
|
196
|
+
electron_1.app.once('quit', (_, exitCode) => {
|
|
197
|
+
if (this.quitAndInstallCalled) {
|
|
198
|
+
this.logger.info('Update installer has already been triggered. Quitting application.');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!this.autoInstallOnAppQuit) {
|
|
202
|
+
this.logger.info('Update will not be installed on quit because autoInstallOnAppQuit is set to false.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (exitCode !== 0) {
|
|
206
|
+
this.logger.info(`Update will be not installed on quit because application is quitting with exit code ${exitCode}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.logger.info('Auto install update on quit');
|
|
210
|
+
this.updateResources();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Copy download resources to application resources.
|
|
215
|
+
*/
|
|
216
|
+
updateResources() {
|
|
217
|
+
let cmd;
|
|
218
|
+
let args;
|
|
219
|
+
let options;
|
|
220
|
+
if (process.platform !== 'win32') {
|
|
221
|
+
cmd = 'cp';
|
|
222
|
+
args = ['-r', '-f', '-v', `${this.downloadUnzipPath}/.`, `${this.resourcesPath}/`];
|
|
223
|
+
options = { stdio: 'ignore' };
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
cmd = 'Start-Process';
|
|
227
|
+
args = [
|
|
228
|
+
'-FilePath cmd',
|
|
229
|
+
'-WindowStyle hidden',
|
|
230
|
+
`-ArgumentList "/c xcopy /i /s /y \`"${this.downloadUnzipPath}\`" \`"${this.resourcesPath}\`""`,
|
|
231
|
+
// '-Verb RunAs',
|
|
232
|
+
];
|
|
233
|
+
options = { shell: 'powershell', stdio: 'ignore' };
|
|
234
|
+
// If can write run using C:\Users\user\cmd.exe
|
|
235
|
+
// If not can write run using C:\WINDOWS\system32\cmd.exe
|
|
236
|
+
if (!this.checkFolderWritePermission(this.resourcesPath)) {
|
|
237
|
+
args.push('-Verb RunAs');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this.logger.info(`Start update process. Command:${cmd}, Args:${args.join(' ')}`);
|
|
241
|
+
try {
|
|
242
|
+
const childProcess = (0, node_child_process_1.spawnSync)(cmd, args, options);
|
|
243
|
+
if (childProcess.status === 1) {
|
|
244
|
+
throw new Error('The operation has been canceled by the user');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
throw new Error('Start shell process Error: ' + error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Format URL and add prefix.
|
|
253
|
+
*/
|
|
254
|
+
formatDownloadUrl(fileUrl) {
|
|
255
|
+
if (/^https?:\/\/.*/.test(fileUrl)) {
|
|
256
|
+
return fileUrl;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
return `${this.publish.url}/${fileUrl}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Check for write permission by writing files to the folder.
|
|
264
|
+
* Why not fs.access: https://github.com/nodejs/node/issues/34395.
|
|
265
|
+
*/
|
|
266
|
+
checkFolderWritePermission(folder) {
|
|
267
|
+
try {
|
|
268
|
+
const file = (0, node_path_1.join)(folder, 'permission.txt');
|
|
269
|
+
fs_extra_1.default.writeFileSync(file, 'check folder write permission.');
|
|
270
|
+
fs_extra_1.default.removeSync(file);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
exports.SmallestUpdater = SmallestUpdater;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function calcSha512(filePath: string): Promise<string>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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.calcSha512 = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
function calcSha512(filePath) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const hash = crypto_1.default.createHash('sha512');
|
|
12
|
+
const readStream = fs_1.default.createReadStream(filePath);
|
|
13
|
+
readStream.pipe(hash);
|
|
14
|
+
readStream.on('end', () => {
|
|
15
|
+
const sha512Hash = hash.digest('hex');
|
|
16
|
+
resolve(sha512Hash);
|
|
17
|
+
});
|
|
18
|
+
readStream.on('error', (err) => {
|
|
19
|
+
reject(err);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
exports.calcSha512 = calcSha512;
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "electron-smallest-updater",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Electron Resources 按需最小更新、自动构建发布包及信息。",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"electron",
|
|
12
|
+
"electron-updater",
|
|
13
|
+
"electron-app-updater",
|
|
14
|
+
"electron-asar-updater",
|
|
15
|
+
"electron-unpacked-updater",
|
|
16
|
+
"electron-smallest-updater"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "tsc -w",
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"release": "tsc && release-it"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "haiweilian@foxmail.com",
|
|
25
|
+
"bugs": "https://github.com/haiweilian/electron-smallest-updater/issues",
|
|
26
|
+
"homepage": "https://github.com/haiweilian/electron-smallest-updater#readme",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/haiweilian/electron-smallest-updater.git"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"adm-zip": "^0.5.10",
|
|
33
|
+
"fast-glob": "^3.3.2",
|
|
34
|
+
"fs-extra": "^11.2.0",
|
|
35
|
+
"got": "^11.0.0",
|
|
36
|
+
"semver": "^7.6.0",
|
|
37
|
+
"tiny-typed-emitter": "^2.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@bfehub/eslint-config-typescript": "^2.1.0",
|
|
41
|
+
"@release-it/conventional-changelog": "^8.0.1",
|
|
42
|
+
"@types/adm-zip": "^0.5.5",
|
|
43
|
+
"@types/fs-extra": "^11.0.4",
|
|
44
|
+
"@types/semver": "^7.5.8",
|
|
45
|
+
"electron": "^28.2.0",
|
|
46
|
+
"electron-builder": "^24.9.1",
|
|
47
|
+
"eslint": "^8.56.0",
|
|
48
|
+
"prettier": "^3.2.4",
|
|
49
|
+
"release-it": "^17.1.1",
|
|
50
|
+
"typescript": "^5.4.2"
|
|
51
|
+
}
|
|
52
|
+
}
|