From 268886576f4dc27ae599058189d78d0cc47b7966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 18 Jun 2015 09:30:25 +0200 Subject: [PATCH 01/18] More resilience against wonky plugin compatibility data (cherry picked from commit 5ed37e0) --- src/octoprint/plugins/pluginmanager/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 9dd14b8d..495f0b83 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -506,7 +506,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, ) if "compatibility" in entry: - if "octoprint" in entry["compatibility"]: + if "octoprint" in entry["compatibility"] and entry["compatibility"]["octoprint"] is not None and len(entry["compatibility"]["octoprint"]): import semantic_version for octo_compat in entry["compatibility"]["octoprint"]: s = semantic_version.Spec("=={}".format(octo_compat)) @@ -515,7 +515,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, else: result["is_compatible"]["octoprint"] = False - if "os" in entry["compatibility"]: + if "os" in entry["compatibility"] and entry["compatibility"]["os"] is not None and len(entry["compatibility"]["os"]): result["is_compatible"]["os"] = current_os in entry["compatibility"]["os"] return result From 0ce1575e12b2e66fe4db0e1cff8f068938ed1a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 18 Jun 2015 11:37:08 +0200 Subject: [PATCH 02/18] Don't allow plugin management while the printer is printing --- .../plugins/pluginmanager/__init__.py | 8 ++-- .../pluginmanager/static/js/pluginmanager.js | 45 ++++++++++++++++--- .../templates/pluginmanager_settings.jinja2 | 13 +++++- .../plugins/softwareupdate/__init__.py | 4 +- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 495f0b83..0b0c6a83 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -141,6 +141,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, if not admin_permission.can(): return make_response("Insufficient rights", 403) + if self._printer.is_printing() or self._printer.is_paused(): + # do not update while a print job is running + return make_response("Printer is currently printing or paused", 409) + if command == "install": url = data["url"] plugin_name = data["plugin"] if "plugin" in data else None @@ -165,10 +169,6 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, plugin = self._plugin_manager.plugins[plugin_name] return self.command_toggle(plugin, command) - elif command == "refresh_repository": - self._repository_available = self._refresh_repository() - return jsonify(repository=dict(available=self._repository_available, plugins=self._repository_plugins)) - def command_install(self, url=None, path=None, force=False, reinstall=None, dependency_links=False): if url is not None: pip_args = ["install", sarge.shell_quote(url)] diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index ca7eaae9..c8178d20 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -4,6 +4,7 @@ $(function() { self.loginState = parameters[0]; self.settingsViewModel = parameters[1]; + self.printerState = parameters[2]; self.plugins = new ItemListHelper( "plugin.pluginmanager.installedplugins", @@ -76,6 +77,22 @@ $(function() { self.workingDialog = undefined; self.workingOutput = undefined; + self.enableManagement = ko.computed(function() { + return !self.printerState.isPrinting(); + }); + + self.enableToggle = function(data) { + return self.enableManagement() && data.key != 'pluginmanager'; + }; + + self.enableUninstall = function(data) { + return self.enableManagement() && !data.bundled && data.key != 'pluginmanager' && !data.pending_uninstall; + }; + + self.enableRepoInstall = function(data) { + return self.enableManagement() && self.isCompatible(data); + }; + self.invalidUrl = ko.computed(function() { var url = self.installUrl(); return url !== undefined && url.trim() != "" && !(_.startsWith(url.toLocaleLowerCase(), "http://") || _.startsWith(url.toLocaleLowerCase(), "https://")); @@ -83,7 +100,7 @@ $(function() { self.enableUrlInstall = ko.computed(function() { var url = self.installUrl(); - return url !== undefined && url.trim() != "" && !self.invalidUrl(); + return self.enableManagement() && url !== undefined && url.trim() != "" && !self.invalidUrl(); }); self.invalidArchive = ko.computed(function() { @@ -93,7 +110,7 @@ $(function() { self.enableArchiveInstall = ko.computed(function() { var name = self.uploadFilename(); - return name !== undefined && name.trim() != "" && !self.invalidArchive(); + return self.enableManagement() && name !== undefined && name.trim() != "" && !self.invalidArchive(); }); self.uploadElement.fileupload({ @@ -187,6 +204,10 @@ $(function() { return; } + if (!self.enableManagement()) { + return; + } + if (data.key == "pluginmanager") return; var command = self._getToggleCommand(data); @@ -217,6 +238,10 @@ $(function() { return; } + if (!self.enableManagement()) { + return; + } + if (self.installed(data)) { self.installPlugin(data.archive, data.title, data.id, data.follow_dependency_links || self.followDependencyLinks()); } else { @@ -229,6 +254,10 @@ $(function() { return; } + if (!self.enableManagement()) { + return; + } + if (url === undefined) { url = self.installUrl(); } @@ -279,6 +308,10 @@ $(function() { return; } + if (!self.enableManagement()) { + return; + } + if (data.bundled) return; if (data.key == "pluginmanager") return; @@ -305,9 +338,7 @@ $(function() { return; } - self._postCommand("refresh_repository", {}, function(data) { - self._fromRepositoryResponse(data.repository); - }) + self.requestData(true); }; self.installed = function(data) { @@ -420,7 +451,7 @@ $(function() { self.toggleButtonCss = function(data) { var icon = self._getToggleCommand(data) == "enable" ? "icon-circle-blank" : "icon-circle"; - var disabled = (data.key == "pluginmanager") ? " disabled" : ""; + var disabled = (self.enableToggle(data)) ? "" : " disabled"; return icon + disabled; }; @@ -578,5 +609,5 @@ $(function() { } // view model class, parameters for constructor, container to bind to - ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel"], "#settings_plugin_pluginmanager"]); + ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel", "printerStateViewModel"], "#settings_plugin_pluginmanager"]); }); diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index f9710de0..8cffb17f 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -1,3 +1,11 @@ +{% macro pluginmanager_printing() %} +
+ {{ _('Take note that all plugin management functionality is disabled while your printer is printing.') }} +
+{% endmacro %} + +{{ pluginmanager_printing() }} +

{{ _('Installed Plugins') }}

@@ -20,7 +28,7 @@ @@ -58,6 +66,7 @@

{{ _('Install new Plugins...') }}

diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 7d74ff2e..d00d22cf 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -116,10 +116,10 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, def perform_update(self): if self._printer.is_printing() or self._printer.is_paused(): # do not update while a print job is running - flask.make_response("Printer is currently printing or paused", 409) + return flask.make_response("Printer is currently printing or paused", 409) if not "application/json" in flask.request.headers["Content-Type"]: - flask.make_response("Expected content-type JSON", 400) + return flask.make_response("Expected content-type JSON", 400) json_data = flask.request.json From cbd3b1424b15040bce1a3012b2cc0bb8199d8fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 18 Jun 2015 12:05:11 +0200 Subject: [PATCH 03/18] Default to full platform compatibility --- .../plugins/softwareupdate/__init__.py | 15 +- .../static/js/softwareupdate.js | 158 +++++++++--------- 2 files changed, 91 insertions(+), 82 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index d00d22cf..a74e85f9 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -17,7 +17,7 @@ from . import version_checks, updaters, exceptions, util from octoprint.server.util.flask import restricted_access -from octoprint.server import admin_permission +from octoprint.server import admin_permission, user_permission from octoprint.util import dict_merge import octoprint.settings @@ -43,6 +43,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, def refresh_checks(name, plugin): self._refresh_configured_checks = True + self._send_client_message("update_versions") + self._plugin_lifecycle_manager.add_callback("enabled", refresh_checks) self._plugin_lifecycle_manager.add_callback("disabled", refresh_checks) @@ -92,6 +94,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, #~~ BluePrint API @octoprint.plugin.BlueprintPlugin.route("/check", methods=["GET"]) + @restricted_access + @user_permission.require(403) def check_for_update(self): if "check" in flask.request.values: check_targets = map(str.strip, flask.request.values["check"].split(",")) @@ -101,7 +105,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if "force" in flask.request.values and flask.request.values["force"] in octoprint.settings.valid_boolean_trues: force = True else: - force=False + force = False try: information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force) @@ -128,9 +132,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, else: check_targets = None - if "force" in json_data: - from octoprint.settings import valid_boolean_trues - force = (json_data["force"] in valid_boolean_trues) + if "force" in json_data and json_data["force"] in octoprint.settings.valid_boolean_trues: + force = True else: force = False @@ -382,7 +385,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if target in checks: # TODO make this cleaner, right now it saves too much to disk checks[target]["current"] = target_version - self._settings.set(["checks"], checks) + self._settings.set(["checks", target], checks[target]) # we have to save here (even though that makes us save quite often) since otherwise the next # load will overwrite our changes we just made diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index b8f0c3c6..cf5cd7f8 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -89,6 +89,82 @@ $(function() { self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); }; + self.fromCheckResponse = function(data) { + var versions = []; + _.each(data.information, function(value, key) { + value["key"] = key; + + if (!value.hasOwnProperty("displayName") || value.displayName == "") { + value.displayName = value.key; + } + if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") { + value.displayVersion = value.information.local.name; + } + + versions.push(value); + }); + self.versions.updateItems(versions); + + if (data.status == "updateAvailable" || data.status == "updatePossible") { + var text = gettext("There are updates available for the following components:"); + + text += "
    "; + _.each(self.versions.items(), function(update_info) { + if (update_info.updateAvailable) { + var displayName = update_info.key; + if (update_info.hasOwnProperty("displayName")) { + displayName = update_info.displayName; + } + text += "
  • " + displayName + (update_info.updatePossible ? " " : "") + "
  • "; + } + }); + text += "
"; + + text += "" + gettext("Those components marked with can be updated directly.") + ""; + + var options = { + title: gettext("Update Available"), + text: text, + hide: false + }; + var eventListeners = {}; + + if (data.status == "updatePossible" && self.loginState.isAdmin()) { + // if user is admin, add action buttons + options["confirm"] = { + confirm: true, + buttons: [{ + text: gettext("Ignore"), + click: function() { + self._markNotificationAsSeen(data.information); + self._showPopup({ + text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"") + }); + } + }, { + text: gettext("Update now"), + addClass: "btn-primary", + click: self.update + }] + }; + options["buttons"] = { + closer: false, + sticker: false + }; + } + + if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) { + self._showPopup(options, eventListeners); + } + } else if (data.status == "current" && showIfNothingNew) { + self._showPopup({ + title: gettext("Everything is up-to-date"), + hide: false, + type: "success" + }); + } + }; + self.performCheck = function(showIfNothingNew, force, ignoreSeen) { if (!self.loginState.isUser()) return; @@ -101,81 +177,7 @@ $(function() { url: url, type: "GET", dataType: "json", - success: function(data) { - var versions = []; - _.each(data.information, function(value, key) { - value["key"] = key; - - if (!value.hasOwnProperty("displayName") || value.displayName == "") { - value.displayName = value.key; - } - if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") { - value.displayVersion = value.information.local.name; - } - - versions.push(value); - }); - self.versions.updateItems(versions); - - if (data.status == "updateAvailable" || data.status == "updatePossible") { - var text = gettext("There are updates available for the following components:"); - - text += "
    "; - _.each(self.versions.items(), function(update_info) { - if (update_info.updateAvailable) { - var displayName = update_info.key; - if (update_info.hasOwnProperty("displayName")) { - displayName = update_info.displayName; - } - text += "
  • " + displayName + (update_info.updatePossible ? " " : "") + "
  • "; - } - }); - text += "
"; - - text += "" + gettext("Those components marked with can be updated directly.") + ""; - - var options = { - title: gettext("Update Available"), - text: text, - hide: false - }; - var eventListeners = {}; - - if (data.status == "updatePossible" && self.loginState.isAdmin()) { - // if user is admin, add action buttons - options["confirm"] = { - confirm: true, - buttons: [{ - text: gettext("Ignore"), - click: function() { - self._markNotificationAsSeen(data.information); - self._showPopup({ - text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"") - }); - } - }, { - text: gettext("Update now"), - addClass: "btn-primary", - click: self.update - }] - }; - options["buttons"] = { - closer: false, - sticker: false - }; - } - - if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) { - self._showPopup(options, eventListeners); - } - } else if (data.status == "current" && showIfNothingNew) { - self._showPopup({ - title: gettext("Everything is up-to-date"), - hide: false, - type: "success" - }); - } - } + success: self.fromCheckResponse }); }; @@ -421,6 +423,10 @@ $(function() { self.updateInProgress = false; break; } + case "update_versions": { + self.performCheck(); + break; + } } if (options != undefined) { @@ -432,4 +438,4 @@ $(function() { // view model class, parameters for constructor, container to bind to ADDITIONAL_VIEWMODELS.push([SoftwareUpdateViewModel, ["loginStateViewModel", "printerStateViewModel", "settingsViewModel"], document.getElementById("settings_plugin_softwareupdate")]); -}); \ No newline at end of file +}); From 0d886ab6f10a223b59dc983b2c222967964ba73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 18 Jun 2015 12:11:54 +0200 Subject: [PATCH 04/18] Revert cbd3b1424b15040bce1a3012b2cc0bb8199d8fa1 That was a bit too early and an accident caused by a too fast "OK" in IntelliJ (as quite visible from the completely unrelated commit message) --- .../plugins/softwareupdate/__init__.py | 15 +- .../static/js/softwareupdate.js | 156 +++++++++--------- 2 files changed, 81 insertions(+), 90 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index a74e85f9..d00d22cf 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -17,7 +17,7 @@ from . import version_checks, updaters, exceptions, util from octoprint.server.util.flask import restricted_access -from octoprint.server import admin_permission, user_permission +from octoprint.server import admin_permission from octoprint.util import dict_merge import octoprint.settings @@ -43,8 +43,6 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, def refresh_checks(name, plugin): self._refresh_configured_checks = True - self._send_client_message("update_versions") - self._plugin_lifecycle_manager.add_callback("enabled", refresh_checks) self._plugin_lifecycle_manager.add_callback("disabled", refresh_checks) @@ -94,8 +92,6 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, #~~ BluePrint API @octoprint.plugin.BlueprintPlugin.route("/check", methods=["GET"]) - @restricted_access - @user_permission.require(403) def check_for_update(self): if "check" in flask.request.values: check_targets = map(str.strip, flask.request.values["check"].split(",")) @@ -105,7 +101,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if "force" in flask.request.values and flask.request.values["force"] in octoprint.settings.valid_boolean_trues: force = True else: - force = False + force=False try: information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force) @@ -132,8 +128,9 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, else: check_targets = None - if "force" in json_data and json_data["force"] in octoprint.settings.valid_boolean_trues: - force = True + if "force" in json_data: + from octoprint.settings import valid_boolean_trues + force = (json_data["force"] in valid_boolean_trues) else: force = False @@ -385,7 +382,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if target in checks: # TODO make this cleaner, right now it saves too much to disk checks[target]["current"] = target_version - self._settings.set(["checks", target], checks[target]) + self._settings.set(["checks"], checks) # we have to save here (even though that makes us save quite often) since otherwise the next # load will overwrite our changes we just made diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index cf5cd7f8..74472574 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -89,82 +89,6 @@ $(function() { self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); }; - self.fromCheckResponse = function(data) { - var versions = []; - _.each(data.information, function(value, key) { - value["key"] = key; - - if (!value.hasOwnProperty("displayName") || value.displayName == "") { - value.displayName = value.key; - } - if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") { - value.displayVersion = value.information.local.name; - } - - versions.push(value); - }); - self.versions.updateItems(versions); - - if (data.status == "updateAvailable" || data.status == "updatePossible") { - var text = gettext("There are updates available for the following components:"); - - text += "
    "; - _.each(self.versions.items(), function(update_info) { - if (update_info.updateAvailable) { - var displayName = update_info.key; - if (update_info.hasOwnProperty("displayName")) { - displayName = update_info.displayName; - } - text += "
  • " + displayName + (update_info.updatePossible ? " " : "") + "
  • "; - } - }); - text += "
"; - - text += "" + gettext("Those components marked with can be updated directly.") + ""; - - var options = { - title: gettext("Update Available"), - text: text, - hide: false - }; - var eventListeners = {}; - - if (data.status == "updatePossible" && self.loginState.isAdmin()) { - // if user is admin, add action buttons - options["confirm"] = { - confirm: true, - buttons: [{ - text: gettext("Ignore"), - click: function() { - self._markNotificationAsSeen(data.information); - self._showPopup({ - text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"") - }); - } - }, { - text: gettext("Update now"), - addClass: "btn-primary", - click: self.update - }] - }; - options["buttons"] = { - closer: false, - sticker: false - }; - } - - if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) { - self._showPopup(options, eventListeners); - } - } else if (data.status == "current" && showIfNothingNew) { - self._showPopup({ - title: gettext("Everything is up-to-date"), - hide: false, - type: "success" - }); - } - }; - self.performCheck = function(showIfNothingNew, force, ignoreSeen) { if (!self.loginState.isUser()) return; @@ -177,7 +101,81 @@ $(function() { url: url, type: "GET", dataType: "json", - success: self.fromCheckResponse + success: function(data) { + var versions = []; + _.each(data.information, function(value, key) { + value["key"] = key; + + if (!value.hasOwnProperty("displayName") || value.displayName == "") { + value.displayName = value.key; + } + if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") { + value.displayVersion = value.information.local.name; + } + + versions.push(value); + }); + self.versions.updateItems(versions); + + if (data.status == "updateAvailable" || data.status == "updatePossible") { + var text = gettext("There are updates available for the following components:"); + + text += "
    "; + _.each(self.versions.items(), function(update_info) { + if (update_info.updateAvailable) { + var displayName = update_info.key; + if (update_info.hasOwnProperty("displayName")) { + displayName = update_info.displayName; + } + text += "
  • " + displayName + (update_info.updatePossible ? " " : "") + "
  • "; + } + }); + text += "
"; + + text += "" + gettext("Those components marked with can be updated directly.") + ""; + + var options = { + title: gettext("Update Available"), + text: text, + hide: false + }; + var eventListeners = {}; + + if (data.status == "updatePossible" && self.loginState.isAdmin()) { + // if user is admin, add action buttons + options["confirm"] = { + confirm: true, + buttons: [{ + text: gettext("Ignore"), + click: function() { + self._markNotificationAsSeen(data.information); + self._showPopup({ + text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"") + }); + } + }, { + text: gettext("Update now"), + addClass: "btn-primary", + click: self.update + }] + }; + options["buttons"] = { + closer: false, + sticker: false + }; + } + + if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) { + self._showPopup(options, eventListeners); + } + } else if (data.status == "current" && showIfNothingNew) { + self._showPopup({ + title: gettext("Everything is up-to-date"), + hide: false, + type: "success" + }); + } + } }); }; @@ -423,10 +421,6 @@ $(function() { self.updateInProgress = false; break; } - case "update_versions": { - self.performCheck(); - break; - } } if (options != undefined) { From 31d7eaad90976557477a4b5777d5bbfb7a30d122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 18 Jun 2015 13:00:18 +0200 Subject: [PATCH 05/18] Add ";" delimiters between bundled JS files This is to make sure that they all end on a ; and hence stuff doesn't break if a file included directly after starts with a (...) construct. Of course it would be better if all bundled files are valid in that matter, however since we can't enforce that, we'll add this slight overhead to reduce the risk of stuff breaking. See for example: http://stackoverflow.com/questions/20307462/js-cant-combine-lib-files --- src/octoprint/server/__init__.py | 39 ++++++++++++++++++++++++-------- src/octoprint/settings.py | 3 ++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 437333ed..fca1ad8a 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -674,6 +674,20 @@ class Server(): base_folder = settings().getBaseFolder("generated") + # clean the folder + if settings().getBoolean(["devel", "webassets", "clean_on_startup"]): + import shutil + for entry in ("webassets", ".webassets-cache"): + path = os.path.join(base_folder, entry) + self._logger.debug("Deleting {path}...".format(**locals())) + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + elif os.path.isfile(path): + try: + os.remove(path) + except: + self._logger.exception("Exception while trying to delete {entry} from {base_folder}".format(**locals())) + AdjustedEnvironment = type(Environment)(Environment.__name__, (Environment,), dict( resolver_class=util.flask.PluginAssetResolver )) @@ -754,16 +768,7 @@ class Server(): if len(less_app) == 0: less_app = ["empty"] - js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js") - if settings().getBoolean(["devel", "webassets", "minify"]): - js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin") - else: - js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js") - - css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css") - css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css") - - from webassets.filter import register_filter + from webassets.filter import register_filter, Filter from webassets.filter.cssrewrite.base import PatternRewriter import re class LessImportRewrite(PatternRewriter): @@ -782,7 +787,21 @@ class Server(): return "{import_with_options}\"{import_url}\";".format(**locals()) + class JsDelimiterBundle(Filter): + name = "js_delimiter_bundler" + options = {} + def input(self, _in, out, **kwargs): + out.write(_in.read()) + out.write("\n;\n") + register_filter(LessImportRewrite) + register_filter(JsDelimiterBundle) + + js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js", filters="js_delimiter_bundler") + js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler") + + css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css") + css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css") all_less_bundle = Bundle(*less_app, output="webassets/packed_app.less", filters="less_importrewrite") diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index ab4e24f3..b44a6844 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -251,7 +251,8 @@ default_settings = { }, "webassets": { "minify": False, - "bundle": True + "bundle": True, + "clean_on_startup": True }, "virtualPrinter": { "enabled": False, From 6e90c9d730dac73e2ed987b0b52d1a64225446fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 18 Jun 2015 13:49:40 +0200 Subject: [PATCH 06/18] Reintroduced minification Still opt-in though. --- src/octoprint/server/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index fca1ad8a..26687855 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -798,7 +798,10 @@ class Server(): register_filter(JsDelimiterBundle) js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js", filters="js_delimiter_bundler") - js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler") + if settings().getBoolean(["devel", "webassets", "minify"]): + js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="rjsmin, js_delimiter_bundler") + else: + js_app_bundle = Bundle(*js_app, output="webassets/packed_app.js", filters="js_delimiter_bundler") css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css") css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css") From a26b203205d707a70d475e09877b1908866877c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 18 Jun 2015 17:08:56 +0200 Subject: [PATCH 07/18] Enforce update of updateable software after plugin changes When any plugins are enabled/disabled, the client now gets a trigger to fetch a fresh list of update information from the server. This should take care of any old popups still hinting at now uninstalled plugins. --- .../plugins/softwareupdate/__init__.py | 10 +- .../static/js/softwareupdate.js | 168 ++++++++++-------- 2 files changed, 98 insertions(+), 80 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index d00d22cf..76264435 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -43,6 +43,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, def refresh_checks(name, plugin): self._refresh_configured_checks = True + self._send_client_message("update_versions") + self._plugin_lifecycle_manager.add_callback("enabled", refresh_checks) self._plugin_lifecycle_manager.add_callback("disabled", refresh_checks) @@ -92,6 +94,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, #~~ BluePrint API @octoprint.plugin.BlueprintPlugin.route("/check", methods=["GET"]) + @restricted_access def check_for_update(self): if "check" in flask.request.values: check_targets = map(str.strip, flask.request.values["check"].split(",")) @@ -101,7 +104,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if "force" in flask.request.values and flask.request.values["force"] in octoprint.settings.valid_boolean_trues: force = True else: - force=False + force = False try: information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force) @@ -128,9 +131,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, else: check_targets = None - if "force" in json_data: - from octoprint.settings import valid_boolean_trues - force = (json_data["force"] in valid_boolean_trues) + if "force" in json_data and json_data["force"] in octoprint.settings.valid_boolean_trues: + force = True else: force = False diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index 74472574..19c88137 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -44,9 +44,7 @@ $(function() { }; self._showPopup = function(options, eventListeners) { - if (self.popup !== undefined) { - self.popup.remove(); - } + self._closePopup(); self.popup = new PNotify(options); if (eventListeners) { @@ -65,6 +63,12 @@ $(function() { } }; + self._closePopup = function() { + if (self.popup !== undefined) { + self.popup.remove(); + } + }; + self.showPluginSettings = function() { self._copyConfig(); self.configurationDialog.modal(); @@ -89,6 +93,86 @@ $(function() { self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); }; + self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) { + var versions = []; + _.each(data.information, function(value, key) { + value["key"] = key; + + if (!value.hasOwnProperty("displayName") || value.displayName == "") { + value.displayName = value.key; + } + if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") { + value.displayVersion = value.information.local.name; + } + + versions.push(value); + }); + self.versions.updateItems(versions); + + if (data.status == "updateAvailable" || data.status == "updatePossible") { + var text = gettext("There are updates available for the following components:"); + + text += "
    "; + _.each(self.versions.items(), function(update_info) { + if (update_info.updateAvailable) { + var displayName = update_info.key; + if (update_info.hasOwnProperty("displayName")) { + displayName = update_info.displayName; + } + text += "
  • " + displayName + (update_info.updatePossible ? " " : "") + "
  • "; + } + }); + text += "
"; + + text += "" + gettext("Those components marked with can be updated directly.") + ""; + + var options = { + title: gettext("Update Available"), + text: text, + hide: false + }; + var eventListeners = {}; + + if (data.status == "updatePossible" && self.loginState.isAdmin()) { + // if user is admin, add action buttons + options["confirm"] = { + confirm: true, + buttons: [{ + text: gettext("Ignore"), + click: function() { + self._markNotificationAsSeen(data.information); + self._showPopup({ + text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"") + }); + } + }, { + text: gettext("Update now"), + addClass: "btn-primary", + click: self.update + }] + }; + options["buttons"] = { + closer: false, + sticker: false + }; + } + + if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) { + self._showPopup(options, eventListeners); + } + } else if (data.status == "current") { + if (showIfNothingNew) { + self._showPopup({ + title: gettext("Everything is up-to-date"), + hide: false, + type: "success" + }); + } else { + self._closePopup(); + } + } + }; + self.performCheck = function(showIfNothingNew, force, ignoreSeen) { if (!self.loginState.isUser()) return; @@ -102,79 +186,7 @@ $(function() { type: "GET", dataType: "json", success: function(data) { - var versions = []; - _.each(data.information, function(value, key) { - value["key"] = key; - - if (!value.hasOwnProperty("displayName") || value.displayName == "") { - value.displayName = value.key; - } - if (!value.hasOwnProperty("displayVersion") || value.displayVersion == "") { - value.displayVersion = value.information.local.name; - } - - versions.push(value); - }); - self.versions.updateItems(versions); - - if (data.status == "updateAvailable" || data.status == "updatePossible") { - var text = gettext("There are updates available for the following components:"); - - text += "
    "; - _.each(self.versions.items(), function(update_info) { - if (update_info.updateAvailable) { - var displayName = update_info.key; - if (update_info.hasOwnProperty("displayName")) { - displayName = update_info.displayName; - } - text += "
  • " + displayName + (update_info.updatePossible ? " " : "") + "
  • "; - } - }); - text += "
"; - - text += "" + gettext("Those components marked with can be updated directly.") + ""; - - var options = { - title: gettext("Update Available"), - text: text, - hide: false - }; - var eventListeners = {}; - - if (data.status == "updatePossible" && self.loginState.isAdmin()) { - // if user is admin, add action buttons - options["confirm"] = { - confirm: true, - buttons: [{ - text: gettext("Ignore"), - click: function() { - self._markNotificationAsSeen(data.information); - self._showPopup({ - text: gettext("You can make this message display again via \"Settings\" > \"SoftwareUpdate\" > \"Check for update now\"") - }); - } - }, { - text: gettext("Update now"), - addClass: "btn-primary", - click: self.update - }] - }; - options["buttons"] = { - closer: false, - sticker: false - }; - } - - if (ignoreSeen || !self._hasNotificationBeenSeen(data.information)) { - self._showPopup(options, eventListeners); - } - } else if (data.status == "current" && showIfNothingNew) { - self._showPopup({ - title: gettext("Everything is up-to-date"), - hide: false, - type: "success" - }); - } + self.fromCheckResponse(data, ignoreSeen, showIfNothingNew); } }); }; @@ -421,6 +433,10 @@ $(function() { self.updateInProgress = false; break; } + case "update_versions": { + self.performCheck(); + break; + } } if (options != undefined) { From b40cba659a7b4096d6ce6a23e080cf77fb32bc9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 11:06:51 +0200 Subject: [PATCH 08/18] Fix: defaults are now correctly applied when setting config for plugins --- src/octoprint/settings.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index b44a6844..6c6dd06b 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -854,7 +854,7 @@ class Settings(object): return results def getInt(self, path, config=None, defaults=None, preprocessors=None): - value = self.get(path, defaults=defaults, preprocessors=preprocessors) + value = self.get(path, config=config, defaults=defaults, preprocessors=preprocessors) if value is None: return None @@ -931,14 +931,15 @@ class Settings(object): #~~ setter - def set(self, path, value, force=False, defaults=None, preprocessors=None): + def set(self, path, value, config=None, force=False, defaults=None, preprocessors=None): if len(path) == 0: return if self._mtime is not None and self.last_modified != self._mtime: self.load() - config = self._config + if config is None: + config = self._config if defaults is None: defaults = default_settings if preprocessors is None: @@ -974,9 +975,9 @@ class Settings(object): config[key] = value self._dirty = True - def setInt(self, path, value, force=False, defaults=None, preprocessors=None): + def setInt(self, path, value, config=None, force=False, defaults=None, preprocessors=None): if value is None: - self.set(path, None, force=force, defaults=defaults, preprocessors=preprocessors) + self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors) return try: @@ -985,11 +986,11 @@ class Settings(object): self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path)) return - self.set(path, intValue, force) + self.set(path, intValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors) - def setFloat(self, path, value, force=False, defaults=None, preprocessors=None): + def setFloat(self, path, value, config=None, force=False, defaults=None, preprocessors=None): if value is None: - self.set(path, None, force=force, defaults=defaults, preprocessors=preprocessors) + self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors) return try: @@ -998,15 +999,15 @@ class Settings(object): self._logger.warn("Could not convert %r to a valid integer when setting option %r" % (value, path)) return - self.set(path, floatValue, force) + self.set(path, floatValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors) - def setBoolean(self, path, value, force=False, defaults=None, preprocessors=None): + def setBoolean(self, path, value, config=None, force=False, defaults=None, preprocessors=None): if value is None or isinstance(value, bool): - self.set(path, value, force=force, defaults=defaults, preprocessors=preprocessors) + self.set(path, value, config=config, force=force, defaults=defaults, preprocessors=preprocessors) elif value.lower() in valid_boolean_trues: - self.set(path, True, force=force, defaults=defaults, preprocessors=preprocessors) + self.set(path, True, config=config, force=force, defaults=defaults, preprocessors=preprocessors) else: - self.set(path, False, force=force, defaults=defaults, preprocessors=preprocessors) + self.set(path, False, config=config, force=force, defaults=defaults, preprocessors=preprocessors) def setBaseFolder(self, type, path, force=False): if type not in default_settings["folder"].keys(): From 9f4a74cca065b89f841371d7a8c94b4f0d22b628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 11:08:11 +0200 Subject: [PATCH 09/18] Fix: setting defaults supplied to set/get from plugins are no longer overwritten --- src/octoprint/plugin/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 6477da92..dc5700cc 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -309,11 +309,17 @@ class PluginSettings(object): return result def add_getter_kwargs(kwargs): - kwargs.update(defaults=self.defaults, preprocessors=self.get_preprocessors) + if not "defaults" in kwargs: + kwargs.update(defaults=self.defaults) + if not "preprocessors" in kwargs: + kwargs.update(preprocessors=self.get_preprocessors) return kwargs def add_setter_kwargs(kwargs): - kwargs.update(defaults=self.defaults, preprocessors=self.set_preprocessors) + if not "defaults" in kwargs: + kwargs.update(defaults=self.defaults) + if not "preprocessors" in kwargs: + kwargs.update(preprocessors=self.set_preprocessors) return kwargs self.access_methods = dict( From e55677da22f1bb668c5f35ee080f6063bf047116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 11:09:08 +0200 Subject: [PATCH 10/18] Plugin system now support pre/post implementation init methods --- src/octoprint/plugin/core.py | 38 ++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index d5c59bd9..55973c2c 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -428,7 +428,9 @@ class PluginManager(object): It is able to discover plugins both through possible file system locations as well as customizable entry points. """ - def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None, plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None): + def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, + plugin_disabled_list=None, plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, + plugin_validators=None): self.logger = logging.getLogger(__name__) if logging_prefix is None: @@ -453,6 +455,8 @@ class PluginManager(object): self.implementation_injects = dict() self.implementation_inject_factories = [] + self.implementation_pre_inits = [] + self.implementation_post_inits = [] self.on_plugin_loaded = lambda *args, **kwargs: None self.on_plugin_unloaded = lambda *args, **kwargs: None @@ -849,27 +853,35 @@ class PluginManager(object): return False return hook in self.plugin_obsolete_hooks - def initialize_implementations(self, additional_injects=None, additional_inject_factories=None): + def initialize_implementations(self, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None): for name, plugin in self.enabled_plugins.items(): self.initialize_implementation_of_plugin(name, plugin, additional_injects=additional_injects, - additional_inject_factories=additional_inject_factories) + additional_inject_factories=additional_inject_factories, + additional_pre_inits=additional_pre_inits, + additional_post_inits=additional_post_inits) self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations))) - def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None): + def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None): if plugin.implementation is None: return return self.initialize_implementation(name, plugin, plugin.implementation, additional_injects=additional_injects, - additional_inject_factories=additional_inject_factories) + additional_inject_factories=additional_inject_factories, + additional_pre_inits=additional_pre_inits, + additional_post_inits=additional_post_inits) - def initialize_implementation(self, name, plugin, implementation, additional_injects=None, additional_inject_factories=None): + def initialize_implementation(self, name, plugin, implementation, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None): if additional_injects is None: additional_injects = dict() if additional_inject_factories is None: additional_inject_factories = [] + if additional_pre_inits is None: + additional_pre_inits = [] + if additional_post_inits is None: + additional_post_inits = [] injects = self.implementation_injects injects.update(additional_injects) @@ -877,6 +889,12 @@ class PluginManager(object): inject_factories = self.implementation_inject_factories inject_factories += additional_inject_factories + pre_inits = self.implementation_pre_inits + pre_inits += additional_pre_inits + + post_inits = self.implementation_post_inits + post_inits += additional_post_inits + try: kwargs = dict(injects) @@ -904,8 +922,16 @@ class PluginManager(object): for arg, value in return_value.items(): setattr(implementation, "_" + arg, value) + # execute any additional pre init methods + for pre_init in pre_inits: + pre_init(name, implementation) + implementation.initialize() + # execute any additional post init methods + for post_init in post_inits: + post_init(name, implementation) + except Exception as e: self._deactivate_plugin(name, plugin) plugin.enabled = False From d5af7b9b48e6c49fcdb092c80324f73209172008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 11:10:07 +0200 Subject: [PATCH 11/18] SettingsPlugins may now track configuration versions OctoPrint will take care of calling a migration function on the plugin if the plugin demands a newer configuration version than currently stored in config.yaml. --- src/octoprint/plugin/__init__.py | 1 + src/octoprint/plugin/types.py | 35 ++++++++++++++++++++++++++++++++ src/octoprint/server/__init__.py | 15 ++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index dc5700cc..4dde566f 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -281,6 +281,7 @@ class PluginSettings(object): defaults = dict() self.defaults = dict(plugins=dict()) self.defaults["plugins"][plugin_key] = defaults + self.defaults["plugins"][plugin_key]["_config_version"] = None if get_preprocessors is None: get_preprocessors = dict() diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 842e68f9..caabeed4 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -830,6 +830,41 @@ class SettingsPlugin(OctoPrintPlugin): """ return dict(), dict() + def get_settings_version(self): + """ + Retrieves the settings format version of the plugin. + + Use this to have OctoPrint trigger your migration function if it detects an outdated settings version in + config.yaml. + + Returns: + int or None: an int signifying the current settings format, should be incremented by plugins whenever there + are backwards incompatible changes. Returning None here disables the version tracking for the + plugin's configuration. + """ + return None + + def on_settings_migrate(self, target, current): + """ + Called by OctoPrint if it detects that the installed version of the plugin necessitates a higher settings version + than the one currently stored in _config.yaml. Will also be called if the settings data stored in config.yaml + doesn't have version information, in which case the ``current`` parameter will be None. + + Your plugin's implementation should take care of migrating any data by utilizing self._settings. OctoPrint + will take care of saving any changes to disk by calling `self._settings.save()` after returning from this method. + + This method will be called before your plugin's :func:`initialize` method, but with all injections already + having taken place. You can therefore depend on the configuration having been migrated by the time :func:`initialize` + is called. + + Arguments: + target (int): The settings format version the plugin requires, this should always be the same value as + returned by :func:`get_settings_version`. + current (int or None): The settings format version as currently stored in config.yaml. May be None if + no version information can be found. + """ + pass + class EventHandlerPlugin(OctoPrintPlugin): """ diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 26687855..d94ba4a4 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -210,7 +210,22 @@ class Server(): set_preprocessors=set_preprocessors) return dict(settings=plugin_settings) + def settings_plugin_pre_init(name, implementation): + if not isinstance(implementation, octoprint.plugin.SettingsPlugin): + return + + settings_version = implementation.get_settings_version() + settings_migrator = implementation.on_settings_migrate + + if settings_version is not None and settings_migrator is not None: + stored_version = implementation._settings.get_int(["_config_version"]) + if stored_version is None or stored_version < settings_version: + settings_migrator(settings_version, stored_version) + implementation._settings.set_int(["_config_version"], settings_version) + implementation._settings.save() + pluginManager.implementation_inject_factories=[octoprint_plugin_inject_factory, settings_plugin_inject_factory] + pluginManager.implementation_pre_inits=[settings_plugin_pre_init] pluginManager.initialize_implementations() pluginManager.log_all_plugins() From f1be116190b353eeb20f29a691fd27bd48caa016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 11:12:24 +0200 Subject: [PATCH 12/18] SoftwareUpdate: Don't persist more check data than necessary in config The SoftwareUpdate Plugin had a bug that caused way too much check data to be stored in the configuration, leading to plugins potentially being stuck in an "update available" loop although the update had already been applied. Now only the current version of github_commit update types is persisted, not the full check configuration. Also introduced a configuration version and made the migration function migrate old configs to remove anything that was same as the default supplied for the "octoprint" and all plugin hook checks. That should clean things up in existing installations. --- .../plugins/softwareupdate/__init__.py | 78 +++++++++++++++++-- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 76264435..f92c70ee 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -91,6 +91,69 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, super(SoftwareUpdatePlugin, self).on_settings_save(data) self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60 + def get_settings_version(self): + return 1 + + def on_settings_migrate(self, target, current=None): + if current is None: + # there might be some left over data from the time we still persisted everything to settings, + # even the stuff that shouldn't be persisted but always provided by the hook - let's + # clean up + + # take care of the octoprint entry + configured_checks = self._settings.get(["checks"], merged=True) + octoprint_check = dict(configured_checks["octoprint"]) + if "type" in octoprint_check and not octoprint_check["type"] == "github_commit": + deletables=["current"] + else: + deletables=[] + octoprint_check = self._clean_settings_check("octoprint", octoprint_check, self.get_settings_defaults()["checks"]["octoprint"], delete=deletables, save=False) + configured_checks["octoprint"] = octoprint_check + + # and the hooks + update_check_hooks = self._plugin_manager.get_hooks("octoprint.plugin.softwareupdate.check_config") + for name, hook in update_check_hooks.items(): + try: + hook_checks = hook() + except: + self._logger.exception("Error while retrieving update information from plugin {name}".format(**locals())) + else: + for key, data in hook_checks.items(): + if key in configured_checks: + settings_check = dict(configured_checks[key]) + merged = dict_merge(data, settings_check) + if "type" in merged and not merged["type"] == "github_commit": + deletables = ["current", "displayVersion"] + else: + deletables = [] + + self._clean_settings_check(key, settings_check, data, delete=deletables, save=False) + + def _clean_settings_check(self, key, data, defaults, delete=None, save=True): + if delete is None: + delete = [] + + for k, v in data.items(): + if k in defaults and defaults[k] == data[k]: + del data[k] + + for k in delete: + if k in data: + del data[k] + + dummy_defaults = dict(plugins=dict()) + dummy_defaults["plugins"][self._identifier] = dict(checks=dict()) + dummy_defaults["plugins"][self._identifier]["checks"][key] = defaults + if len(data): + self._settings.set(["checks", key], data, defaults=dummy_defaults) + else: + self._settings.set(["checks", key], None, defaults=dummy_defaults) + + if save: + self._settings.save() + + return data + #~~ BluePrint API @octoprint.plugin.BlueprintPlugin.route("/check", methods=["GET"]) @@ -380,15 +443,14 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, # persist the new version if necessary for check type if check["type"] == "github_commit": - checks = self._settings.get(["checks"], merged=True) - if target in checks: - # TODO make this cleaner, right now it saves too much to disk - checks[target]["current"] = target_version - self._settings.set(["checks"], checks) + dummy_default = dict(plugins=dict()) + dummy_default["plugins"][self._identifier] = dict(checks=dict()) + dummy_default["plugins"][self._identifier]["checks"][target] = dict(current=None) + self._settings.set(["checks", target, "current"], target_version, defaults=dummy_default) - # we have to save here (even though that makes us save quite often) since otherwise the next - # load will overwrite our changes we just made - self._settings.save() + # we have to save here (even though that makes us save quite often) since otherwise the next + # load will overwrite our changes we just made + self._settings.save() return target_error, target_result From 0846a0fed0718c928aa9c76814d89feba3d318ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 11:24:37 +0200 Subject: [PATCH 13/18] Fix: Wrong parameter order --- src/octoprint/settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 6c6dd06b..a024338b 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -931,7 +931,7 @@ class Settings(object): #~~ setter - def set(self, path, value, config=None, force=False, defaults=None, preprocessors=None): + def set(self, path, value, force=False, defaults=None, config=None, preprocessors=None): if len(path) == 0: return @@ -975,7 +975,7 @@ class Settings(object): config[key] = value self._dirty = True - def setInt(self, path, value, config=None, force=False, defaults=None, preprocessors=None): + def setInt(self, path, value, force=False, defaults=None, config=None, preprocessors=None): if value is None: self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors) return @@ -988,7 +988,7 @@ class Settings(object): self.set(path, intValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors) - def setFloat(self, path, value, config=None, force=False, defaults=None, preprocessors=None): + def setFloat(self, path, value, force=False, defaults=None, config=None, preprocessors=None): if value is None: self.set(path, None, config=config, force=force, defaults=defaults, preprocessors=preprocessors) return @@ -1001,7 +1001,7 @@ class Settings(object): self.set(path, floatValue, config=config, force=force, defaults=defaults, preprocessors=preprocessors) - def setBoolean(self, path, value, config=None, force=False, defaults=None, preprocessors=None): + def setBoolean(self, path, value, force=False, defaults=None, config=None, preprocessors=None): if value is None or isinstance(value, bool): self.set(path, value, config=config, force=force, defaults=defaults, preprocessors=preprocessors) elif value.lower() in valid_boolean_trues: From 633d1ae594e29cdc8c415e139b774e7922d7852a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 13:17:47 +0200 Subject: [PATCH 14/18] Fix: capture TypeError caused by dynamic reloading on settings save and load Usage of super(MyClass, self) is henceforth discouraged due to dynamic reloading within OctoPrint's plugin subsystem. Refer to https://thingspython.wordpress.com/2010/09/27/another-super-wrinkle-raising-typeerror/ for details on this problem. Action by plugin authors is needed to remove the super(...) call and substitute it with octoprint.plugin.SettingsPlugin.on_settings_save(self, data) and octoprint.plugin.SettingsPlugin.on_settings_load(self). Without these modifications, calls to plugin methods utilizing super(...) will fail after a call to the plugin managers reload_plugins method. Closes #942 --- src/octoprint/plugin/types.py | 2 +- src/octoprint/server/api/settings.py | 31 ++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index caabeed4..d285173c 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -718,7 +718,7 @@ class SettingsPlugin(OctoPrintPlugin): def on_settings_save(self, data): old_flag = self._settings.get_boolean(["sub", "some_flag"]) - super(MySettingsPlugin, self).on_settings_save(data) + octoprint.plugin.SettingsPlugin.on_settings_save(self, data) new_flag = self._settings.get_boolean(["sub", "some_flag"]) if old_flag != new_flag: diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 7bba8a02..187ff460 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -26,6 +26,8 @@ import octoprint.util @api.route("/settings", methods=["GET"]) def getSettings(): + logger = logging.getLogger(__name__) + s = settings() connectionOptions = get_connection_options() @@ -118,7 +120,7 @@ def getSettings(): for name in gcode_scripts: data["scripts"]["gcode"][name] = s.loadScript("gcode", name, source=True) - def process_plugin_result(name, plugin, result): + def process_plugin_result(name, result): if result: if not "plugins" in data: data["plugins"] = dict() @@ -126,9 +128,17 @@ def getSettings(): del result["__enabled"] data["plugins"][name] = result - octoprint.plugin.call_plugin(octoprint.plugin.SettingsPlugin, - "on_settings_load", - callback=process_plugin_result) + for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin): + try: + result = plugin.on_settings_load() + process_plugin_result(plugin._identifier, result) + except TypeError: + logger.warn("Could not load settings for plugin {name} ({version}) since it called super(...)".format(name=plugin._plugin_name, version=plugin._plugin_version)) + logger.warn("in a way which has issues due to OctoPrint's dynamic reloading after plugin operations.") + logger.warn("Please contact the plugin's author and ask to update the plugin to use a direct call like") + logger.warn("octoprint.plugin.SettingsPlugin.on_settings_load(self) instead.") + except: + logger.exception("Could not load settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name)) return jsonify(data) @@ -137,6 +147,8 @@ def getSettings(): @restricted_access @admin_permission.require(403) def setSettings(): + logger = logging.getLogger(__name__) + if not "application/json" in request.headers["Content-Type"]: return make_response("Expected content-type JSON", 400) @@ -235,8 +247,15 @@ def setSettings(): for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin): plugin_id = plugin._identifier if plugin_id in data["plugins"]: - plugin.on_settings_save(data["plugins"][plugin_id]) - + try: + plugin.on_settings_save(data["plugins"][plugin_id]) + except TypeError: + logger.warn("Could not save settings for plugin {name} ({version}) since it called super(...)".format(name=plugin._plugin_name, version=plugin._plugin_version)) + logger.warn("in a way which has issues due to OctoPrint's dynamic reloading after plugin operations.") + logger.warn("Please contact the plugin's author and ask to update the plugin to use a direct call like") + logger.warn("octoprint.plugin.SettingsPlugin.on_settings_save(self, data) instead.") + except: + logger.exception("Could not save settings for plugin {name} ({version})".format(version=plugin._plugin_version, name=plugin._plugin_name)) if s.save(): eventManager().fire(Events.SETTINGS_UPDATED) From dedf3f234fba9851cb40cab8e16bbd6cd20755c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 13:19:54 +0200 Subject: [PATCH 15/18] super(...).on_settings_save(data) => octoprint.plugin.SettingsPlugin.on_settings_save(self, data) --- src/octoprint/plugins/cura/__init__.py | 4 ++-- src/octoprint/plugins/softwareupdate/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index b1d0087d..e1ce090c 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -129,7 +129,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, def on_settings_save(self, data): old_debug_logging = self._settings.get_boolean(["debug_logging"]) - super(CuraPlugin, self).on_settings_save(data) + octoprint.plugin.SettingsPlugin.on_settings_save(self, data) new_debug_logging = self._settings.get_boolean(["debug_logging"]) if old_debug_logging != new_debug_logging: @@ -410,4 +410,4 @@ __plugin_author__ = "Gina Häußge" __plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura" __plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint" __plugin_license__ = "AGPLv3" -__plugin_implementation__ = CuraPlugin() \ No newline at end of file +__plugin_implementation__ = CuraPlugin() diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index f92c70ee..0d2c96af 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -88,7 +88,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, } def on_settings_save(self, data): - super(SoftwareUpdatePlugin, self).on_settings_save(data) + octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60 def get_settings_version(self): From f58d63bb4946278f79231bdbfa5dec40493939a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 15:20:25 +0200 Subject: [PATCH 16/18] pluginmanager: Reset flag for repository_available when refreshing it --- src/octoprint/plugins/pluginmanager/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 0b0c6a83..e7bde03c 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -133,7 +133,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, result.append(self._to_external_representation(plugin)) if "refresh_repository" in request.values and request.values["refresh_repository"] in valid_boolean_trues: - self._refresh_repository() + self._repository_available = self._refresh_repository() return jsonify(plugins=result, repository=dict(available=self._repository_available, plugins=self._repository_plugins), os=self._get_os(), octoprint=self._get_octoprint_version()) From 904a3cdafff6cc84b2a78d0345d8b8dafb21836b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 15:25:37 +0200 Subject: [PATCH 17/18] super(...).method(...) => BaseClass.method(self, ...) --- src/octoprint/plugin/core.py | 8 ++++---- src/octoprint/slicing/exceptions.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 55973c2c..8a505372 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -1215,13 +1215,13 @@ class RestartNeedingPlugin(Plugin): class PluginNeedsRestart(BaseException): def __init__(self, name): - super(BaseException, self).__init__() + BaseException.__init__(self) self.name = name self.message = "Plugin {name} cannot be enabled or disabled after system startup".format(**locals()) class PluginLifecycleException(BaseException): def __init__(self, name, reason, message): - super(BaseException, self).__init__() + BaseException.__init__(self) self.name = name self.reason = reason @@ -1232,7 +1232,7 @@ class PluginLifecycleException(BaseException): class PluginCantInitialize(PluginLifecycleException): def __init__(self, name, reason): - super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be initialized: {reason}") + PluginLifecycleException.__init__(self, name, reason, "Plugin {name} cannot be initialized: {reason}") class PluginCantEnable(PluginLifecycleException): def __init__(self, name, reason): @@ -1240,4 +1240,4 @@ class PluginCantEnable(PluginLifecycleException): class PluginCantDisable(PluginLifecycleException): def __init__(self, name, reason): - super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be disabled: {reason}") + PluginLifecycleException.__init__(self, name, reason, "Plugin {name} cannot be disabled: {reason}") diff --git a/src/octoprint/slicing/exceptions.py b/src/octoprint/slicing/exceptions.py index c726f508..4d789107 100644 --- a/src/octoprint/slicing/exceptions.py +++ b/src/octoprint/slicing/exceptions.py @@ -54,7 +54,7 @@ class SlicerException(SlicingException): Identifier of the slicer for which the exception was raised. """ def __init__(self, slicer, *args, **kwargs): - super(SlicingException, self).__init__(*args, **kwargs) + SlicingException.__init__(self, *args, **kwargs) self.slicer = slicer class SlicerNotConfigured(SlicerException): @@ -62,7 +62,7 @@ class SlicerNotConfigured(SlicerException): Raised if a slicer is not yet configured but must be configured to proceed. """ def __init__(self, slicer, *args, **kwargs): - super(SlicerException, self).__init__(slicer, *args, **kwargs) + SlicerException.__init__(self, slicer, *args, **kwargs) self.message = "Slicer not configured: {slicer}".format(slicer=slicer) class UnknownSlicer(SlicerException): @@ -70,7 +70,7 @@ class UnknownSlicer(SlicerException): Raised if a slicer is unknown. """ def __init__(self, slicer, *args, **kwargs): - super(SlicerException, self).__init__(slicer, *args, **kwargs) + SlicerException.__init__(self, slicer, *args, **kwargs) self.message = "No such slicer: {slicer}".format(slicer=slicer) class ProfileException(BaseException): @@ -86,7 +86,7 @@ class ProfileException(BaseException): Identifier of the profile for which the exception was raised. """ def __init__(self, slicer, profile, *args, **kwargs): - super(BaseException, self).__init__(*args, **kwargs) + BaseException.__init__(self, *args, **kwargs) self.slicer = slicer self.profile = profile @@ -95,7 +95,7 @@ class UnknownProfile(ProfileException): Raised if a slicing profile does not exist but must exist to proceed. """ def __init__(self, slicer, profile, *args, **kwargs): - super(ProfileException, self).__init__(slicer, profile, *args, **kwargs) + ProfileException.__init__(self, slicer, profile, *args, **kwargs) self.message = "Profile {profile} for slicer {slicer} does not exist".format(profile=profile, slicer=slicer) class ProfileAlreadyExists(ProfileException): @@ -103,5 +103,5 @@ class ProfileAlreadyExists(ProfileException): Raised if a slicing profile already exists and must not be overwritten. """ def __init__(self, slicer, profile, *args, **kwargs): - super(ProfileException, self).__init__(slicer, profile, *args, **kwargs) - self.message = "Profile {profile} for slicer {slicer} already exists".format(profile=profile, slicer=slicer) \ No newline at end of file + ProfileException.__init__(self, slicer, profile, *args, **kwargs) + self.message = "Profile {profile} for slicer {slicer} already exists".format(profile=profile, slicer=slicer) From 4abd97df093131d5651717421eec6d5cc95b68e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 19 Jun 2015 19:19:02 +0200 Subject: [PATCH 18/18] File manager will now migrate analysis result from old metadata That should make the initial startup after switching to 1.2.0 way faster. --- src/octoprint/filemanager/__init__.py | 16 ++++- src/octoprint/filemanager/storage.py | 87 +++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index bc8903b7..2fdfeb89 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -136,8 +136,16 @@ class FileManager(object): def initialize(self): self.reload_plugins() - for storage_type, storage_manager in self._storage_managers.items(): - self._determine_analysis_backlog(storage_type, storage_manager) + + def worker(): + self._logger.info("Adding backlog items from all storage types to analysis queue...".format(**locals())) + for storage_type, storage_manager in self._storage_managers.items(): + self._determine_analysis_backlog(storage_type, storage_manager) + + import threading + thread = threading.Thread(target=worker) + thread.daemon = True + thread.start() def reload_plugins(self): self._progress_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.ProgressPlugin) @@ -150,13 +158,15 @@ class FileManager(object): self._slicing_progress_callbacks.remove(callback) def _determine_analysis_backlog(self, storage_type, storage_manager): - self._logger.info("Adding backlog items from {storage_type} to analysis queue".format(**locals())) + counter = 0 for entry, path, printer_profile in storage_manager.analysis_backlog: file_type = get_file_type(path)[-1] # we'll use the default printer profile for the backlog since we don't know better queue_entry = QueueEntry(entry, file_type, storage_type, path, self._printer_profile_manager.get_default()) self._analysis_queue.enqueue(queue_entry, high_priority=False) + counter += 1 + self._logger.info("Added {counter} items from storage type \"{storage_type}\" to analysis queue".format(**locals())) def add_storage(self, storage_type, storage_manager): self._storage_managers[storage_type] = storage_manager diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index bd81df0e..6e855cce 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -309,6 +309,41 @@ class LocalFileStorage(StorageInterface): self._metadata_cache = pylru.lrucache(10) + self._old_metadata = None + self._initialize_metadata() + + def _initialize_metadata(self): + self._logger.info("Initializing the file metadata for {}...".format(self.basefolder)) + + old_metadata_path = os.path.join(self.basefolder, "metadata.yaml") + backup_path = os.path.join(self.basefolder, "metadata.yaml.backup") + + if os.path.exists(old_metadata_path): + # load the old metadata file + try: + with open(old_metadata_path) as f: + import yaml + self._old_metadata = yaml.safe_load(f) + except: + self._logger.exception("Error while loading old metadata file") + + # make sure the metadata is initialized as far as possible + self._list_folder(self.basefolder) + + # rename the old metadata file + self._old_metadata = None + try: + import shutil + shutil.move(old_metadata_path, backup_path) + except: + self._logger.exception("Could not rename old metadata.yaml file") + + else: + # make sure the metadata is initialized as far as possible + self._list_folder(self.basefolder) + + self._logger.info("... file metadata for {} initialized successfully.".format(self.basefolder)) + @property def analysis_backlog(self): for entry in self._analysis_backlog_generator(): @@ -919,12 +954,7 @@ class LocalFileStorage(StorageInterface): if entry in metadata and isinstance(metadata[entry], dict): entry_data = metadata[entry] else: - entry_data = dict( - hash=self._create_hash(entry_path), - links=[], - notes=[] - ) - metadata[entry] = entry_data + entry_data = self._add_basic_metadata(path, entry, save=False, metadata=metadata) metadata_dirty = True # TODO extract model hash from source if possible to recreate link @@ -959,6 +989,31 @@ class LocalFileStorage(StorageInterface): return result + def _add_basic_metadata(self, path, entry, additional_metadata=None, save=True, metadata=None): + if additional_metadata is None: + additional_metadata = dict() + + if metadata is None: + metadata = self._get_metadata(path) + + entry_data = dict( + hash=self._create_hash(os.path.join(path, entry)), + links=[], + notes=[] + ) + + if path == self.basefolder and self._old_metadata is not None and entry in self._old_metadata and "gcodeAnalysis" in self._old_metadata[entry]: + # if there is still old metadata available and that contains an analysis for this file, use it! + entry_data["analysis"] = self._old_metadata[entry]["gcodeAnalysis"] + + entry_data.update(additional_metadata) + metadata[entry] = entry_data + + if save: + self._save_metadata(path, metadata) + + return entry_data + def _create_hash(self, path): import hashlib @@ -993,16 +1048,22 @@ class LocalFileStorage(StorageInterface): def _save_metadata(self, path, metadata): metadata_path = os.path.join(path, ".metadata.yaml") - fh, metadata_temporary_path = tempfile.mkstemp() - os.close(fh) - with self._metadata_lock: try: - with open(metadata_temporary_path, "w") as f: - import yaml - yaml.safe_dump(metadata, stream=f, default_flow_style=False, indent=" ", allow_unicode=True) + import yaml import shutil - shutil.move(metadata_temporary_path, metadata_path) + + file_obj = tempfile.NamedTemporaryFile(delete=False) + try: + yaml.safe_dump(metadata, stream=file_obj, default_flow_style=False, indent=" ", allow_unicode=True) + file_obj.close() + shutil.move(file_obj.name, metadata_path) + finally: + try: + if os.path.exists(file_obj.name): + os.remove(file_obj.name) + except Exception as e: + self._logger.warn("Could not delete file {}: {}".format(file_obj.name, str(e))) except: self._logger.exception("Error while writing .metadata.yaml to {path}".format(**locals())) else:
-  |  +  |