diff --git a/src/apps.js b/src/apps.js index d37a006ea..9e4cb01ea 100644 --- a/src/apps.js +++ b/src/apps.js @@ -37,6 +37,9 @@ exports = module.exports = { getAppConfig: getAppConfig, + downloadFile: downloadFile, + uploadFile: uploadFile, + // exported for testing _validateHostname: validateHostname, _validatePortBindings: validatePortBindings, @@ -1126,3 +1129,35 @@ function configureInstalledApps(callback) { }, callback); }); } + +function downloadFile(appId, filePath, callback) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof filePath, 'string'); + assert.strictEqual(typeof callback, 'function'); + + var filename = path.basename(filePath); + exec(appId, { cmd: ['cat', filePath ], tty: false }, function (error, stream) { + if (error) return callback(error); + + return callback(null, stream, filename); + }); +} + +function uploadFile(appId, sourceFilePath, destFilePath, callback) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof sourceFilePath, 'string'); + assert.strictEqual(typeof destFilePath, 'string'); + assert.strictEqual(typeof callback, 'function'); + + exec(appId, { cmd: [ 'bash', '-c', 'cat - > ' + destFilePath ], tty: false }, function (error, stream) { + if (error) return callback(error); + + var readFile = fs.createReadStream(sourceFilePath); + readFile.on('error', console.error); + + readFile.pipe(stream); + + callback(null); + }); +} + diff --git a/src/routes/apps.js b/src/routes/apps.js index 787e08352..140250bca 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -19,7 +19,10 @@ exports = module.exports = { exec: exec, execWebSocket: execWebSocket, - cloneApp: cloneApp + cloneApp: cloneApp, + + uploadFile: uploadFile, + downloadFile: downloadFile }; var apps = require('../apps.js'), @@ -529,3 +532,41 @@ function listBackups(req, res, next) { next(new HttpSuccess(200, { backups: result })); }); } + +function uploadFile(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + + debug('uploadFile: %s %s -> %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(new HttpError(500, 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, result, filename) { + if (error) return next(new HttpError(500, error)); + + // TODO get real content type and size + res.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': 'attachment; filename="' + filename + '"' + // 'Content-Length': stat.size + }); + + result.pipe(res); + }); +} diff --git a/src/server.js b/src/server.js index 738ca71dd..95916ba2f 100644 --- a/src/server.js +++ b/src/server.js @@ -195,6 +195,8 @@ function initializeExpressSync() { // websocket cannot do bearer authentication router.get ('/api/v1/apps/:id/execws', routes.oauth2.websocketAuth.bind(null, [ clients.SCOPE_APPS ]), routes.user.requireAdmin, routes.apps.execWebSocket); router.post('/api/v1/apps/:id/clone', appsScope, routes.user.requireAdmin, routes.apps.cloneApp); + router.get ('/api/v1/apps/:id/download', appsScope, routes.user.requireAdmin, routes.apps.downloadFile); + router.post('/api/v1/apps/:id/upload', appsScope, routes.user.requireAdmin, multipart, routes.apps.uploadFile); // settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above) router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern); @@ -249,7 +251,6 @@ function initializeExpressSync() { }; } else { res.sendUpgradeHandshake = function () { // could extend express.response as well - console.log('----- now send the upgrade handshake') socket.write('HTTP/1.1 101 TCP Handshake\r\n' + 'Upgrade: tcp\r\n' + 'Connection: Upgrade\r\n' + diff --git a/webadmin/src/js/client.js b/webadmin/src/js/client.js index 69e638471..12350cfd8 100644 --- a/webadmin/src/js/client.js +++ b/webadmin/src/js/client.js @@ -1101,6 +1101,19 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', return (available - needed) >= 0; }; + Client.prototype.uploadFile = function (appId, file, callback) { + var fd = new FormData(); + fd.append('file', file); + + post('/api/v1/apps/' + appId + '/upload?file=/tmp/' + file.name, fd, { + headers: { 'Content-Type': undefined }, + transformRequest: angular.identity + }).success(function(data, status) { + if (status !== 202) return callback(new ClientError(status, data)); + callback(null); + }).error(defaultErrorHandler(callback)); + }; + client = new Client(); return client; }]); diff --git a/webadmin/src/views/debug.html b/webadmin/src/views/debug.html index 55aed0d3f..314be9ba3 100644 --- a/webadmin/src/views/debug.html +++ b/webadmin/src/views/debug.html @@ -1,26 +1,51 @@ - + +