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):