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 @@ - + + -
-
- - - - - +
+
+ + + + + - - Download + + Download Full Logs - - - - - - - + + + +
+ + +
+ +
+ + + +
+
-
- - +
diff --git a/webadmin/src/views/debug.js b/webadmin/src/views/debug.js index 61ec3a1f2..9a3307a8e 100644 --- a/webadmin/src/views/debug.js +++ b/webadmin/src/views/debug.js @@ -3,31 +3,53 @@ /* global moment */ /* global Terminal */ + angular.module('Application').controller('DebugController', ['$scope', '$location', 'Client', function ($scope, $location, Client) { Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); }); $scope.config = Client.getConfig(); $scope.user = Client.getUserInfo(); + $scope.terminalVisible = true; $scope.logs = []; $scope.selected = ''; $scope.activeEventSource = null; $scope.terminal = null; $scope.terminalSocket = null; $scope.lines = 10; - $scope.terminalVisible = false; function ab2str(buf) { return String.fromCharCode.apply(null, new Uint16Array(buf)); } + $scope.downloadFile = { + filePath: '', + + downloadUrl: function () { + var filePath = '/app/data/' + $scope.downloadFile.filePath; + filePath = filePath.replace(/\/*\//g, '/'); + + return Client.apiOrigin + '/api/v1/apps/' + $scope.selected.value + '/download?file=' + filePath + '&access_token=' + Client.getToken(); + }, + + show: function () { + $scope.downloadFile.filePath = ''; + $('#downloadFileModal').modal('show'); + } + }; + $scope.populateLogTypes = function () { $scope.logs.push({ name: 'System (All)', type: 'platform', value: 'all', url: Client.makeURL('/api/v1/cloudron/logs?units=all') }); $scope.logs.push({ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs?units=box') }); $scope.logs.push({ name: 'Mail', type: 'platform', value: 'mail', url: Client.makeURL('/api/v1/cloudron/logs?units=mail') }); Client.getInstalledApps().forEach(function (app) { - $scope.logs.push({ name: app.fqdn + ' (' + app.manifest.title + ')', type: 'app', value: app.id, url: Client.makeURL('/api/v1/apps/' + app.id + '/logs'), addons: app.manifest.addons }); + $scope.logs.push({ + type: 'app', + value: app.id, + name: app.fqdn + ' (' + app.manifest.title + ')', + addons: app.manifest.addons + }); }); $scope.selected = $scope.logs[0]; @@ -104,7 +126,7 @@ angular.module('Application').controller('DebugController', ['$scope', '$locatio if ($scope.selected.type !== 'app') { var tmp = $('.logs-and-term-container'); var logLine = $('
'); - logLine.html('Terminal is only supported for app, not for ' + $scope.selected.name); + logLine.html('Terminal is only supported for apps, not for ' + $scope.selected.name); tmp.append(logLine); return; } @@ -149,6 +171,18 @@ angular.module('Application').controller('DebugController', ['$scope', '$locatio $scope.terminal.focus(); } + $scope.uploadFile = function () { + var fileUpload = document.querySelector('#fileUpload'); + + fileUpload.oninput = function (e) { + Client.uploadFile($scope.selected.value, e.target.files[0], function (error) { + if (error) console.error(error); + }); + }; + + fileUpload.click(); + }; + $scope.$watch('selected', function (newVal) { if (!newVal) return; @@ -169,4 +203,11 @@ angular.module('Application').controller('DebugController', ['$scope', '$locatio $scope.terminal.destroy(); } }); + + // setup all the dialog focus handling + ['downloadFileModal'].forEach(function (id) { + $('#' + id).on('shown.bs.modal', function () { + $(this).find("[autofocus]:first").focus(); + }); + }); }]);