asar-updater
Version:
Auto update asar file
171 lines (155 loc) • 5.48 kB
JavaScript
const fs = require('fs')
const os = require('os')
const path = require('path')
const zlib = require('zlib')
const crypto = require('crypto')
const electron = require('electron')
const EventEmitter = require('events')
const got = require('got')
// const unzip = require('unzip')
const semver = require('semver')
const isOnline = require('on-line')
const app = electron.app || electron.remote.app
const appName = app.getName()
const appPath = app.getAppPath()
class Updater extends EventEmitter {
constructor () {
super()
this.tasks = []
}
_fetchJson (url) {
return got(url, { encoding: 'utf8', timeout: 1500, retries: 1, json: true, headers: this.headers })
.then(response => Promise.resolve(response.body))
}
_fetchFile (url, name) {
let onProgress = p => console.log(p)
let total = 0
let current = 0
let timer = null
const tempFile = path.resolve(this.options.cacheDirectory, name)
const promise = new Promise((resolve, reject) => {
got.stream(url, { encoding: 'utf8', timeout: 1500, retries: 1, headers: this.headers })
.on('request', request => { timer = setTimeout(() => request && request.abort(), 2 * 60 * 1000) })
.on('response', response => response.headers['content-length'] ? (total = parseInt(response.headers['content-length'], 10)) : onProgress(-1))
.on('data', chunk => total ? onProgress((current += chunk.length) / total) : onProgress(-1))
.on('end', chunk => clearTimeout(timer))
.on('error', (error, body, response) => reject(error))
.pipe(zlib.Gunzip())
.pipe(fs.createWriteStream(tempFile))
// .pipe(unzip.Extract({ path: this.folder }))
.on('error', (error) => reject(error))
.on('close', () => resolve(tempFile))
})
promise.progress = callback => {
onProgress = callback
return promise
}
return promise
}
init (options) {
const def = {
tmpdir: os.tmpdir(),
headers: {},
name: appName
}
this.options = Object.assign({}, def, options)
this.options.cacheDirectory = path.resolve(this.options.tmpdir, this.options.name)
this.options.headers['user-agent'] = this.options.headers['user-agent'] || 'asar-updater/v0.0.1 (https://github.com/zce/asar-updater)'
// TODO: mkdirp
fs.existsSync(this.options.cacheDirectory) || fs.mkdirSync(this.options.cacheDirectory)
}
setFeedURL (filename, url) {
if (!path.isAbsolute(filename)) {
filename = path.resolve(appPath, filename)
}
const name = path.basename(filename, '.asar')
this.tasks.push({ name, filename, url })
}
checkForUpdates () {
isOnline((err, online) => {
if (err) return this.emit('error', err)
if (!online) return this.emit('completed', false, 'offline')
this.manifest = []
this.emit('checking-for-update')
Promise.all(
this.tasks
.map(t => this._local(t))
.map(t => this._remote(t))
.map(p => this._compare(p))
.map(p => this._download(p))
)
.then(tasks => this._allCompleted(tasks))
.catch(error => this.emit('error', error))
})
}
_local (task) {
try {
task.local = require(path.resolve(task.filename, 'package.json'))
if (!task.local.version) throw new Error('There is no version in the package.json')
return task
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') throw e
throw new Error('There is no package.json in the ' + task.filename)
}
}
_remote (task) {
return this._fetchJson(task.url + '?v=' + Date.now())
.then(remote => {
task.remote = remote
if (!task.remote.version) return Promise.reject(new Error('There is no version in the remote'))
return task
})
}
_compare (promise) {
return promise.then(task => {
task.available = semver.gt(semver.clean(task.remote.version), semver.clean(task.local.version))
this.emit(task.available ? 'available' : 'not-available', task)
return task
})
}
_getFileStamp (filename, type) {
type = type || 'sha1'
const buffer = fs.readFileSync(filename)
var hash = crypto.createHash(type)
hash.update(buffer)
return hash.digest('hex')
}
_download (promise) {
return promise.then(task => {
if (!task.available) return task
return this._fetchFile(task.remote.url + '?v=' + Date.now(), task.name)
.progress(p => this.emit('progress', task, p))
.then(filename => {
if (task.remote.sha1 === this._getFileStamp(filename)) {
this.manifest.push({ from: filename, to: task.filename })
}
this.emit('downloaded', task)
return task
})
})
}
_allCompleted (tasks) {
let updated = false
for (let i = tasks.length - 1; i >= 0; i--) {
if (tasks[i].available) {
updated = true
}
}
if (!updated) {
this.emit('completed', false, tasks)
return
}
fs.writeFile(path.resolve(this.options.cacheDirectory, 'manifest.json'), JSON.stringify(this.manifest), 'utf8', error => {
if (error) return fs.unlink(this.options.cacheDirectory)
this.emit('completed', this.manifest, tasks)
this.manifest = []
})
}
quitAndInstall (timeout) {
setTimeout(() => {
app.relaunch({ args: process.argv.slice(1) + ['--relaunch'] })
app.exit(0)
}, timeout || 100)
}
}
module.exports = new Updater()