1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | const fs = require('fs')
|
13 | const os = require('os')
|
14 | const path = require('path')
|
15 | const zlib = require('zlib')
|
16 | const electron = require('electron')
|
17 | const EventEmitter = require('events')
|
18 | const got = require('got')
|
19 | const semver = require('semver')
|
20 |
|
21 | const app = electron.app || electron.remote.app
|
22 |
|
23 | class Updater extends EventEmitter {
|
24 | constructor () {
|
25 | super()
|
26 | this.tasks = []
|
27 | }
|
28 |
|
29 | _fetchJson (url) {
|
30 | return got(url, { encoding: 'utf8', timeout: 1500, retries: 1, json: true, headers: this.headers })
|
31 | .then(response => Promise.resolve(response.body))
|
32 | }
|
33 |
|
34 | _fetchFile (url, name) {
|
35 | let onProgress = p => console.log(p)
|
36 | let total = 0
|
37 | let current = 0
|
38 | let timer = null
|
39 | const tempFile = path.resolve(this.options.cacheDirectory, name)
|
40 | const promise = new Promise((resolve, reject) => {
|
41 | got.stream(url, { encoding: 'utf8', timeout: 1500, retries: 1, headers: this.headers })
|
42 | .on('request', request => { timer = setTimeout(() => request && request.abort(), 2 * 60 * 1000) })
|
43 | .on('response', response => response.headers['content-length'] ? (total = parseInt(response.headers['content-length'], 10)) : onProgress(-1))
|
44 | .on('data', chunk => total ? onProgress((current += chunk.length) / total) : onProgress(-1))
|
45 | .on('end', chunk => clearTimeout(timer))
|
46 | .on('error', (error, body, response) => reject(error))
|
47 | .pipe(zlib.Gunzip())
|
48 | .pipe(fs.createWriteStream(tempFile))
|
49 |
|
50 | .on('error', (error) => reject(error))
|
51 | .on('close', () => resolve(tempFile))
|
52 | })
|
53 | promise.progress = callback => {
|
54 | onProgress = callback
|
55 | return promise
|
56 | }
|
57 | return promise
|
58 | }
|
59 |
|
60 | init (options) {
|
61 | const def = {
|
62 | tmpdir: os.tmpdir(),
|
63 | headers: {},
|
64 | name: app.getName()
|
65 | }
|
66 | this.options = Object.assign({}, def, options)
|
67 | this.options.cacheDirectory = path.resolve(this.options.tmpdir, this.options.name)
|
68 | this.options.headers['user-agent'] = this.options.headers['user-agent'] || 'asar-updater/v0.0.1 (https://github.com/zce/asar-updater)'
|
69 |
|
70 | fs.existsSync(this.options.cacheDirectory) || fs.mkdirSync(this.options.cacheDirectory)
|
71 | }
|
72 |
|
73 | setFeedURL (filename, url) {
|
74 | if (!path.isAbsolute(filename)) {
|
75 | filename = path.resolve(app.getAppPath(), filename)
|
76 | }
|
77 | const name = path.basename(filename, '.asar')
|
78 | this.tasks.push({ name, filename, url })
|
79 | }
|
80 |
|
81 | checkForUpdates () {
|
82 | this.manifest = []
|
83 | this.emit('checking-for-update')
|
84 | Promise.all(
|
85 | this.tasks
|
86 | .map(t => this._local(t))
|
87 | .map(t => this._remote(t))
|
88 | .map(p => this._compare(p))
|
89 | .map(p => this._download(p))
|
90 | )
|
91 | .then(tasks => this._allCompleted(tasks))
|
92 | .catch(error => this.emit('error', error))
|
93 | }
|
94 |
|
95 | _local (task) {
|
96 | try {
|
97 | task.local = require(path.resolve(task.filename, 'package.json'))
|
98 | if (!task.local.version) throw new Error('There is no version in the package.json')
|
99 | return task
|
100 | } catch (e) {
|
101 | if (e.code !== 'MODULE_NOT_FOUND') throw e
|
102 | throw new Error('There is no package.json in the ' + task.filename)
|
103 | }
|
104 | }
|
105 |
|
106 | _remote (task) {
|
107 | return this._fetchJson(task.url + '?v=' + Date.now())
|
108 | .then(remote => {
|
109 | task.remote = remote
|
110 | if (!task.remote.version) return Promise.reject(new Error('There is no version in the remote'))
|
111 | return task
|
112 | })
|
113 | }
|
114 |
|
115 | _compare (promise) {
|
116 | return promise.then(task => {
|
117 | task.available = semver.gt(semver.clean(task.remote.version), semver.clean(task.local.version))
|
118 | this.emit(task.available ? 'available' : 'not-available', task)
|
119 | return task
|
120 | })
|
121 | }
|
122 |
|
123 | _download (promise) {
|
124 | return promise.then(task => {
|
125 | if (!task.available) return task
|
126 | return this._fetchFile(task.remote.url + '?v=' + Date.now(), task.name)
|
127 | .progress(p => this.emit('progress', task, p))
|
128 | .then(filename => {
|
129 | this.manifest.push({ from: filename, to: task.filename })
|
130 | this.emit('downloaded', task)
|
131 | return task
|
132 | })
|
133 | })
|
134 | }
|
135 |
|
136 | _allCompleted (tasks) {
|
137 | for (let i = tasks.length - 1; i >= 0; i--) {
|
138 | if (!tasks[i].available) {
|
139 | this.emit('completed', false, tasks)
|
140 | return
|
141 | }
|
142 | }
|
143 | fs.writeFile(path.resolve(this.options.cacheDirectory, 'manifest.json'), JSON.stringify(this.manifest), 'utf8', error => {
|
144 | if (error) return fs.unlink(this.options.cacheDirectory)
|
145 | this.emit('completed', this.manifest, tasks)
|
146 | this.manifest = []
|
147 | })
|
148 | }
|
149 |
|
150 | quitAndInstall () {
|
151 | setTimeout(() => {
|
152 | app.relaunch({ args: process.argv.slice(1) + ['--relaunch'] })
|
153 | app.exit(0)
|
154 | }, 100)
|
155 | }
|
156 | }
|
157 |
|
158 | module.exports = new Updater()
|