unpm-s3-backend
Version:
s3 backend for unpm
335 lines (271 loc) • 9.33 kB
JavaScript
var EE = require('events').EventEmitter
var join = require('path').join
var qs = require('querystring')
var aws = require('aws-sdk')
var through = require('through')
var PassThrough = require('stream').PassThrough
var Readable = require('stream').Readable
module.exports = S3Backend
function S3Backend(options) {
if (!(this instanceof S3Backend)) {
return new S3Backend(options)
}
EE.call(this)
this.options = options || {}
this.s3 = new aws.S3(options.s3)
}
S3Backend.prototype = Object.create(EE.prototype)
S3Backend.prototype.constructor = S3Backend
S3Backend.prototype.getUser = getter('users', '~/users/')
S3Backend.prototype.setUser = setter('users', '~/users/', 'setUser', 'getUser')
S3Backend.prototype.removeUser = remover('users', '~/users/', 'removeUser', 'getUser')
S3Backend.prototype.createUserStream = streamer('users', '~/users/', [])
S3Backend.prototype._setMeta = setter('meta', '', 'setMeta', 'getMeta')
S3Backend.prototype.getMeta = getter('meta', '')
S3Backend.prototype.removeMeta = remover('meta', '', 'removeMeta', 'getMeta')
S3Backend.prototype.createMetaStream = streamer('meta', '', [])
S3Backend.prototype.get = getter('store', '~/store/')
S3Backend.prototype.set = setter('store', '~/store/', 'set', 'get')
S3Backend.prototype.remove = remover('store', '~/store/', 'remove', 'get')
S3Backend.prototype.createStream = streamer('store', '~/store/', [])
S3Backend.prototype.setMeta = function setMeta(name, meta, done) {
var escaped = escape(name)
var baseUrl = this.options.baseUrl || ''
if(baseUrl && baseUrl[baseUrl.length - 1] === '/') {
baseUrl = baseUrl.slice(0, -1)
}
if (meta.versions) {
Object.keys(meta.versions).forEach(function(version) {
var url = baseUrl + join('/', escaped, '-', escaped + '-' + version + '.tgz')
meta.versions[version].dist = meta.versions[version].dist || {}
meta.versions[version].dist.tarball = url
if(!meta.versions[version]._id) {
meta.versions[version]._id = name + '@' + version
}
})
}
this._setMeta(name, meta, done)
}
S3Backend.prototype.getTarball = function getTarball(_name, version) {
var stream = new PassThrough()
var name = escape(_name)
var params = {}
if (this.options.tarballs) {
params.Bucket = this.options.tarballs.Bucket
name = (this.options.tarballs.prefix || '') + name
}
params.Key = name + '/-/' + name + '-' + version + '.tgz'
this.s3.getObject(params, function write(err, data) {
if (err) return stream.emit('error', err)
stream.end(data.Body)
})
return stream
}
S3Backend.prototype.setTarball = function setTarball(_name, version) {
var stream = new PassThrough()
var name = escape(_name)
var params = {}
if (this.options.tarballs) {
params.Bucket = this.options.tarballs.Bucket
name = (this.options.tarballs.prefix || '') + name
}
params.Key = name + '/-/' + name + '-' + version + '.tgz'
params.ContentType = 'application/x-compressed'
params.Body = stream
this.s3.upload(params, function uploaded(err) {
if (err) return stream.emit('error', err)
})
return stream
}
S3Backend.prototype.removeTarball = function removeTarball(_name, version, _done) {
var params = {}
var name = escape(_name)
var done = _done || new Function
if (this.options.tarballs) {
params.Bucket = this.options.tarballs.Bucket
name = (this.options.tarballs.prefix || '') + name
}
params.Key = name + '/-/' + name + '-' + version + '.tgz'
this.s3.deleteObject(params, function removed(err, data) {
if (err) return done(err, null)
done(null)
})
}
S3Backend.prototype.getFileList = function getFileList(bucket, prefix, ignores, done) {
var params = {Bucket: bucket, Delimiter: '/'}
if ('prefix') params.Prefix = prefix
var backend = this
var items = []
getSome()
function getSome(marker) {
if(marker) params.marker = marker
backend.s3.listObjects(params, function gotSome(err, data) {
if (err) return done(err)
items = items.concat(data.Contents.map(addName).filter(notIgnored))
if (data.NextMarker) return getSome(data.NextMarker)
done(null, items)
})
}
function addName(item) {
var name = item && unescape(item.Key.slice(prefix.length))
if(!name) return null
return {name: name, params: {Bucket: bucket, Key: item.Key}}
}
function notIgnored(name) {
if (!name) return false
for (var i = ignores.length - 1; i >= 0; --i) {
if (ignores.test(name)) return false
}
return true
}
}
function getter(type, prefix) {
return function get(_name, _done) {
var done = _done || new Function
var name = escape(_name)
var params = {}
if (this.options[type]) {
params.Bucket = this.options[type].Bucket
name = (this.options[type].prefix || prefix) + name
} else {
name = prefix + name
}
params.Key = name
this.s3.getObject(params, function parse(err, data) {
if (err) return done(err.statusCode === 404 ? null : err, null)
try {
data = JSON.parse(data.Body.toString())
} catch (err) {
return done(err, null)
}
done(null, data)
})
}
}
function setter(type, prefix, eventName, getter) {
return function set(_name, data, _done) {
var backend = this
var params = {}
var name = escape(_name)
var done = _done || new Function
if (!data && data !== '') return done(new Error('Must provide data to store'))
try {
params.Body = JSON.stringify(data, null, 2)
} catch (err) {
return done(err)
}
if (this.options[type]) {
params.Bucket = this.options[type].Bucket
name = (this.options[type].prefix || prefix) + name
} else {
name = prefix + name
}
params.Key = name
params.ContentType = 'application/json'
this[getter](_name, gotPrevious)
function gotPrevious(err, previous) {
if (err) return done(err, null)
backend.s3.upload(params, function uploaded(err) {
if (err) return done(err, null)
done(null, data, previous)
backend.emit(eventName, _name, data, previous)
})
}
}
}
function remover(type, prefix, eventName, getter) {
return function remove(_name, _done) {
var backend = this
var params = {}
var name = escape(_name)
var done = _done || new Function
if (this.options[type]) {
params.Bucket = this.options[type].Bucket
name = (this.options[type].prefix || prefix) + name
} else {
name = prefix + name
}
params.Key = name
this[getter](_name, gotPrevious)
function gotPrevious(err, previous) {
if (err) return done(err, null)
backend.s3.deleteObject(params, function removed(err, data) {
if (err) return done(err, null)
done(null, previous)
backend.emit(eventName, _name, previous)
})
}
}
}
function streamer(type, prefix, ignores) {
return function stream(options) {
var stream = new ContentStream(this.s3, options)
var bucket = this.options.s3.params.Bucket
if (this.options[type] && this.options[type].Bucket) {
bucket = this.options[type].Bucket
}
this.getFileList(bucket, prefix, ignores, function gotFiles(err, items) {
if (err) return stream.emit('error', err)
stream.init(items)
})
return stream
}
}
function escape(name) {
return name.replace('/', '%2f')
}
function unescape(name) {
return name.replace('%2f', '/')
}
function ContentStream(s3, options) {
if (!(this instanceof ContentStream)) {
return new ContentStream(s3, options)
}
Readable.call(this, {objectMode: true})
this.options = options || {}
this.reading = false
this.items = null
this.s3 = s3
}
ContentStream.prototype = Object.create(Readable.prototype)
ContentStream.prototype.constructor = ContentStream
ContentStream.prototype.init = function init(items) {
var opts = this.options
this.items = items.filter(byRange)
if (opts.reverse) this.items.reverse()
if (opts.limit >= 0) this.items = items.slice(opts.limit)
if (this.reading) this.readOne()
function byRange(item) {
if (opts.gt && !(item.name > opts.gt)) return false
if (opts.start && !(item.name >= opts.start)) return false
if (opts.gte && !(item.name >= opts.gt)) return false
if (opts.lt && !(item.name < opts.gt)) return false
if (opts.lte && !(item.name <= opts.gt)) return false
if (opts.end && !(item.name <= opts.end)) return false
return true
}
}
ContentStream.prototype._read = function read() {
if (this.items) return this.readOne()
this.reading = true
}
ContentStream.prototype.readOne = function readOne() {
if (!this.items || !this.items.length) return this.push(null)
var item = this.items.shift()
var stream = this
if (!stream.options.values && typeof stream.options.values !== 'undefined') {
return stream.push(item.name)
}
this.s3.getObject(item.params, function parse(err, data) {
if (err) return stream.emit('error', err)
try {
data = JSON.parse(data.Body.toString())
} catch (err) {
return stream.emit('error', err)
}
if (!stream.options.keys && typeof stream.options.keys !== 'undefined') {
return stream.push(data)
}
stream.push({key: item.name, value: data})
})
}