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
This commit is contained in:
Gina Häußge 2015-05-11 15:47:40 +02:00
parent 797a5c8a5c
commit 4c7520efb9
5 changed files with 109 additions and 14 deletions

View file

@ -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
: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 <https://github.com/OctoPrint/Plugin-Examples/blob/master/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

View file

@ -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

View file

@ -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,

View file

@ -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": {

View file

@ -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