diff --git a/src/apps.js b/src/apps.js index b1272bcd8..bca61124e 100644 --- a/src/apps.js +++ b/src/apps.js @@ -93,6 +93,7 @@ exports = module.exports = { listEventlog, downloadFile, + uploadFile, writeConfig, loadConfig, @@ -2887,6 +2888,32 @@ async function downloadFile(app, filePath) { return { stream: stdoutStream, filename, size }; } +async function uploadFile(app, sourceFilePath, destFilePath) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof sourceFilePath, 'string'); + assert.strictEqual(typeof destFilePath, 'string'); + + // the built-in bash printf understands "%q" but not /usr/bin/printf. + // ' gets replaced with '\'' . the first closes the quote and last one starts a new one + const escapedDestFilePath = await shell.exec('uploadFile', `printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash' }); + debug(`uploadFile: ${sourceFilePath} -> ${escapedDestFilePath}`); + + const execId = await createExec(app, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false }); + const destStream = await startExec(app, execId, { tty: false }); + + return new Promise((resolve, reject) => { + const done = once(error => reject(new BoxError(BoxError.FS_ERROR, error.message))); + + const sourceStream = fs.createReadStream(sourceFilePath); + sourceStream.on('error', done); + destStream.on('error', done); + + destStream.on('finish', resolve); + + sourceStream.pipe(destStream); + }); +} + async function writeConfig(app) { assert.strictEqual(typeof app, 'object'); diff --git a/src/routes/apps.js b/src/routes/apps.js index 2c5430a4f..bd4ec7c45 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -58,6 +58,7 @@ exports = module.exports = { clone, downloadFile, + uploadFile, updateBackup, downloadBackup, @@ -929,6 +930,21 @@ async function downloadBackup(req, res, next) { result.stream.pipe(res); } +async function uploadFile(req, res, next) { + assert.strictEqual(typeof req.app, 'object'); + + 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')); + + req.clearTimeout(); + + const [error] = await safe(apps.uploadFile(req.app, req.files.file.path, req.query.file)); + safe.fs.unlinkSync(req.files.file.path); + + if (error) return next(BoxError.toHttpError(error)); + next(new HttpSuccess(202, {})); +} + async function downloadFile(req, res, next) { assert.strictEqual(typeof req.app, 'object'); diff --git a/src/server.js b/src/server.js index ba2befece..309f154b7 100644 --- a/src/server.js +++ b/src/server.js @@ -282,11 +282,13 @@ async function initializeExpressSync() { router.get ('/api/v1/apps/:id/task', token, routes.apps.load, authorizeOperator, routes.apps.getTask); router.get ('/api/v1/apps/:id/graphs', token, routes.apps.load, authorizeOperator, routes.apps.getGraphs); router.post('/api/v1/apps/:id/clone', json, token, routes.apps.load, authorizeAdmin, routes.apps.clone); - router.get ('/api/v1/apps/:id/download', token, routes.apps.load, authorizeOperator, routes.apps.downloadFile); router.use ('/api/v1/apps/:id/files/*', token, routes.apps.load, authorizeOperator, routes.filemanager.proxy('app')); router.post('/api/v1/apps/:id/exec', json, token, routes.apps.load, authorizeOperator, routes.apps.createExec); router.get ('/api/v1/apps/:id/exec/:execId/start', token, routes.apps.load, authorizeOperator, routes.apps.startExec); router.get ('/api/v1/apps/:id/exec/:execId', token, routes.apps.load, authorizeOperator, routes.apps.getExec); + // these two routes are wrappers on exec. It allows upload/download to anywhere in filesystem unlike the files route which is only /app/data + router.get ('/api/v1/apps/:id/download', token, routes.apps.load, authorizeOperator, routes.apps.downloadFile); + router.post('/api/v1/apps/:id/upload', json, token, multipart, routes.apps.load, authorizeOperator, routes.apps.uploadFile); // websocket cannot do bearer authentication router.get ('/api/v1/apps/:id/exec/:execId/startws', token, routes.apps.load, authorizeOperator, routes.apps.startExecWebSocket);