From 097800ae9ccd6da666db044d3736618228cba0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 27 Aug 2015 10:29:11 +0200 Subject: [PATCH] Made diskspace running low a bit more obvious * Added "total" space to "free" in file list * Added configurable space thresholds. If free space is below "warning" threshold, exclamation sign will be added to report. If free space is below "critical" threshold, report will be styled bold and red. --- setup.py | 3 +- src/octoprint/server/api/files.py | 9 ++-- src/octoprint/server/api/log.py | 7 +++- src/octoprint/server/api/settings.py | 7 ++++ src/octoprint/settings.py | 4 ++ src/octoprint/static/js/app/helpers.js | 42 +++++++++++++++++++ .../static/js/app/viewmodels/files.js | 37 ++++++++++++++-- .../static/js/app/viewmodels/settings.js | 5 +++ .../templates/dialogs/settings/folders.jinja2 | 19 +++++++++ src/octoprint/templates/sidebar/files.jinja2 | 4 +- src/octoprint/util/__init__.py | 28 +++---------- 11 files changed, 130 insertions(+), 35 deletions(-) diff --git a/setup.py b/setup.py index 7d4956f6..ada6ae9f 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,8 @@ INSTALL_REQUIRES = [ "rsa", "pkginfo", "requests", - "semantic_version" + "semantic_version", + "psutil" ] # Additional requirements for optional install options diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index 5edb1dab..33ae4efd 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -7,7 +7,6 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms from flask import request, jsonify, make_response, url_for -import octoprint.util as util from octoprint.filemanager.destinations import FileDestinations from octoprint.settings import settings, valid_boolean_trues from octoprint.server import printer, fileManager, slicingManager, eventManager, NO_CONTENT @@ -18,6 +17,8 @@ import octoprint.filemanager import octoprint.filemanager.util import octoprint.slicing +import psutil + #~~ GCODE file handling @@ -29,7 +30,8 @@ def readGcodeFiles(): filter = request.values["filter"] files = _getFileList(FileDestinations.LOCAL, filter=filter) files.extend(_getFileList(FileDestinations.SDCARD)) - return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads"))) + usage = psutil.disk_usage(settings().getBaseFolder("uploads")) + return jsonify(files=files, free=usage.free, total=usage.total) @api.route("/files/", methods=["GET"]) @@ -40,7 +42,8 @@ def readGcodeFilesForOrigin(origin): files = _getFileList(origin) if origin == FileDestinations.LOCAL: - return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads"))) + usage = psutil.disk_usage(settings().getBaseFolder("uploads")) + return jsonify(files=files, free=usage.free, total=usage.total) else: return jsonify(files=files) diff --git a/src/octoprint/server/api/log.py b/src/octoprint/server/api/log.py index ccfeb01a..ba298b33 100644 --- a/src/octoprint/server/api/log.py +++ b/src/octoprint/server/api/log.py @@ -15,15 +15,18 @@ from octoprint.settings import settings from octoprint.server import NO_CONTENT, admin_permission from octoprint.server.util.flask import redirect_to_tornado, restricted_access from octoprint.server.api import api -from octoprint.util import get_free_bytes @api.route("/logs", methods=["GET"]) @restricted_access @admin_permission.require(403) def getLogFiles(): + import psutil + usage = psutil.disk_usage(settings().getBaseFolder("logs")) + files = _getLogFiles() - return jsonify(files=files, free=get_free_bytes(settings().getBaseFolder("logs"))) + + return jsonify(files=files, free=usage.free, total=usage.total) @api.route("/logs/", methods=["GET"]) diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 78858407..36650fd2 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -123,6 +123,10 @@ def getSettings(): "systemShutdownCommand": s.get(["server", "commands", "systemShutdownCommand"]), "systemRestartCommand": s.get(["server", "commands", "systemRestartCommand"]), "serverRestartCommand": s.get(["server", "commands", "serverRestartCommand"]) + }, + "diskspace": { + "warning": s.getInt(["server", "diskspace", "warning"]), + "critical": s.getInt(["server", "diskspace", "critical"]) } } } @@ -276,6 +280,9 @@ def _saveSettings(data): if "systemShutdownCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "systemShutdownCommand"], data["server"]["commands"]["systemShutdownCommand"]) if "systemRestartCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "systemRestartCommand"], data["server"]["commands"]["systemRestartCommand"]) if "serverRestartCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "serverRestartCommand"], data["server"]["commands"]["serverRestartCommand"]) + if "diskspace" in data["server"]: + if "warning" in data["server"]["diskspace"]: s.setInt(["server", "diskspace", "warning"], data["server"]["diskspace"]["warning"]) + if "critical" in data["server"]["diskspace"]: s.setInt(["server", "diskspace", "critical"], data["server"]["diskspace"]["critical"]) if "plugins" in data: for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin): diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 2419b8e4..6c7987dc 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -112,6 +112,10 @@ default_settings = { "systemShutdownCommand": None, "systemRestartCommand": None, "serverRestartCommand": None + }, + "diskspace": { + "warning": 500 * 1024 * 1024, # 500 MB + "critical": 200 * 1024 * 1024, # 200 MB } }, "webcam": { diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index 2652d601..8a67a576 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -333,6 +333,34 @@ function formatSize(bytes) { return _.sprintf("%.1f%s", bytes, "TB"); } +function bytesFromSize(size) { + if (size == undefined || size.trim() == "") return undefined; + + var parsed = size.match(/^([+]?[0-9]*\.?[0-9]+)(?:\s*)?(.*)$/); + var number = parsed[1]; + var unit = parsed[2].trim(); + + if (unit == "") return parseFloat(number); + + var units = { + b: 1, + byte: 1, + bytes: 1, + kb: 1024, + mb: Math.pow(1024, 2), + gb: Math.pow(1024, 3), + tb: Math.pow(1024, 4) + }; + unit = unit.toLowerCase(); + + if (!units.hasOwnProperty(unit)) { + return undefined; + } + + var factor = units[unit]; + return number * factor; +} + function formatDuration(seconds) { if (!seconds) return "-"; if (seconds < 0) return "00:00:00"; @@ -698,3 +726,17 @@ function callViewModelsIf(allViewModels, method, condition, callback) { } }); } + +var sizeObservable = function(observable) { + return ko.computed({ + read: function() { + return formatSize(observable()); + }, + write: function(value) { + var result = bytesFromSize(value); + if (result != undefined) { + observable(result); + } + } + }) +}; diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 60e700cc..885f5a40 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -2,9 +2,10 @@ $(function() { function GcodeFilesViewModel(parameters) { var self = this; - self.printerState = parameters[0]; + self.settingsViewModel = parameters[0]; self.loginState = parameters[1]; - self.slicing = parameters[2]; + self.printerState = parameters[2]; + self.slicing = parameters[3]; self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); @@ -21,11 +22,35 @@ $(function() { }); self.freeSpace = ko.observable(undefined); + self.totalSpace = ko.observable(undefined); self.freeSpaceString = ko.computed(function() { if (!self.freeSpace()) return "-"; return formatSize(self.freeSpace()); }); + self.totalSpaceString = ko.computed(function() { + if (!self.totalSpace()) + return "-"; + return formatSize(self.totalSpace()); + }); + + self.diskusageWarning = ko.computed(function() { + return self.freeSpace() != undefined + && self.freeSpace() < self.settingsViewModel.server_diskspace_warning(); + }); + self.diskusageCritical = ko.computed(function() { + return self.freeSpace() != undefined + && self.freeSpace() < self.settingsViewModel.server_diskspace_critical(); + }); + self.diskusageString = ko.computed(function() { + if (self.diskusageCritical()) { + return gettext("Your available free disk space is critically low."); + } else if (self.diskusageWarning()) { + return gettext("Your available free disk space is starting to run low."); + } else { + return gettext("Your current disk usage."); + } + }); self.uploadButton = undefined; @@ -155,10 +180,14 @@ $(function() { } } - if (response.free) { + if (response.free != undefined) { self.freeSpace(response.free); } + if (response.total != undefined) { + self.totalSpace(response.total); + } + self.highlightFilename(self.printerState.filename()); }; @@ -575,7 +604,7 @@ $(function() { OCTOPRINT_VIEWMODELS.push([ GcodeFilesViewModel, - ["printerStateViewModel", "loginStateViewModel", "slicingViewModel"], + ["settingsViewModel", "loginStateViewModel", "printerStateViewModel", "slicingViewModel"], "#files_wrapper" ]); }); diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 561fbb5a..9ac886e4 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -165,6 +165,11 @@ $(function() { self.server_commands_systemRestartCommand = ko.observable(undefined); self.server_commands_serverRestartCommand = ko.observable(undefined); + self.server_diskspace_warning = ko.observable(); + self.server_diskspace_critical = ko.observable(); + self.server_diskspace_warning_str = sizeObservable(self.server_diskspace_warning); + self.server_diskspace_critical_str = sizeObservable(self.server_diskspace_critical); + self.settings = undefined; self.lastReceivedSettings = undefined; diff --git a/src/octoprint/templates/dialogs/settings/folders.jinja2 b/src/octoprint/templates/dialogs/settings/folders.jinja2 index 5e19f866..f72c04e8 100644 --- a/src/octoprint/templates/dialogs/settings/folders.jinja2 +++ b/src/octoprint/templates/dialogs/settings/folders.jinja2 @@ -1,4 +1,6 @@
+

{{ _('Folders') }}

+
@@ -36,4 +38,21 @@
+ +

{{ _('Disk space thresholds') }}

+ +

{{ _('If the free disk space falls below these thresholds, OctoPrint will warn the user.') }}

+ +
+ +
+ +
+
+
+ +
+ +
+
diff --git a/src/octoprint/templates/sidebar/files.jinja2 b/src/octoprint/templates/sidebar/files.jinja2 index 2935855b..39b4d9c8 100644 --- a/src/octoprint/templates/sidebar/files.jinja2 +++ b/src/octoprint/templates/sidebar/files.jinja2 @@ -33,8 +33,8 @@
-
- {{ _('Free') }}: +
+ {{ _('Free') }}: / {{ _('Total') }}:
diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index 92077883..b4b156e5 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -19,7 +19,6 @@ from functools import wraps import warnings import contextlib - logger = logging.getLogger(__name__) def warning_decorator_factory(warning_type): @@ -198,29 +197,12 @@ def get_exception_string(): return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1]) +@deprecated("get_free_bytes has been deprecated and will be removed in the future", + includedoc="Replaced by `psutil.disk_usage `_.", + since="1.2.5") def get_free_bytes(path): - """ - Retrieves the number of free bytes on the partition ``path`` is located at and returns it. Works on both Windows and - Unix/Linux. - - Taken from http://stackoverflow.com/a/2372171/2028598 - - Arguments: - path (string): The path for which to check the remaining partition space. - - Returns: - int: The amount of bytes still left on the partition. - """ - - path = os.path.abspath(path) - if sys.platform == "win32": - import ctypes - freeBytes = ctypes.c_ulonglong(0) - ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path), None, None, ctypes.pointer(freeBytes)) - return freeBytes.value - else: - st = os.statvfs(path) - return st.f_bavail * st.f_frsize + import psutil + return psutil.disk_usage(path).free def get_dos_filename(origin, existing_filenames=None, extension=None, **kwargs):