'use strict'; exports = module.exports = { getApp: getApp, getApps: getApps, getAppIcon: getAppIcon, installApp: installApp, uninstallApp: uninstallApp, restoreApp: restoreApp, backupApp: backupApp, updateApp: updateApp, getLogs: getLogs, getLogStream: getLogStream, listBackups: listBackups, repairApp: repairApp, setAccessRestriction: setAccessRestriction, setLabel: setLabel, setTags: setTags, setIcon: setIcon, setMemoryLimit: setMemoryLimit, setAutomaticBackup: setAutomaticBackup, setAutomaticUpdate: setAutomaticUpdate, setRobotsTxt: setRobotsTxt, setCertificate: setCertificate, setDebugMode: setDebugMode, setEnvironment: setEnvironment, setMailbox: setMailbox, setLocation: setLocation, setDataDir: setDataDir, stopApp: stopApp, startApp: startApp, exec: exec, execWebSocket: execWebSocket, cloneApp: cloneApp, uploadFile: uploadFile, downloadFile: downloadFile }; var apps = require('../apps.js'), AppsError = apps.AppsError, assert = require('assert'), auditSource = require('../auditsource.js'), debug = require('debug')('box:routes/apps'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, safe = require('safetydance'), util = require('util'), WebSocket = require('ws'); function toHttpError(appError) { switch (appError.reason) { case AppsError.NOT_FOUND: return new HttpError(404, appError); case AppsError.ALREADY_EXISTS: case AppsError.BAD_STATE: return new HttpError(409, appError); case AppsError.BAD_FIELD: return new HttpError(400, appError); case AppsError.PLAN_LIMIT: return new HttpError(402, appError); case AppsError.EXTERNAL_ERROR: return new HttpError(424, appError); case AppsError.INTERNAL_ERROR: default: return new HttpError(500, appError); } } function getApp(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); apps.get(req.params.id, function (error, app) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, apps.removeInternalFields(app))); }); } function getApps(req, res, next) { assert.strictEqual(typeof req.user, 'object'); apps.getAllByUser(req.user, function (error, allApps) { if (error) return next(toHttpError(error)); allApps = allApps.map(apps.removeRestrictedFields); next(new HttpSuccess(200, { apps: allApps })); }); } function getAppIcon(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) { if (error) return next(toHttpError(error)); res.sendFile(iconPath); }); } function installApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); var data = req.body; // atleast one if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object')); if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string')); if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required')); // required if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required')); if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required')); if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required')); // optional if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string')); if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null')); if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null')); if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string')); // falsy values in cert and key unset the cert if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string')); if (data.cert && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string')); if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided')); if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided')); if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number')); if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean')); if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean')); if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean')); if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object')); if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string')); if ('alternateDomains' in data) { if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array')); if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings')); } if ('env' in data) { if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object')); if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings')); } if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean')); debug('Installing app :%j', data); apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { id: result.id, taskId: result.taskId })); }); } function setAccessRestriction(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object')); apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, {})); }); } function setLabel(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string')); apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, {})); }); } function setTags(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array')); if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings')); apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, {})); }); } function setIcon(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string')); apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, {})); }); } function setMemoryLimit(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number')); apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function setAutomaticBackup(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean')); apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, {})); }); } function setAutomaticUpdate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean')); apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, {})); }); } function setRobotsTxt(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string')); apps.setRobotsTxt(req.params.id, req.body.robotsTxt, auditSource.fromRequest(req), function (error) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, {})); }); } function setCertificate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string')); if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string')); if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided')); if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided')); apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, {})); }); } function setEnvironment(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object')); if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings')); apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function setDebugMode(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object')); apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function setMailbox(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string')); apps.setMailbox(req.params.id, req.body.mailboxName, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function setLocation(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (!req.body.location) return next(new HttpError(400, 'location is required')); if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); if (!req.body.domain) return next(new HttpError(400, 'domain is required')); if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string')); if ('portBindings' in req.body && typeof req.body.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); if ('alternateDomains' in req.body) { if (!Array.isArray(req.body.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array')); if (req.body.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings')); } if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean')); apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function setDataDir(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string')); apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function repairApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); debug('Repair app id:%s', req.params.id); const data = req.body; if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null')); if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null')); if (data.location && typeof data.location !== 'string') return next(new HttpError(400, 'location is required')); if (data.domain && typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required')); if ('alternateDomains' in data) { if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array')); if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings')); } if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean')); apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function restoreApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); var data = req.body; debug('Restore app id:%s', req.params.id); if (!('backupId' in req.body)) return next(new HttpError(400, 'backupId is required')); if (data.backupId !== null && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null')); apps.restore(req.params.id, data, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function cloneApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); var data = req.body; debug('Clone app id:%s', req.params.id); if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string')); if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required')); if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required')); if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean')); apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(201, { id: result.id, taskId: result.taskId })); }); } function backupApp(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('Backup app id:%s', req.params.id); apps.backup(req.params.id, function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function uninstallApp(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('Uninstalling app id:%s', req.params.id); apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function startApp(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('Start app id:%s', req.params.id); apps.start(req.params.id, function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function stopApp(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('Stop app id:%s', req.params.id); apps.stop(req.params.id, function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } function updateApp(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); assert.strictEqual(typeof req.body, 'object'); var data = req.body; // atleast one if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object')); if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string')); if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required')); if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean')); debug('Update app id:%s to manifest:%j', req.params.id, data.manifest); apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); }); } // this route is for streaming logs function getLogStream(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('Getting logstream of app id:%s', req.params.id); var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number')); function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; } if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream')); var options = { lines: lines, follow: true, format: 'json' }; apps.getLogs(req.params.id, options, function (error, logStream) { if (error) return next(toHttpError(error)); res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // disable nginx buffering 'Access-Control-Allow-Origin': '*' }); res.write('retry: 3000\n'); res.on('close', logStream.close); logStream.on('data', function (data) { var obj = JSON.parse(data); res.write(sse(obj.realtimeTimestamp, JSON.stringify(obj))); // send timestamp as id }); logStream.on('end', res.end.bind(res)); logStream.on('error', res.end.bind(res, null)); }); } function getLogs(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number')); debug('Getting logs of app id:%s', req.params.id); var options = { lines: lines, follow: false, format: req.query.format || 'json' }; apps.getLogs(req.params.id, options, function (error, logStream) { if (error) return next(toHttpError(error)); res.writeHead(200, { 'Content-Type': 'application/x-logs', 'Content-Disposition': 'attachment; filename="log.txt"', 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no' // disable nginx buffering }); logStream.pipe(res); }); } function demuxStream(stream, stdin) { var header = null; stream.on('readable', function() { header = header || stream.read(4); while (header !== null) { var length = header.readUInt32BE(0); if (length === 0) { header = null; return stdin.end(); // EOF } var payload = stream.read(length); if (payload === null) break; stdin.write(payload); header = stream.read(4); } }); } function exec(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('Execing into app id:%s and cmd:%s', req.params.id, req.query.cmd); var cmd = null; if (req.query.cmd) { cmd = safe.JSON.parse(req.query.cmd); if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1')); } var columns = req.query.columns ? parseInt(req.query.columns, 10) : null; if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number')); var rows = req.query.rows ? parseInt(req.query.rows, 10) : null; if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number')); var tty = req.query.tty === 'true' ? true : false; apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) { if (error) return next(toHttpError(error)); if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade')); req.clearTimeout(); res.sendUpgradeHandshake(); // When tty is disabled, the duplexStream has 2 separate streams. When enabled, it has stdout/stderr merged. duplexStream.pipe(res.socket); if (tty) { res.socket.pipe(duplexStream); // in tty mode, the client always waits for server to exit } else { demuxStream(res.socket, duplexStream); res.socket.on('error', function () { duplexStream.end(); }); res.socket.on('end', function () { duplexStream.end(); }); } }); } function execWebSocket(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('Execing websocket into app id:%s and cmd:%s', req.params.id, req.query.cmd); var cmd = null; if (req.query.cmd) { cmd = safe.JSON.parse(req.query.cmd); if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1')); } var columns = req.query.columns ? parseInt(req.query.columns, 10) : null; if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number')); var rows = req.query.rows ? parseInt(req.query.rows, 10) : null; if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number')); var tty = req.query.tty === 'true' ? true : false; apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) { if (error) return next(toHttpError(error)); debug('Connected to terminal'); req.clearTimeout(); res.handleUpgrade(function (ws) { duplexStream.on('end', function () { ws.close(); }); duplexStream.on('close', function () { ws.close(); }); duplexStream.on('error', function (error) { debug('duplexStream error:', error); }); duplexStream.on('data', function (data) { if (ws.readyState !== WebSocket.OPEN) return; ws.send(data.toString()); }); ws.on('error', function (error) { debug('websocket error:', error); }); ws.on('message', function (msg) { duplexStream.write(msg); }); ws.on('close', function () { // Clean things up, if any? }); }); }); } function listBackups(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number')); var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number')); apps.listBackups(page, perPage, req.params.id, function (error, result) { if (error) return next(toHttpError(error)); next(new HttpSuccess(200, { backups: result })); }); } function uploadFile(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('uploadFile: %s %j -> %s', req.params.id, req.files, req.query.file); if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided')); if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart')); apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) { if (error) return next(toHttpError(error)); debug('uploadFile: done'); next(new HttpSuccess(202, {})); }); } function downloadFile(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); debug('downloadFile: ', req.params.id, req.query.file); if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided')); apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) { if (error) return next(toHttpError(error)); var headers = { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename*=utf-8''${encodeURIComponent(info.filename)}` // RFC 2184 section 4 }; if (info.size) headers['Content-Length'] = info.size; res.writeHead(200, headers); stream.pipe(res); }); }