var path = require('path') var multicb = require('multicb') var toPath = require('multiblob/util').toPath var createHash = require('multiblob/util').createHash var pull = require('pull-stream') var toPull = require('stream-to-pull-stream') var querystring = require('querystring') var fs = require('fs') // blob url parser var re = /^blob:([a-z0-9\+\/=]+\.blake2s)\??(.*)$/i var url_parse = module.exports.url_parse = function (str) { var parts = re.exec(str) if (parts) return { hash: parts[1], qs: querystring.parse(parts[2]) } } // blob url builder var url_stringify = module.exports.url_stringify = function (hash, qs) { var url = 'blob:'+hash if (qs && typeof qs == 'object') url += '?' + querystring.stringify(qs) return url } module.exports = function (blobs_dir, checkout_dir) { return { // behavior for the blob: protocol protocol: function (request) { var protocol = require('protocol') // have to require here, doing so before app:ready causes errors // simple fetch var parsed = url_parse(request.url) if (request.method == 'GET' && parsed) { return new protocol.RequestFileJob(toPath(blobs_dir, parsed.hash)) } }, // copy file from blobs into given dir with nice name checkout: function (url, cb) { var parsed = url_parse(url) if (!parsed) return cb({ badUrl: true }) var filename = parsed.qs.name || parsed.qs.filename || parsed.hash // check if we have the blob, at the same time find an available filename var done = multicb() fs.stat(toPath(blobs_dir, parsed.hash), done()) findCheckoutDst(filename, parsed.hash, done()) done(function (err, res) { if (!res[0][1]) return cb({ notFound: true }) // do we need to copy? var dst = res[1][1] var nocopy = res[1][2] if (nocopy) return cb(null, dst) // copy the file var src = toPath(blobs_dir, parsed.hash) var read = fs.createReadStream(src) var write = fs.createWriteStream(dst) read.on('error', done) write.on('error', done) write.on('close', done) read.pipe(write) function done (err) { cb && cb(err, dst) cb = null } }) } } // helper to create a filename in checkout_dir that isnt already in use // - cb(err, filepath, nocopy) - if nocopy==true, no need to do the copy operation function findCheckoutDst (filename, hash, cb) { var n = 1 var parsed = path.parse(filename) next() function gen () { var name = parsed.name if (n !== 1) name += ' ('+n+')' name += parsed.ext n++ return path.join(checkout_dir, name) } function next () { var dst = gen() // exists? fs.stat(dst, function (err, stat) { if (!stat) return cb(null, dst, false) // yes, check its hash var hasher = createHash() pull( toPull.source(fs.createReadStream(dst)), hasher, pull.onEnd(function () { // if the hash matches, we're set if (hasher.digest == hash) return cb(null, dst, true) // try next next() }) ) }) } } }