From 4bc5dbc5b52917a3cef4b88c4f92da40e890a08e Mon Sep 17 00:00:00 2001 From: Francisco Calle Moreno Date: Tue, 8 May 2018 08:38:42 +0200 Subject: [PATCH 1/5] feat(Server): Add option to configure routes path Defaults to /files --- src/index.js | 4 +++- src/server.js | 17 ++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index 210d978..1d77bdb 100755 --- a/src/index.js +++ b/src/index.js @@ -7,14 +7,16 @@ const opt = nodegetopt.create([ ['', 'totalsize=TOTALSIZE', "total size parameter (default 'totalsize')"], ['', 'storageType=TYPE', "disk or memory (default 'memory')"], ['', 'storagePath=PATH', "where to save files (default '/tmp')"], + ['', 'route=flies', "the API starting path (default '/files')"], ]).bindHelp().parseSystem().options; server.run({ - chunkNumber: opt.chunknumber, port: (opt.port || process.env.PORT || 5000), + chunkNumber: opt.chunknumber, totalSize: opt.totalsize, storage: { type: opt.storageType, path: opt.storagePath, }, + route: (opt.route || 'files'), }); diff --git a/src/server.js b/src/server.js index 618d54f..16e42e8 100644 --- a/src/server.js +++ b/src/server.js @@ -3,6 +3,7 @@ import express from 'express'; import cors from 'cors'; import multer, { diskStorage, memoryStorage } from 'multer'; import { urlencoded, json } from 'body-parser'; + import { writeFile, getFileSize, readFile, removeFile, writeFileChunk, assembleFileChunks } from './file-service'; import logger from './log'; @@ -28,8 +29,6 @@ export default { init(options) { let storage; if (options.storage.type === 'disk') { - logger.info('Using disk storage', (options.storage.path || '/tmp')); - if (options.storage.path && !existsSync(options.storage.pathh)) { logger.error(options.storage.path, 'does not exist'); throw new Error(`${options.storage.path} does not exist`); @@ -56,29 +55,29 @@ export default { app.use(json()); app.use(cors()); - app.get('/files/:filename/size', (request, response) => { + app.get(`/${options.route}/:filename/size`, (request, response) => { const result = getFileSize(request.params.filename); response.status(result.status).send(result.data); }); - app.get('/files/:filename', (request, response) => { + app.get(`/${options.route}/:filename`, (request, response) => { const result = readFile(request.params.filename); response.status(result.status).send(result.data); }); - app.delete('/files/:filename', (request, response) => { + app.delete(`/${options.route}/:filename`, (request, response) => { const result = removeFile(request.params.filename); response.status(result.status); }); - app.post('/files', upload.single('file'), (request, response) => { + app.post(`/${options.route}`, upload.single('file'), (request, response) => { saveFile(request, response, request.file.originalname); }); - app.post('/files/:filename', upload.single('file'), (request, response) => { + app.post(`/${options.route}/:filename`, upload.single('file'), (request, response) => { saveFile(request, response, request.params.filename); }); - app.post('/chunk/:filename', upload.single('file'), (request, response) => { + app.post(`/${options.route}/chunk/:filename`, upload.single('file'), (request, response) => { const result = writeFileChunk( request.params.filename, request.file.buffer, @@ -87,7 +86,7 @@ export default { response.status(result.status).send(result.data); }); - app.post('/assemble/:filename', (request, response) => { + app.post(`/${options.route}/assemble/:filename`, (request, response) => { const result = assembleFileChunks( request.params.filename, request.body[options.totalSize || 'totalsize'] From 2856e236153eb7c4f389f9b3c3a1ff5aa89f171a Mon Sep 17 00:00:00 2001 From: Francisco Calle Moreno Date: Tue, 8 May 2018 16:51:44 +0200 Subject: [PATCH 2/5] feat(log): Add verbose option from command line By adding the -v or --verbose parameter to the command line, the log level will be set to the lowest one (will print everithing). --- package.json | 2 +- src/index.js | 18 ++++++++++++++++-- src/log.js | 14 +++++++++----- src/server.js | 15 +++++++++------ 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 3f5b633..0ce8fe3 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "commit": "git-cz", "prebuild": "rimraf lib", "prepublishOnly": "pkg-ok", - "start": "babel-node src/index.js", + "start": "babel-node src/index.js -v", "start:disk": "babel-node src/index.js --storageType=disk", "start:prod": "node lib/index.js", "test": "echo \"Error: no test specified\" && exit 0", diff --git a/src/index.js b/src/index.js index 1d77bdb..18cb837 100755 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,7 @@ import nodegetopt from 'node-getopt'; +import logger, { setLogLevel } from './log'; import server from './server'; +import pkg from '../package.json'; const opt = nodegetopt.create([ ['p', 'port=PORT', 'server port (default 5000)'], @@ -8,15 +10,27 @@ const opt = nodegetopt.create([ ['', 'storageType=TYPE', "disk or memory (default 'memory')"], ['', 'storagePath=PATH', "where to save files (default '/tmp')"], ['', 'route=flies', "the API starting path (default '/files')"], + ['v', 'verbose', 'change log level to lowest'], ]).bindHelp().parseSystem().options; +logger.info('================================'); +logger.info('>>> Express REST file server'); +logger.info(`>>> version: ${pkg.version}`); +logger.info('================================'); + +if (opt.verbose) { + setLogLevel('silly'); + logger.debug('Command line options', opt); +} + server.run({ port: (opt.port || process.env.PORT || 5000), chunkNumber: opt.chunknumber, totalSize: opt.totalsize, storage: { - type: opt.storageType, - path: opt.storagePath, + type: (opt.storageType || 'memory'), + path: (opt.storageType === 'disk' && opt.storagePath ? opt.storagePath : '/tmp'), }, route: (opt.route || 'files'), + verbose: (!!opt.verbose), }); diff --git a/src/log.js b/src/log.js index 587deb9..77b8d3c 100644 --- a/src/log.js +++ b/src/log.js @@ -1,22 +1,26 @@ import winston, { Logger, transports as _transports } from 'winston'; -const { config } = winston; -const logger = new (Logger)({ +const logger = new Logger({ + level: winston.level, transports: [ new (_transports.Console)({ timestamp() { return Date.now(); }, formatter(options) { - const color = config.colorize(options.level, options.level.toUpperCase()); + const level = winston.config.colorize(options.level, options.level.toUpperCase().padEnd(6)); const message = options.message ? options.message : ''; const meta = options.meta && Object.keys(options.meta).length ? - `\n\t${JSON.stringify(options.meta)}` : + `\n${JSON.stringify(options.meta, null, 2)}` : ''; - return `[${options.timestamp()}][${color}] ${message} ${meta}`; + return `[${options.timestamp()}][${level}] ${message} ${meta}`; }, }), ], }); export default logger; + +export function setLogLevel(level) { + logger.transports.console.level = level; +} diff --git a/src/server.js b/src/server.js index 16e42e8..058859c 100644 --- a/src/server.js +++ b/src/server.js @@ -29,7 +29,7 @@ export default { init(options) { let storage; if (options.storage.type === 'disk') { - if (options.storage.path && !existsSync(options.storage.pathh)) { + if (options.storage.path && !existsSync(options.storage.path)) { logger.error(options.storage.path, 'does not exist'); throw new Error(`${options.storage.path} does not exist`); } @@ -43,7 +43,6 @@ export default { }, }); } else { - logger.info('Using memory storage'); storage = memoryStorage(); } const upload = multer({ storage }); @@ -98,12 +97,16 @@ export default { }, run(options) { - logger.info('================================'); - logger.info('>>> Express REST file server <<<'); - logger.info('================================'); const server = this.init(options); server.listen(options.port, () => { - logger.info('Listening on', options.port); + logger.info('Server ready. Configuration:'); + logger.info( + ' * Storage: %s %s', options.storage.type, + ((options.storage.type === 'disk' && options.storage.path) || '') + ); + logger.info(' * Port:', options.port); + logger.info(' * Routes:', `/${options.route}`); + logger.info(' * Verbose:', options.verbose ? 'yes' : 'no'); }); }, }; From 4bc170489d00823600022cd7c251920105f087ec Mon Sep 17 00:00:00 2001 From: Francisco Calle Moreno Date: Tue, 8 May 2018 16:53:20 +0200 Subject: [PATCH 3/5] chore(babel): Add babel-preset-stage-0 --- package-lock.json | 211 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 6 +- 2 files changed, 216 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 4106c6e..26e5fdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -658,6 +658,17 @@ } } }, + "babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, "babel-helper-builder-binary-assignment-operator-visitor": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", @@ -704,6 +715,18 @@ "babel-types": "6.26.0" } }, + "babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", + "dev": true, + "requires": { + "babel-helper-bindify-decorators": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, "babel-helper-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", @@ -819,18 +842,83 @@ "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", "dev": true }, + "babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", + "dev": true + }, + "babel-plugin-syntax-class-constructor-call": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=", + "dev": true + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", + "dev": true + }, + "babel-plugin-syntax-do-expressions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", + "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=", + "dev": true + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", + "dev": true + }, "babel-plugin-syntax-exponentiation-operator": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", "dev": true }, + "babel-plugin-syntax-export-extensions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=", + "dev": true + }, + "babel-plugin-syntax-function-bind": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", + "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, "babel-plugin-syntax-trailing-function-commas": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", "dev": true }, + "babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-generators": "6.13.0", + "babel-runtime": "6.26.0" + } + }, "babel-plugin-transform-async-to-generator": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", @@ -842,6 +930,52 @@ "babel-runtime": "6.26.0" } }, + "babel-plugin-transform-class-constructor-call": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", + "dev": true, + "requires": { + "babel-plugin-syntax-class-constructor-call": "6.18.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", + "dev": true, + "requires": { + "babel-helper-explode-class": "6.24.1", + "babel-plugin-syntax-decorators": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-do-expressions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", + "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", + "dev": true, + "requires": { + "babel-plugin-syntax-do-expressions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", @@ -1087,6 +1221,36 @@ "babel-runtime": "6.26.0" } }, + "babel-plugin-transform-export-extensions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", + "dev": true, + "requires": { + "babel-plugin-syntax-export-extensions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-function-bind": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", + "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", + "dev": true, + "requires": { + "babel-plugin-syntax-function-bind": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-runtime": "6.26.0" + } + }, "babel-plugin-transform-regenerator": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", @@ -1155,6 +1319,53 @@ "semver": "5.5.0" } }, + "babel-preset-stage-0": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", + "integrity": "sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=", + "dev": true, + "requires": { + "babel-plugin-transform-do-expressions": "6.22.0", + "babel-plugin-transform-function-bind": "6.22.0", + "babel-preset-stage-1": "6.24.1" + } + }, + "babel-preset-stage-1": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", + "dev": true, + "requires": { + "babel-plugin-transform-class-constructor-call": "6.24.1", + "babel-plugin-transform-export-extensions": "6.22.0", + "babel-preset-stage-2": "6.24.1" + } + }, + "babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", + "dev": true, + "requires": { + "babel-plugin-syntax-dynamic-import": "6.18.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-decorators": "6.24.1", + "babel-preset-stage-3": "6.24.1" + } + }, + "babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", + "dev": true, + "requires": { + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-generator-functions": "6.24.1", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-object-rest-spread": "6.26.0" + } + }, "babel-register": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", diff --git a/package.json b/package.json index 0ce8fe3..5505827 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ }, "devDependencies": { "babel-cli": "^6.26.0", + "babel-preset-stage-0": "^6.24.1", "commitizen": "^2.9.6", "cz-conventional-changelog": "^2.1.0", "eslint": "^4.19.1", @@ -60,7 +61,10 @@ }, "babel": { "plugins": [], - "presets": "env" + "presets": [ + "env", + "stage-0" + ] }, "eslintConfig": { "extends": "airbnb-base", From ecd647bb1b8c718cdc4553dc7587b276f760e1f9 Mon Sep 17 00:00:00 2001 From: Francisco Calle Moreno Date: Tue, 8 May 2018 17:02:37 +0200 Subject: [PATCH 4/5] docs(README): Add fancy flags --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 687e7e1..866543e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # express-rest-file-server -[![travis-ci](https://travis-ci.org/bitIO/express-rest-file-server.svg?branch=master)](https://travis-ci.org/bitIO/express-rest-file-server) +[![Build Status](https://travis-ci.org/bitIO/express-rest-file-server.svg?branch=master)](https://travis-ci.org/bitIO/express-rest-file-server) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) +[![Version npm](https://img.shields.io/npm/v/express-rest-file-server.svg?style=flat-square)](https://www.npmjs.com/package/express-rest-file-server) +[![npm Downloads](https://img.shields.io/npm/dm/express-rest-file-server.svg?style=flat-square)](https://npmcharts.com/compare/express-rest-file-server?minimal=true) + +[![NPM](https://nodei.co/npm/express-rest-file-server.png?downloads=true&downloadRank=true)](https://nodei.co/npm/express-rest-file-server/) An express based application, inspired by [mock-file-server](https://github.com/betajs/mock-file-server), to be used as a CRUD file server storing content in the memory (temporal) or in the disk (permanent) of the server. From 632cd3b865dcf294928c63f6b5220bc65b2f5726 Mon Sep 17 00:00:00 2001 From: Francisco Calle Moreno Date: Tue, 8 May 2018 20:33:50 +0200 Subject: [PATCH 5/5] docs(README): Add REST methods basic documentation --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 866543e..3f141ac 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,32 @@ npx express-rest-file-server * storageType: can be `memory` or `disk` (defaults to memory) * storagePath: where to store the files if storage is set to `disk` (defaults to `/tmp`) +## Routes + +### POST /files + +Uploads a file to the store with its original name + +### POST /files/:filename + +Uploads a file to the store with a custom name (:filename) + +### POST /files/chunk/:filename + +Uploads a chuck of a file to the store with a custom name (:filename) + +### POST /files/assemble/:filename + +_Builds_ a file from its chunks + +### GET /files/:filename + +Retrieve a file by its name + +### GET /files/:filename/size + +Retrieve a file size by its name + +### DELETE /files/:filename + +Remove a file by its name