UNPKG

content-store

Version:

Content server with file upload, automatic hashing and hash based naming scheme.

180 lines (144 loc) 4.8 kB
'use strict' const restify = require('restify') const ERRS = require('restify-errors') const logger = require('morgan') const multiparty = require('multiparty') const path = require('path') const os = require('os') const fs = require('fs-extra') const { randomBytes } = require('crypto') const eventPromise = require('./lib/event-promise') const HashThrough = require('hash-through') async function ContentStore (opts, createHash) { if (!createHash || typeof createHash !== 'function') { throw new TypeError('createHash should be a function') } opts = { // apply defaults name: 'content-store', storageDir: 'data', ...opts } const storageDir = path.resolve(process.cwd(), opts.storageDir) function tmpFileName () { return path.resolve(os.tmpdir(), `tmp_${randomBytes(16).toString('hex')}`) } function destFileName (basename) { return path.resolve(storageDir, `${basename}`) } await fs.ensureDir(storageDir) const server = restify.createServer({ name: opts.name }) server.use(logger('dev')) server.use(restify.plugins.acceptParser(server.acceptable)) server.get('/', function (req, res, next) { res.send(200, {result: `Hi, this is a ${server.name}. Use POST /upload to feed me with files.`}) next() }) server.get('/health', function (req, res, next) { res.send(200, {result: 'OK, healthy.'}) next() }) server.get('\/.*', restify.plugins.serveStatic({ directory: storageDir })) server.del('/:id', (req, res, next) => { const filepath = path.resolve(storageDir, req.params.id) fs.pathExists(filepath) .then(exists => { if (exists) { fs.remove(filepath) .then(_ => { res.send(200) next() }) .catch(err => { res.send(new ERRS.ConflictError(err)) next() }) } else { res.send(new ERRS.NotFoundError()) next() } }) .catch(err => { res.send(new ERRS.InternalServerError(err)) next() }) }) server.post('/upload', function (req, res, next) { let files = [] // Will count the things we have to finish before sending a response. // The counter will be 0 when and only when: // 1. the form is fully parsed (form 'close' event is detected) // 2. all the files ('file parts' in terms of multiparty) have been // successfully written to disk let taskCounter = 0 // call this one before any subtask is started function taskPlus () { taskCounter++ } // call this one after any subtask is finished function taskMinus() { taskCounter-- if (taskCounter === 0) { // Time to respond if (files.length === 0) { res.send(200, { result: 'no files to upload', files }) } else { res.send(201, { result: 'upload OK', files }) } next() } } // {autoFields: true} to allow omitting form.on('field',...) // without hanging on request const form = new multiparty.Form({ autoFields: true }) function onAnyError (err) { res.send(new ERRS.InternalServerError(err)) next() } form.on('part', async function (part) { part.on('error', onAnyError) try { if (part.filename) { // part is a readable stream of data for a single file. // If multiple files are being uploaded, then 'part' event // will be triggered that much times. taskPlus() // add a task for current file const tmpFile = tmpFileName() const fileSink = fs.createWriteStream(tmpFile) // a stream to calculate the data hash on the fly const ht = HashThrough(createHash) // Pipe file's data to disk part.pipe(ht).pipe(fileSink) await (eventPromise(fileSink, 'finish')) // By now both the file is written down and the hash is ready. // Let's take the digest and use it as a destination file name. const destBaseName = ht.digest('hex') files.push([part.filename, destBaseName]) // rename tmp file const destFile = destFileName(destBaseName) await fs.move(tmpFile, destFile, {overwrite: true}) taskMinus() // done with this file } } catch (err) { onAnyError(err) } }) form.on('close', taskMinus) // done with parsing form form.on('error', onAnyError) // Let's start taskPlus() // add a task for parsing form form.parse(req) }) server.on('uncaughtException', function (req, res, route, err) { res.send(500, 'Unexpected error occured') }) return server } module.exports = ContentStore