electron-incremental-update 0.8.4 → 0.8.6
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/README.md +240 -234
- package/dist/{chunk-GXZSAUBR.mjs → chunk-5BZLJPHJ.mjs} +8 -1
- package/dist/chunk-CMBFI77K.mjs +75 -0
- package/dist/chunk-MFFH2NRM.mjs +118 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +42 -41
- package/dist/index.mjs +17 -19
- package/dist/{updateJson.d.mts → updateJson-7e45d9e1.d.ts} +1 -2
- package/dist/utils.d.mts +31 -16
- package/dist/utils.d.ts +31 -16
- package/dist/utils.js +76 -71
- package/dist/utils.mjs +13 -10
- package/dist/vite.d.mts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +20 -22
- package/dist/vite.mjs +16 -16
- package/package.json +10 -9
- package/dist/chunk-2JVXVTC5.mjs +0 -9
- package/dist/chunk-CR6HTU6P.mjs +0 -170
- package/dist/chunk-ZFXKCRJC.mjs +0 -11
- package/dist/updateJson.d.ts +0 -12
- package/dist/updateJson.js +0 -33
- package/dist/updateJson.mjs +0 -7
package/README.md
CHANGED
|
@@ -1,234 +1,240 @@
|
|
|
1
|
-
## electron incremental updater
|
|
2
|
-
|
|
3
|
-
This project provide a vite plugin, `Updater` class and some useful functions to generate incremental update.
|
|
4
|
-
|
|
5
|
-
There will be two asar in production, `app.asar` and `main.asar` (if "main" is your app's name).
|
|
6
|
-
|
|
7
|
-
The `app.asar` is used to load `main.asar` and initialize the `updater`. Also, all the **native modules**, which are set as `dependencies` in `package.json`, will be packaged into `app.asar` by `electron-builder`, [see usage](#use-native-modules).
|
|
8
|
-
|
|
9
|
-
The new `main.asar` downloaded from remote will be verified by presigned RSA + Signature. When pass the check and restart, the old `main.asar` will be replaced by the new one. Hooks like `beforeDoUpdate` are provided.
|
|
10
|
-
|
|
11
|
-
- inspired by Obsidian's update strategy
|
|
12
|
-
|
|
13
|
-
### notice
|
|
14
|
-
|
|
15
|
-
- this plugin is developed with [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron), and may be effect in other electron vite frameworks
|
|
16
|
-
- **all options are documented in the jsdoc**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
├──
|
|
41
|
-
├──
|
|
42
|
-
├──
|
|
43
|
-
│ ├──
|
|
44
|
-
|
|
45
|
-
└──
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const { cdnPrefix:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
repository,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
'app.
|
|
143
|
-
'!**/{.
|
|
144
|
-
'
|
|
145
|
-
'
|
|
146
|
-
'!**/*.{
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
import
|
|
178
|
-
import {
|
|
179
|
-
import {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
console.log(
|
|
185
|
-
console.log(`\
|
|
186
|
-
console.log(`\
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
updater.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
db
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
1
|
+
## electron incremental updater
|
|
2
|
+
|
|
3
|
+
This project provide a vite plugin, `Updater` class and some useful functions to generate incremental update.
|
|
4
|
+
|
|
5
|
+
There will be two asar in production, `app.asar` and `main.asar` (if "main" is your app's name).
|
|
6
|
+
|
|
7
|
+
The `app.asar` is used to load `main.asar` and initialize the `updater`. Also, all the **native modules**, which are set as `dependencies` in `package.json`, will be packaged into `app.asar` by `electron-builder`, [see usage](#use-native-modules).
|
|
8
|
+
|
|
9
|
+
The new `main.asar` downloaded from remote will be verified by presigned RSA + Signature. When pass the check and restart, the old `main.asar` will be replaced by the new one. Hooks like `beforeDoUpdate` are provided.
|
|
10
|
+
|
|
11
|
+
- inspired by Obsidian's update strategy
|
|
12
|
+
|
|
13
|
+
### notice
|
|
14
|
+
|
|
15
|
+
- this plugin is developed with [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron), and may be effect in other electron vite frameworks
|
|
16
|
+
- **all options are documented in the jsdoc**
|
|
17
|
+
|
|
18
|
+
## install
|
|
19
|
+
|
|
20
|
+
### npm
|
|
21
|
+
```bash
|
|
22
|
+
npm install electron-incremental-update
|
|
23
|
+
```
|
|
24
|
+
### yarn
|
|
25
|
+
```bash
|
|
26
|
+
yarn add electron-incremental-update
|
|
27
|
+
```
|
|
28
|
+
### pnpm
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add electron-incremental-update
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## setup
|
|
34
|
+
|
|
35
|
+
base on [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
electron
|
|
39
|
+
├── app.ts // <- add app entry file
|
|
40
|
+
├── electron-env.d.ts
|
|
41
|
+
├── main
|
|
42
|
+
│ ├── db.ts
|
|
43
|
+
│ ├── index.ts
|
|
44
|
+
└── preload
|
|
45
|
+
└── index.ts
|
|
46
|
+
src
|
|
47
|
+
└── ...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### setup app
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// electron/app.ts
|
|
54
|
+
import { getGithubReleaseCdnGroup, initApp, parseGithubCdnURL } from 'electron-incremental-update'
|
|
55
|
+
import { name, repository } from '../package.json'
|
|
56
|
+
|
|
57
|
+
const SIGNATURE_CERT = '' // auto generate certificate when start app
|
|
58
|
+
|
|
59
|
+
const { cdnPrefix: asarPrefix } = getGithubReleaseCdnGroup()[0]
|
|
60
|
+
const { cdnPrefix: jsonPrefix } = getGithubFileCdnGroup()[0]
|
|
61
|
+
initApp({ onStart: console.log })
|
|
62
|
+
// can be updater option or function that return updater
|
|
63
|
+
.setUpdater({
|
|
64
|
+
SIGNATURE_CERT,
|
|
65
|
+
productName: name,
|
|
66
|
+
repository,
|
|
67
|
+
updateJsonURL: parseGithubCdnURL(repository, jsonPrefix, 'version.json'),
|
|
68
|
+
releaseAsarURL: parseGithubCdnURL(repository, asarPrefix, `download/latest/${name}.asar.gz`),
|
|
69
|
+
receiveBeta: true
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### setup vite.config.ts
|
|
74
|
+
|
|
75
|
+
make sure the plugin is set in the **last** build task
|
|
76
|
+
|
|
77
|
+
- for `vite-plugin-electron`, set it to `preload` (the second object in the plugin option array)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// vite.config.ts
|
|
81
|
+
export default defineConfig(({ command }) => {
|
|
82
|
+
|
|
83
|
+
const isBuild = command === 'build'
|
|
84
|
+
// ...
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
plugins: [
|
|
88
|
+
electron([
|
|
89
|
+
// main
|
|
90
|
+
{
|
|
91
|
+
// ...
|
|
92
|
+
},
|
|
93
|
+
// preload
|
|
94
|
+
{
|
|
95
|
+
// ...
|
|
96
|
+
vite: {
|
|
97
|
+
plugins: [
|
|
98
|
+
updater({
|
|
99
|
+
productName: pkg.name,
|
|
100
|
+
version: pkg.version,
|
|
101
|
+
isBuild,
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
// ...
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
// when using vite-plugin-electron-renderer
|
|
108
|
+
{
|
|
109
|
+
// ...
|
|
110
|
+
}
|
|
111
|
+
]),
|
|
112
|
+
// ...
|
|
113
|
+
],
|
|
114
|
+
// ...
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### modify package.json
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
// ...
|
|
124
|
+
"main": "app.js" // <- app entry file path
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### config electron-builder
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
const { name } = require('./package.json')
|
|
132
|
+
|
|
133
|
+
const target = `${name}.asar`
|
|
134
|
+
/**
|
|
135
|
+
* @type {import('electron-builder').Configuration}
|
|
136
|
+
*/
|
|
137
|
+
module.exports = {
|
|
138
|
+
appId: 'YourAppID',
|
|
139
|
+
productName: name,
|
|
140
|
+
files: [
|
|
141
|
+
'app.js', // <- app entry file
|
|
142
|
+
'!**/{.eslintignore,.eslintrc.cjs,.editorconfig,.prettierignore,.prettierrc.yaml,dev-app-update.yml,LICENSE,.nvmrc,.npmrc}',
|
|
143
|
+
'!**/{tsconfig.json,tsconfig.node.json,tsconfig.web.json}',
|
|
144
|
+
'!**/*debug*.*',
|
|
145
|
+
'!**/*.{md,zip,map}',
|
|
146
|
+
'!**/*.{c,cpp,h,hpp,cc,hh,cxx,hxx,gypi,gyp,sh}',
|
|
147
|
+
'!**/.{github,vscode}',
|
|
148
|
+
'!node_modules/**/better-sqlite3/deps/**',
|
|
149
|
+
],
|
|
150
|
+
asarUnpack: [
|
|
151
|
+
'**/*.{node,dll}',
|
|
152
|
+
],
|
|
153
|
+
directories: {
|
|
154
|
+
output: 'release',
|
|
155
|
+
},
|
|
156
|
+
extraResources: [
|
|
157
|
+
{ from: `release/${target}`, to: target }, // <- asar file
|
|
158
|
+
],
|
|
159
|
+
publish: null, // <- disable publish
|
|
160
|
+
// ...
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Usage
|
|
165
|
+
|
|
166
|
+
### use in main process
|
|
167
|
+
|
|
168
|
+
To use electron's `net` module for updating, the `checkUpdate` and `download` functions must be called after the app is ready by default.
|
|
169
|
+
|
|
170
|
+
However, you have the option to customize the download function when creating the updater.
|
|
171
|
+
|
|
172
|
+
**NOTE: There can only be one function and should be default export in the entry file**
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// electron/main/index.ts
|
|
176
|
+
import type { StartupWithUpdater, Updater } from 'electron-incremental-update'
|
|
177
|
+
import { getAppVersion, getElectronVersion, getProductAsarPath } from 'electron-incremental-update/utils'
|
|
178
|
+
import { app } from 'electron'
|
|
179
|
+
import { name } from '../../package.json'
|
|
180
|
+
|
|
181
|
+
const startup: StartupWithUpdater = (updater: Updater) => {
|
|
182
|
+
await app.whenReady()
|
|
183
|
+
console.log('\ncurrent:')
|
|
184
|
+
console.log(`\tasar path: ${getProductAsarPath(name)}`)
|
|
185
|
+
console.log(`\tapp: ${getAppVersion(name)}`)
|
|
186
|
+
console.log(`\telectron: ${getElectronVersion()}`)
|
|
187
|
+
updater.onDownloading = ({ percent }) => {
|
|
188
|
+
console.log(percent)
|
|
189
|
+
}
|
|
190
|
+
updater.logger = console
|
|
191
|
+
updater.checkUpdate().then(async (result) => {
|
|
192
|
+
if (result === undefined) {
|
|
193
|
+
console.log('Update Unavailable')
|
|
194
|
+
} else if (result instanceof Error) {
|
|
195
|
+
console.error(result)
|
|
196
|
+
} else {
|
|
197
|
+
console.log('new version: ', result.version)
|
|
198
|
+
const { response } = await dialog.showMessageBox({
|
|
199
|
+
type: 'info',
|
|
200
|
+
buttons: ['Download', 'Later'],
|
|
201
|
+
message: 'Application update available!',
|
|
202
|
+
})
|
|
203
|
+
response === 0 && console.log(await updater.download())
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
export default startup
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### use native modules
|
|
211
|
+
|
|
212
|
+
the native modules is packed in `app.asar`, so you cannot directly access it when in production
|
|
213
|
+
|
|
214
|
+
to use it, you can prebundle native modules, or use `requireNative` to load.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
// db.ts
|
|
218
|
+
import { isNoSuchNativeModuleError, requireNative } from 'electron-incremental-update/utils'
|
|
219
|
+
|
|
220
|
+
const Database = requireNative<typeof import('better-sqlite3')>('better-sqlite3')
|
|
221
|
+
if (isNoSuchNativeModuleError(Database)) {
|
|
222
|
+
// ...
|
|
223
|
+
}
|
|
224
|
+
const db = new Database(':memory:')
|
|
225
|
+
db.exec(
|
|
226
|
+
'DROP TABLE IF EXISTS employees; '
|
|
227
|
+
+ 'CREATE TABLE IF NOT EXISTS employees (name TEXT, salary INTEGER)',
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
db.prepare('INSERT INTO employees VALUES (:n, :s)').run({
|
|
231
|
+
n: 'James',
|
|
232
|
+
s: 50000,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const r = db.prepare('SELECT * from employees').all()
|
|
236
|
+
console.log(r)
|
|
237
|
+
// [ { name: 'James', salary: 50000 } ]
|
|
238
|
+
|
|
239
|
+
db.close()
|
|
240
|
+
```
|
|
@@ -30,7 +30,14 @@ var verify = (buffer, signature2, cert) => {
|
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
// src/updateJson.ts
|
|
34
|
+
function isUpdateJSON(json) {
|
|
35
|
+
const is = (j) => "signature" in j && "version" in j && "size" in j && "minimumVersion" in j;
|
|
36
|
+
return is(json) && "beta" in json && is(json.beta);
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export {
|
|
34
40
|
signature,
|
|
35
|
-
verify
|
|
41
|
+
verify,
|
|
42
|
+
isUpdateJSON
|
|
36
43
|
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/utils/version.ts
|
|
10
|
+
function parseVersion(version) {
|
|
11
|
+
const semver = /^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9\.-]+))?/i;
|
|
12
|
+
const match = semver.exec(version);
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new TypeError(`invalid version: ${version}`);
|
|
15
|
+
}
|
|
16
|
+
const [major, minor, patch] = match.slice(1, 4).map(Number);
|
|
17
|
+
const ret = {
|
|
18
|
+
major,
|
|
19
|
+
minor,
|
|
20
|
+
patch,
|
|
21
|
+
stage: "",
|
|
22
|
+
stageVersion: -1
|
|
23
|
+
};
|
|
24
|
+
if (match[4]) {
|
|
25
|
+
let [stage, _v] = match[4].split(".");
|
|
26
|
+
ret.stage = stage;
|
|
27
|
+
ret.stageVersion = Number(_v) || -1;
|
|
28
|
+
}
|
|
29
|
+
if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch) || Number.isNaN(ret.stageVersion)) {
|
|
30
|
+
throw new TypeError(`invalid version: ${version}`);
|
|
31
|
+
}
|
|
32
|
+
return ret;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/utils/zip.ts
|
|
36
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
37
|
+
import { gunzip, gzip } from "node:zlib";
|
|
38
|
+
async function unzipFile(gzipPath, targetFilePath = gzipPath.slice(0, -3)) {
|
|
39
|
+
if (!existsSync(gzipPath)) {
|
|
40
|
+
throw new Error(`path to zipped file not exist: ${gzipPath}`);
|
|
41
|
+
}
|
|
42
|
+
const compressedBuffer = readFileSync(gzipPath);
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
gunzip(compressedBuffer, (err, buffer) => {
|
|
45
|
+
rmSync(gzipPath);
|
|
46
|
+
if (err) {
|
|
47
|
+
reject(err);
|
|
48
|
+
}
|
|
49
|
+
writeFileSync(targetFilePath, buffer);
|
|
50
|
+
resolve(null);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function zipFile(filePath, targetFilePath = `${filePath}.gz`) {
|
|
55
|
+
if (!existsSync(filePath)) {
|
|
56
|
+
throw new Error(`path to be zipped not exist: ${filePath}`);
|
|
57
|
+
}
|
|
58
|
+
const buffer = readFileSync(filePath);
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
gzip(buffer, (err, buffer2) => {
|
|
61
|
+
if (err) {
|
|
62
|
+
reject(err);
|
|
63
|
+
}
|
|
64
|
+
writeFileSync(targetFilePath, buffer2);
|
|
65
|
+
resolve(null);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
__require,
|
|
72
|
+
parseVersion,
|
|
73
|
+
unzipFile,
|
|
74
|
+
zipFile
|
|
75
|
+
};
|