wowa
Version:
Manage World of Warcraft addons, upload WCL, etc.
631 lines (537 loc) • 15.1 kB
JavaScript
const fs = require('fs')
const path = require('path')
const rm = require('rimraf')
const mk = require('mkdirp')
const numeral = require('numeral')
const moment = require('moment')
const async = require('async')
const ncp = require('ncp').ncp
const tb = require('easy-table')
const Listr = require('listr')
const _ = require('underscore')
const g = require('got')
const pi = require('package-info')
const api = require('./source')
const cfg = require('./lib/config')
const unzip = require('./lib/unzip')
const cl = require('./lib/color')
const ads = require('./lib/wowaads').load()
const pkg = require('./package.json')
const log = console.log
function getAd(ad, info, tmp, hook) {
let src = path.join(tmp, '1.zip')
let dst = path.join(tmp, 'dec')
// download git
if (info.source === 'git')
return api.$api.git.clone(ad.uri, ad.branch, dst, hook)
let v = info.version[0]
if (!v) {
log('fatal: version not found')
return hook()
}
if (ad.version) v = _.find(info.version, d => d.name === ad.version)
// log('streaming', v.link)
// fix version
ad.version = v.name
g.stream(v.link, {
headers: {
'user-agent': require('ua-string')
}
})
.on('downloadProgress', hook)
.on('error', err => {
// log('stream error', typeof err, err)
hook(err ? err.toString() : 'download error')
})
.pipe(fs.createWriteStream(src))
.on('close', () => {
unzip(src, dst, err => {
if (err) return hook('unzip failed')
hook('done')
})
})
}
function _install(from, to, sub, done) {
let ls = fs.readdirSync(from)
let c_ver = cfg.getMode() === '_classic_' ?
(cfg.getClassicExp() === '[TBC]' ? /bcc/i : /classic/i)
: null
let _toc = _.filter(ls, x => x.match(/\.toc$/))
let toc // toc to use
let itoc // no -bcc/-classic
if (_toc.length >= 1) {
if (_toc.length > 1)
if (c_ver) toc = _.find(_toc, x => x.match(c_ver))
itoc = _.filter(_toc, x => !x.match(/bcc|classic/i))[0]
if (!itoc) itoc = _toc[0]
if (!toc) toc = itoc
}
// log('\n\n searching', from, toc, to, 'itoc', itoc)
if (itoc) {
let dir = itoc.replace(/\.toc$/, '')
let target = path.join(to, dir)
// log('\n\ntoc found, copy', from, '>>', target, '\n\n')
rm(target, err => {
// log('\n\n', 'rm err', err)
mk(target, err => {
ncp(from, target, err => {
if (itoc != toc) {
fs.renameSync(path.join(target, itoc), path.join(target, itoc + '.bak'))
fs.renameSync(path.join(target, toc), path.join(target, itoc))
}
done(err)
})
sub.push(dir)
})
})
} else {
async.eachLimit(
_.filter(ls.map(x => path.join(from, x)), x =>
fs.statSync(x).isDirectory()
),
1,
(d, cb) => {
_install(d, to, sub, err => {
if (err) {
log('\n\nerr??', err, '\n\n')
done(err)
cb(false)
return
}
// log('\n\ninstalling from', d, 'to', to, sub, '\n\n')
cb()
})
},
() => {
done()
}
)
}
}
function install(ad, update, hook) {
// log('installing', ad)
let tmp = path.join(cfg.getPath('tmp'), ad.key.replace(/\/|:/g, '.'))
let notify = (status, msg) => {
hook({
status,
msg
})
}
if (update && ad.pin) return notify('skip', 'is pinned')
notify('ongoing', update ? 'checking for updates...' : 'waiting...')
api.info(ad, info => {
if (!info) return notify('failed', 'not available')
// fix source
ad.source = info.source
let _d = ads.data[ad.key]
if (
update &&
_d &&
(_d.update >= info.update || (_d.hash && _d.hash === info.hash))
)
return notify('skip', 'is already up to date')
notify('ongoing', 'preparing download...')
rm(tmp, err => {
if (err) return notify('failed', 'failed to rmdir ' + JSON.stringify(err))
let dec = path.join(tmp, 'dec')
mk(dec, err => {
if (err)
return notify('failed', 'failed to mkdir ' + JSON.stringify(err))
let size = 0
notify('ongoing', 'downloading...')
getAd(ad, info, tmp, evt => {
if (!evt || (typeof evt === 'string' && evt !== 'done')) {
notify('failed', !evt ? 'failed to download' : evt)
} else if (evt === 'done') {
notify('ongoing', 'clearing previous install...')
ads.clearUp(ad.key, () => {
let d = (ads.data[ad.key] = {
name: info.name,
version: ad.version,
size,
source: info.source,
update: info.update,
sub: []
})
if (ad.anyway) d.anyway = ad.anyway
if (ad.branch) d.branch = ad.branch
if (ad.source === 'git') {
d.uri = ad.uri
d.hash = info.hash
}
_install(dec, cfg.getPath('addon'), d.sub, err => {
if (err) return notify('failed', 'failed to copy file')
ads.save()
notify('done', update ? 'updated' : 'installed')
})
})
} else {
notify(
'ongoing',
`downloading... ${(evt.percent * 100).toFixed(0)}%`
)
size = evt.transferred
// log(evt)
}
})
})
})
})
}
function batchInstall(aa, update, done) {
let t0 = moment().unix()
let list = new Listr([], {
concurrent: 10,
renderer: process.env.TEST_WOWA ? 'silent' : 'default'
})
let ud = 0
let id = 0
aa.forEach(ad => {
list.add({
title: `${cl.h(ad.key)} waiting...`,
task(ctx, task) {
let promise = new Promise((res, rej) => {
install(ad, update, evt => {
if (!task.$st) {
task.title = ''
task.title += cl.h(ad.key)
if (ad.version) task.title += cl.i2(' @' + cl.i2(ad.version))
if (ad.source) task.title += cl.i(` [${ad.source}]`)
task.title += ' ' + cl.x(evt.msg)
}
if (
evt.status === 'done' ||
evt.status === 'skip' ||
evt.status === 'failed'
) {
task.$st = evt.status
if (evt.status !== 'done') task.skip()
else {
if (update) ud++
id++
}
res('ok')
}
})
})
return promise
}
})
})
list.run().then(res => {
ads.save()
log(`\n${id} addons` + (update ? `, ${ud} updated` : ' installed'))
log(`✨ done in ${moment().unix() - t0}s.\n`)
if (done) done({ count: id, update, ud })
})
}
let core = {
add(aa, done) {
api.getDB(db => {
log('\nInstalling addon' + (aa.length > 1 ? 's...' : '...') + '\n')
batchInstall(aa.map(x => api.parseName(x)), 0, done)
})
},
rm(keys, done) {
let n = 0
async.eachLimit(
keys,
1,
(key, cb) => {
ads.clearUp(key, err => {
if (!err) n++
ads.save()
cb()
})
},
() => {
log(`✨ ${n} addon${n > 1 ? 's' : ''} removed.`)
if (done) done()
}
)
},
pin(keys, pup) {
let d = ads.data
let n = 0
keys.forEach(k => {
if (d[k]) {
d[k].pin = pup
n++
}
})
ads.save()
log(`✨ ${n} addon${n > 1 ? 's' : ''} ${pup ? '' : 'un'}pinned.`)
},
search(text, done) {
// log(text)
api.search(api.parseName(text), info => {
if (!info) {
log('\nNothing is found\n')
if (done) done(info)
return
}
let kv = (k, v) => {
let c = cl.i
let h = cl.x
return `${h(k + ':') + c(' ' + v + '')}`
}
let data = info.data.slice(0, 15)
log(`\n${cl.i(data.length)} results from ${cl.i(info.source)}`)
data.forEach((v, i) => {
log()
log(cl.h(v.name) + ' ' + cl.x('(' + v.page + ')'))
log(
` ${kv('key', v.key)} ${kv(
'download',
numeral(v.download).format('0.0a')
)} ${kv('version', moment(v.update * 1000).format('MM/DD/YYYY'))}`
)
// log('\n ' + v.desc)
})
log()
if (done) done(info)
})
},
ls(opt) {
let t = new tb()
let _d = ads.data
let ks = _.keys(_d)
ks.sort((a, b) => {
return opt.time
? _d[b].update - _d[a].update
: 1 - (a.replace(/[^a-zA-Z]/g, '') < b.replace(/[^a-zA-Z]/g, '')) * 2
})
ks.forEach(k => {
let v = _d[k]
t.cell(cl.x('Addon keys'), cl.h(k) + (v.anyway ? cl.i2(' [anyway]') : ''))
t.cell(cl.x('Version'), (v.pin ? cl.i('! ') : '') + cl.i2(v.version))
t.cell(cl.x('Source'), cl.i(v.source))
t.cell(cl.x('Update'), cl.i(moment(v.update * 1000).format('YYYY-MM-DD')))
t.newRow()
})
log()
if (!ks.length) log('no addons\n')
else log(opt.long ? t.toString() : cl.h(cl.ls(ks)))
ads.checkDuplicate()
log(
`${cl.x('You are in: ')} ${cl.i(cfg.getMode())} ${cl.i2(
cfg.getMode('ver')
)}\n`
)
let ukn = ads.unknownDirs()
if (ukn.length) {
log(
cl.x(
`❗ ${ukn.length} folder${ukn.length > 1 ? 's' : ''
} not managing by wowa`
)
)
log(cl.x('---------------------------------'))
log(cl.x(cl.ls(ukn)))
}
return t.toString()
},
info(ad, done) {
let t = new tb()
ad = api.parseName(ad)
api.info(ad, info => {
log('\n' + cl.h(ad.key) + '\n')
if (!info) {
log('Not available\n')
if (done) done()
return
}
let kv = (k, v) => {
// log('adding', k, v)
t.cell(cl.x('Item'), cl.x(k))
t.cell(cl.x('Info'), cl.i(v))
t.newRow()
}
for (let k in info) {
if (k === 'version' || info[k] === undefined) continue
kv(
k,
k === 'create' || k === 'update'
? moment(info[k] * 1000).format('MM/DD/YYYY')
: k === 'download'
? numeral(info[k]).format('0.0a')
: info[k]
)
}
let v = info.version[0]
if (v && info.source !== 'git') {
kv('version', v.name)
if (v.size) kv('size', v.size)
if (v.game)
kv('game version', _.uniq(info.version.map(x => x.game)).join(', '))
if (v.link) kv('link', v.link)
}
log(t.toString())
if (done) done(t.toString())
})
},
update(keys, opt, done) {
api.getDB(opt.db ? null : db => {
let aa = []
if (!keys) keys = _.keys(ads.data)
keys.forEach(k => {
if (k in ads.data)
aa.push({
key: k,
source: ads.data[k].source,
anyway: ads.data[k].anyway && cfg.anyway(),
branch: ads.data[k].branch,
uri: ads.data[k].uri,
hash: ads.data[k].hash,
pin: ads.data[k].pin
})
})
if (!aa.length) {
log('\nnothing to update\n')
return
}
if (ads.checkDuplicate()) return
log('\nUpdating addons:\n')
batchInstall(aa, 1, done)
})
},
restore(repo, done) {
if (repo) {
log('\nrestore from remote is not implemented yet\n')
return
}
api.getDB(db => {
let aa = []
for (let k in ads.data) {
aa.push({
key: k,
source: ads.data[k].source,
anyway: ads.data[k].anyway && cfg.anyway(),
branch: ads.data[k].branch,
uri: ads.data[k].uri,
hash: ads.data[k].hash
})
}
if (!aa.length) {
log('\nnothing to restore\n')
return
}
log('\nRestoring addons:')
batchInstall(aa, 0, done)
})
},
pickup(done) {
api.getDB(db => {
let p = cfg.getPath('addon')
let imported = 0
let importedDirs = 0
if (!db) {
if (done) done()
return
}
ads.unknownDirs().forEach(dir => {
if (ads.dirStatus(dir)) return
// log('picking up', dir)
let l = _.filter(db, a => a.dir && a.dir.indexOf(dir) >= 0 && cfg.testMode(a.mode))
if (!l.length) return
l.sort((a, b) => a.id - b.id)
// log(l)
l = l[0]
// log('found', l)
importedDirs++
let update = Math.floor(fs.statSync(path.join(p, dir)).mtimeMs / 1000)
let k =
l.source === 'curse'
? l.key
: l.id +
'-' +
_.filter(l.name.split(''), s => s.match(/^[a-z0-9]+$/i)).join('')
if (ads.data[k]) ads.data[k].sub.push(dir)
else {
ads.data[k] = {
name: l.name,
version: 'unknown',
source: l.source,
update,
sub: [dir]
}
imported++
}
})
log(`\n✨ imported ${imported} addons (${importedDirs} folders)\n`)
let ukn = ads.unknownDirs()
if (ukn.length) {
log(
cl.h(
`❗ ${ukn.length} folder${ukn.length > 1 ? 's are' : ' is'
} not recgonized\n`
)
)
log(cl.x(cl.ls(ukn)))
}
ads.save()
if (done) done(ukn)
})
},
switch(opt) {
let mo =
opt.ptr || opt.retailPtr
? '_ptr_'
: opt.beta || opt.retailBeta
? '_beta_'
: opt.classicPtr
? '_classic_ptr_'
: opt.classicTbc
? '_tbc_'
: opt.classicBeta
? '_classic_beta_'
: opt.retail
? '_retail_'
: opt.classic
? '_classic_'
: cfg.testMode('_retail_')
? '_tbc_'
: '_retail_'
cfg.setModePath(mo)
log(
`\n${cl.x('Mode switched to: ')} ${cl.i(cfg.getMode())} ${cl.i2(
cfg.getMode('ver')
)}\n`
)
ads.load()
},
checkUpdate(done) {
let v2n = v => {
let _v = 0
v.split('.').forEach((n, i) => {
_v *= 100
_v += parseInt(n)
})
return _v
}
let p = cfg.getPath('update')
let e = fs.existsSync(p)
let i
if (!e || new Date() - fs.statSync(p).mtime > 24 * 3600 * 1000) {
// fetch new data
pi('/wowa').then(res => {
fs.writeFileSync(p, JSON.stringify(res), 'utf-8')
done(res)
})
return
} else if (e) i = JSON.parse(fs.readFileSync(p, 'utf-8'))
if (i) {
// log(v2n(i.version), v2n(pkg.version))
if (v2n(i.version) > v2n(pkg.version)) {
log(
cl.i('\nNew wowa version'),
cl.i2(i.version),
cl.i('is available, use the command below to update\n'),
' npm install -g wowa\n'
)
}
}
done(i)
}
}
module.exports = core