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