From 4c7520efb9e6dac1c18e937f1e4359b00e69f81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 11 May 2015 15:47:40 +0200 Subject: [PATCH] New hook "octoprint.server.http.bodysize" and lifecycle support for restart needing hooks The new hook allows extending the list of rules for maximum body sizes differing from the default of 100KB and can be used by plugins to allow uploads to them that exceed that file size. Also extended the plugin manager to detect plugins that implement restart needing hooks (such as the above one) and handling those plugins the same as plugins containing implementations that inherit from octoprint.plugin.core.RestartNeedingPlugin --- docs/plugins/hooks.rst | 37 ++++++++++++++++++++++++- src/octoprint/plugin/__init__.py | 12 ++++++-- src/octoprint/plugin/core.py | 41 ++++++++++++++++++++++++---- src/octoprint/server/__init__.py | 25 ++++++++++++++++- src/octoprint/server/util/tornado.py | 8 +++--- 5 files changed, 109 insertions(+), 14 deletions(-) diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index 465cf6d3..e9a13629 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -263,4 +263,39 @@ octoprint.filemanager.preprocessor :param dict printer_profile: The printer profile associated with the file. :param boolean allow_overwrite: Whether to allow overwriting an existing file named the same or not. :return: The `file_object` as passed in or None, or a replaced version to use instead for further processing. - :rtype: AbstractFileWrapper or None \ No newline at end of file + :rtype: AbstractFileWrapper or None + +.. _sec-plugins-hook-server-http-bodysize: + +octoprint.server.http.bodysize +------------------------------ + +.. py:function:: hook(current_max_body_sizes, *args, **kwargs) + + Allows extending the list of custom maximum body sizes on the web server per path and HTTP method with custom entries + from plugins. + + Your plugin might need this if you want to allow uploading files larger than 100KB (the default maximum upload size + for anything but the ``/api/files`` endpoint). + + ``current_max_body_sizes`` will be a (read-only) list of the currently configured maximum body sizes, in case you + want to check from your plugin if you need to even add a new entry. + + The hook must return a list of 3-tuples (the list's length can be 0). Each 3-tuple should have the HTTP method + against which to match as first, a regular expression for the path to match against and the maximum body size as + an integer as the third entry. + + **Example** + + The following plugin example sets the maximum body size for ``POST`` requests against four custom URLs to 100, 200, + 500 and 1024KB. To test its functionality try uploading files larger or smaller than an endpoint's configured maximum + size (as multipart request with the file upload residing in request parameter ``file``) and observe the behaviour. + + .. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/increase_bodysize.py + :linenos: + :tab-width: 4 + :caption: `increase_bodysize.py `_ + + :param list current_max_body_sizes: read-only list of the currently configured maximum body sizes + :return: A list of 3-tuples with additional request specific maximum body sizes as defined above + :rtype: list \ No newline at end of file diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 5813aac3..109ab491 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -31,7 +31,8 @@ from octoprint.util import deprecated # singleton _instance = None -def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None): +def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None, + plugin_restart_needing_hooks=None): """ Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager` singleton. @@ -51,6 +52,9 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en this defaults to the entry point ``octoprint.plugin``. plugin_disabled_list (list): A list of plugin identifiers that are currently disabled. If not provided this defaults to all plugins for which ``enabled`` is set to ``False`` in the settings. + plugin_restart_needing_hooks (list): A list of hook namespaces which cause a plugin to need a restart in order + be enabled/disabled. Does not have to contain full hook identifiers, will be matched with startswith similar + to logging handlers Returns: PluginManager: A fully initialized :class:`~octoprint.plugin.core.PluginManager` instance to be used for plugin @@ -89,8 +93,12 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en plugin_entry_points = "octoprint.plugin" if plugin_disabled_list is None: plugin_disabled_list = settings().get(["plugins", "_disabled"]) + if plugin_restart_needing_hooks is None: + plugin_restart_needing_hooks = [ + "octoprint.server.http" + ] - _instance = PluginManager(plugin_folders, plugin_types, plugin_entry_points, logging_prefix="octoprint.plugins.", plugin_disabled_list=plugin_disabled_list) + _instance = PluginManager(plugin_folders, plugin_types, plugin_entry_points, logging_prefix="octoprint.plugins.", plugin_disabled_list=plugin_disabled_list, plugin_restart_needing_hooks=plugin_restart_needing_hooks) else: raise ValueError("Plugin Manager not initialized yet") return _instance diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 26d4ca41..3ddb2733 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -419,7 +419,7 @@ 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): + def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None, plugin_restart_needing_hooks=None): self.logger = logging.getLogger(__name__) if logging_prefix is None: @@ -431,6 +431,7 @@ class PluginManager(object): self.plugin_types = plugin_types self.plugin_entry_points = plugin_entry_points self.plugin_disabled_list = plugin_disabled_list + self.plugin_restart_needing_hooks = plugin_restart_needing_hooks self.logging_prefix = logging_prefix self.enabled_plugins = dict() @@ -675,7 +676,7 @@ class PluginManager(object): if plugin is None: plugin = self.disabled_plugins[name] - if not startup and plugin.implementation and isinstance(plugin.implementation, RestartNeedingPlugin): + if not startup and self.is_restart_needing_plugin(plugin): raise PluginNeedsRestart(name) try: @@ -711,7 +712,7 @@ class PluginManager(object): if plugin is None: plugin = self.enabled_plugins[name] - if plugin.implementation and isinstance(plugin.implementation, RestartNeedingPlugin): + if self.is_restart_needing_plugin(plugin): raise PluginNeedsRestart(name) try: @@ -737,15 +738,14 @@ class PluginManager(object): return True def _activate_plugin(self, name, plugin): + plugin.hotchangeable = self.is_restart_needing_plugin(plugin) + # evaluate registered hooks for hook, callback in plugin.hooks.items(): self.plugin_hooks[hook].append((name, callback)) # evaluate registered implementation if plugin.implementation: - if isinstance(plugin.implementation, RestartNeedingPlugin): - plugin.hotchangeable = False - for plugin_type in self.plugin_types: if isinstance(plugin.implementation, plugin_type): self.plugin_implementations_by_type[plugin_type].append((name, plugin.implementation)) @@ -771,6 +771,35 @@ class PluginManager(object): # that's ok, the plugin was just not registered for the type pass + def is_restart_needing_plugin(self, plugin): + return self.has_restart_needing_implementation(plugin) or self.has_restart_needing_hooks(plugin) + + def has_restart_needing_implementation(self, plugin): + if not plugin.implementation: + return False + + return isinstance(plugin.implementation, RestartNeedingPlugin) + + def has_restart_needing_hooks(self, plugin): + if not plugin.hooks: + return False + + hooks = plugin.hooks.keys() + for hook in hooks: + if self.is_restart_needing_hook(hook): + return True + return False + + def is_restart_needing_hook(self, hook): + if self.plugin_restart_needing_hooks is None: + return False + + for h in self.plugin_restart_needing_hooks: + if hook.startswith(h): + return True + + return False + def initialize_implementations(self, additional_injects=None, additional_inject_factories=None): for name, plugin in self.enabled_plugins.items(): self.initialize_implementation_of_plugin(name, plugin, diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index a3409eea..3f0d9d44 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -721,6 +721,26 @@ class Server(): max_body_sizes = [ ("POST", r"/api/files/([^/]*)", settings().getInt(["server", "uploads", "maxSize"])) ] + + # allow plugins to extend allowed maximum body sizes + for name, hook in pluginManager.get_hooks("octoprint.server.http.bodysize").items(): + try: + result = hook(list(max_body_sizes)) + except: + self._logger.exception("There was an error while retrieving additional upload sizes from plugin hook {name}".format(**locals())) + else: + if isinstance(result, (list, tuple)): + for entry in result: + if not isinstance(entry, tuple) or not len(entry) == 3: + continue + if not entry[0] in util.tornado.UploadStorageFallbackHandler.BODY_METHODS: + continue + if not isinstance(entry[2], int): + continue + + self._logger.debug("Adding maximum body size of {size}B for {method} requests to {path} (Plugin: {name})".format(method=entry[0], path=entry[1], size=entry[2], name=name)) + max_body_sizes.append(entry) + self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=settings().getInt(["server", "maxSize"])) self._server.listen(self._port, address=self._host) @@ -836,7 +856,10 @@ class Server(): "propagate": False }, "tornado.application": { - "level": "ERROR" + "level": "INFO" + }, + "tornado.general": { + "level": "INFO" } }, "root": { diff --git a/src/octoprint/server/util/tornado.py b/src/octoprint/server/util/tornado.py index 399dd4d7..348511bc 100644 --- a/src/octoprint/server/util/tornado.py +++ b/src/octoprint/server/util/tornado.py @@ -652,7 +652,7 @@ class CustomHTTP1Connection(tornado.http1connection.HTTP1Connection): tornado.http1connection.HTTP1Connection.__init__(self, stream, is_client, params=params, context=context) import re - self._max_body_sizes = map(lambda x: (x[0], re.compile(x[1]), x[2]), self.params.max_body_sizes or dict()) + self._max_body_sizes = map(lambda x: (x[0], re.compile(x[1]), x[2]), self.params.max_body_sizes or list()) self._default_max_body_size = self.params.default_max_body_size or self.stream.max_buffer_size def _read_body(self, code, headers, delegate): @@ -680,7 +680,7 @@ class CustomHTTP1Connection(tornado.http1connection.HTTP1Connection): content_length = int(content_length) max_content_length = self._get_max_content_length(self._request_start_line.method, self._request_start_line.path) - if 0 <= max_content_length < content_length: + if max_content_length is not None and 0 <= max_content_length < content_length: raise tornado.httputil.HTTPInputError("Content-Length too long") else: content_length = None @@ -731,8 +731,8 @@ class CustomHTTP1ConnectionParameters(tornado.http1connection.HTTP1ConnectionPar def __init__(self, *args, **kwargs): tornado.http1connection.HTTP1ConnectionParameters.__init__(self, args, kwargs) - self.max_body_sizes = kwargs["max_body_sizes"] if "max_body_sizes" in kwargs else dict() - self.default_max_body_size = kwargs["default_max_body_size"] if "default_max_body_size" in kwargs else dict() + self.max_body_sizes = kwargs["max_body_sizes"] if "max_body_sizes" in kwargs else list() + self.default_max_body_size = kwargs["default_max_body_size"] if "default_max_body_size" in kwargs else None #~~ customized large response handler