diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 1fccc3e3..8eb1f9e3 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -177,6 +177,7 @@ class Server(): self._tornado_app = Application(self._router.urls + [ (r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}), (r"/downloads/files/local/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}), + (r"/downloads/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True}), (r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)}) ]) self._server = HTTPServer(self._tornado_app) diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 5614bee8..9ff5ae54 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -27,6 +27,7 @@ from . import files as api_files from . import settings as api_settings from . import timelapse as api_timelapse from . import users as api_users +from . import log as api_logs #~~ first run setup diff --git a/src/octoprint/server/api/log.py b/src/octoprint/server/api/log.py new file mode 100644 index 00000000..0fda12dc --- /dev/null +++ b/src/octoprint/server/api/log.py @@ -0,0 +1,62 @@ +# coding=utf-8 +__author__ = "Marc Hannappel Salandora" +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' + +import os +import datetime + +from flask import request, jsonify, make_response, url_for +from werkzeug.utils import secure_filename + +import octoprint.util as util +from octoprint.settings import settings, valid_boolean_trues + +from octoprint.server import restricted_access, admin_permission +from octoprint.server.util import redirectToTornado +from octoprint.server.api import api + +@api.route("/logs", methods=["GET"]) +def getLogData(): + files = _getLogFiles() + return jsonify(files=files) + +@api.route("/logs/online/", methods=["GET"]) +def onlineLog(filename): + secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename)) + if not os.path.exists(secure): + return make_response("Unknown filename: %s" % filename, 404) + + file = open(secure, 'r') + return jsonify(data=file.read()) + +@api.route("/logs/", methods=["GET"]) +def downloadLog(filename): + return redirectToTornado(request, url_for("index") + "downloads/logs/" + filename) + + +@api.route("/logs/", methods=["DELETE"]) +@restricted_access +def deleteLog(filename): + secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename)) + if os.path.exists(secure): + os.remove(secure) + + return getLogData() + +def _getLogFiles(): + files = [] + basedir = settings().getBaseFolder("logs") + for osFile in os.listdir(basedir): + statResult = os.stat(os.path.join(basedir, osFile)) + files.append({ + "name": osFile, + "size": util.getFormattedSize(statResult.st_size), + "bytes": statResult.st_size, + "date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime)) + }) + + for file in files: + file["url"] = url_for("index") + "downloads/logs/" + file["name"] + + return files + diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index b7019ecf..3304fd64 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -1,4 +1,4 @@ -function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) { +function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel, logViewModel) { var self = this; self.loginStateViewModel = loginStateViewModel; @@ -10,6 +10,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM self.gcodeFilesViewModel = gcodeFilesViewModel; self.timelapseViewModel = timelapseViewModel; self.gcodeViewModel = gcodeViewModel; + self.logViewModel = logViewModel; self._socket = undefined; self._autoReconnecting = false; @@ -38,7 +39,8 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM self._autoReconnectTrial = 0; if ($("#offline_overlay").is(":visible")) { - $("#offline_overlay").hide(); + $("#offline_overlay").hide(); + self.logViewModel.requestData(); self.timelapseViewModel.requestData(); self.loginStateViewModel.requestData(); self.gcodeFilesViewModel.requestData(); diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index f8270946..4c6b934c 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -14,6 +14,7 @@ $(function() { var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel); var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel); var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel); + var logViewModel = new LogViewModel(loginStateViewModel); var dataUpdater = new DataUpdater( loginStateViewModel, @@ -24,7 +25,8 @@ $(function() { terminalViewModel, gcodeFilesViewModel, timelapseViewModel, - gcodeViewModel + gcodeViewModel, + logViewModel ); // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at @@ -302,6 +304,7 @@ $(function() { ko.applyBindings(navigationViewModel, document.getElementById("navbar")); ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]); ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay")); + ko.applyBindings(logViewModel, document.getElementById("logs")); var timelapseElement = document.getElementById("timelapse"); if (timelapseElement) { @@ -316,6 +319,7 @@ $(function() { gcodeFilesViewModel.requestData(); timelapseViewModel.requestData(); settingsViewModel.requestData(); + logViewModel.requestData(); loginStateViewModel.subscribe(function(change, data) { if ("login" == change) { diff --git a/src/octoprint/static/js/app/viewmodels/log.js b/src/octoprint/static/js/app/viewmodels/log.js new file mode 100644 index 00000000..15578885 --- /dev/null +++ b/src/octoprint/static/js/app/viewmodels/log.js @@ -0,0 +1,62 @@ +function LogViewModel(loginStateViewModel) { + var self = this; + + self.loginState = loginStateViewModel; + + // initialize list helper + self.listHelper = new ItemListHelper( + "logFiles", + { + "name": function(a, b) { + // sorts ascending + if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; + if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; + return 0; + }, + "creation": function(a, b) { + // sorts descending + if (a["date"] > b["date"]) return -1; + if (a["date"] < b["date"]) return 1; + return 0; + }, + "size": function(a, b) { + // sorts descending + if (a["bytes"] > b["bytes"]) return -1; + if (a["bytes"] < b["bytes"]) return 1; + return 0; + } + }, + { + }, + "name", + [], + [], + CONFIG_LOGFILESPERPAGE + ); + + self.requestData = function() { + $.ajax({ + url: API_BASEURL + "logs", + type: "GET", + dataType: "json", + success: self.fromResponse + }); + }; + + self.fromResponse = function(response) { + var files = response.files; + if (files === undefined) + return; + + self.listHelper.updateItems(files); + } + + self.removeFile = function(filename) { + $.ajax({ + url: API_BASEURL + "logs/" + filename, + type: "DELETE", + dataType: "json", + success: self.requestData + }); + } +} diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index 17f25b44..cae0178a 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -27,6 +27,7 @@ var CONFIG_GCODEFILESPERPAGE = 5; var CONFIG_TIMELAPSEFILESPERPAGE = 10; + var CONFIG_LOGFILESPERPAGE = 10; var CONFIG_USERSPERPAGE = 10; var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %} @@ -252,6 +253,7 @@ {% if enableGCodeVisualizer %}
  • GCode Viewer
  • {% endif %}
  • Terminal
  • {% if enableTimelapse %}
  • Timelapse
  • {% endif %} +
  • Logs
  • @@ -585,6 +587,40 @@
    {% endif %} +
    +

    Logs

    + + + + + + + + + + + + + + + + + +
    NameSizeAction
     | 
    + +
    @@ -644,6 +680,7 @@ +