Permalink
Comparing changes
Open a pull request
1
contributor
Commits on Sep 11, 2015
Sessions could get duplicated, wrongly saved etc. The reason was not persisting the actual user object to the internal session map (but the LocalProxy instead). That could lead to multiple sessions being created for one login, or the session user being set to an anonymous user, or various other odd effects depending on timing. (cherry picked from commit 8aeac51)
Commits on Sep 21, 2015
There was a problem with software update checks configurations stored in config.yaml for which the providing plugin was then removed, since those check definitions then lacked their default values to be merged on whatever was stored in config.yaml, causing incomplete check configurations as a consequence over which the plugin tripped. This patch fixes that in that it tracks which check config keys are provided by plugins and only returns those as the active check configurations that belong to plugins that are still in the system. TODO: This is only half of the solution. Check configurations of plugins that are being uninstalled should be removed from the config if the user decides to remove any settings by the plugin too. We need some adjustments in the lifecycle tracking in order to make this possible however, so for now this must suffice to at least prevent any errors from occuring when incomplete configs are encountered. (cherry picked from commit 8af8b8f)
Commits on Sep 28, 2015
Commits on Oct 02, 2015
Commits on Oct 05, 2015
Commits on Oct 06, 2015
ETag values returned with download responses were cached internally within the LargeResponseHandler class, with the file's path as key. That let to problems once the content changed and hence the ETag value actually becoming invalid. Since the path however stayed the same, the same ETag value for the modified file was assumed and a 304 Not Modified response was generated. This patch changes the behaviour of LargeResponseHandler to use the last modified date of the file as the ETag value and alternatively allowing an etag generator function to be provided as constructor parameter as well to use for calculating (or disabling) the ETag header dependant on the situation.
Commits on Oct 07, 2015
Commits on Oct 08, 2015
Unified
Split
Showing
with
1,111 additions
and 272 deletions.
- +3 −3 .versioneer-lookup
- +56 −0 CHANGELOG.md
- +15 −1 README.md
- +1 −1 docs/api/logs.rst
- +2 −2 setup.py
- +3 −0 src/octoprint/events.py
- +44 −3 src/octoprint/filemanager/__init__.py
- +3 −1 src/octoprint/filemanager/util.py
- +8 −10 src/octoprint/plugin/core.py
- +1 −1 src/octoprint/plugins/cura/__init__.py
- +31 −4 src/octoprint/plugins/pluginmanager/__init__.py
- +90 −5 src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js
- +74 −0 src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2
- +21 −10 src/octoprint/plugins/softwareupdate/__init__.py
- +5 −11 src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js
- +1 −1 src/octoprint/printer/standard.py
- +28 −7 src/octoprint/server/__init__.py
- +5 −5 src/octoprint/server/api/timelapse.py
- +6 −3 src/octoprint/server/util/flask.py
- +33 −4 src/octoprint/server/util/tornado.py
- +9 −0 src/octoprint/server/views.py
- +3 −1 src/octoprint/settings.py
- +26 −1 src/octoprint/static/js/app/dataupdater.js
- +9 −0 src/octoprint/static/js/app/main.js
- +1 −1 src/octoprint/templates/dialogs/settings.jinja2
- +1 −1 src/octoprint/templates/dialogs/usersettings.jinja2
- +3 −3 src/octoprint/templates/index.jinja2
- +8 −8 src/octoprint/templates/initscript.jinja2
- +2 −1 src/octoprint/templates/tabs/timelapse.jinja2
- +204 −132 src/octoprint/timelapse.py
- +38 −28 src/octoprint/users.py
- +51 −4 src/octoprint/util/__init__.py
- +7 −6 src/octoprint/util/comm.py
- +3 −1 src/octoprint/util/gcodeInterpreter.py
- +40 −8 src/octoprint/util/pip.py
- +0 −1 tests/filemanager/__init__.py
- +96 −4 tests/filemanager/test_filemanager.py
- +180 −0 tests/util/test_repeated_timer.py
| @@ -10,10 +10,10 @@ | ||
| # master shall not use the lookup table, only tags | ||
| master | ||
|
|
||
| # maintenance is currently the branch for preparation of maintenance release 1.2.6 | ||
| # maintenance is currently the branch for preparation of maintenance release 1.2.7 | ||
| # so are any fix/... branches | ||
| maintenance 1.2.6-dev 96fc70bdb2dd74ba04c3071f70da385b0408904a | ||
| fix/.* 1.2.6-dev 96fc70bdb2dd74ba04c3071f70da385b0408904a | ||
| maintenance 1.2.7-dev 536bb31965db17b969e7c1c53e241ddac4ae1814 | ||
| fix/.* 1.2.7-dev 536bb31965db17b969e7c1c53e241ddac4ae1814 | ||
|
|
||
| # Special case disconnected checkouts, e.g. 'git checkout <tag>' | ||
| \(detached.* | ||
| @@ -1,5 +1,61 @@ | ||
| # OctoPrint Changelog | ||
|
|
||
| ## 1.2.7 (2015-10-20) | ||
|
|
||
| ### Improvements | ||
|
|
||
| * [#1062](https://github.com/foosel/OctoPrint/issues/1062) - Plugin Manager | ||
| now has a configuration dialog that among other things allows defining the | ||
| used `pip` command if auto detection proves to be insufficient here. | ||
| * Allow defining additional `pip` parameters in Plugin Manager. That might | ||
| make `sudo`-less installation of plugins possible in situations where it's | ||
| tricky otherwise. | ||
| * Improved timelapse processing (backported from `devel` branch): | ||
| * Individually captured frames cannot "overtake" each other anymore through | ||
| usage of a capture queue. | ||
| * Notifications will now be shown when the capturing of the timelapse's | ||
| post roll happens, including an approximation of how long that will take. | ||
| * Usage of `requests` instead of `urllib` for fetching the snapshot, | ||
| appears to also have [positive effects on webcam compatibility](https://github.com/foosel/OctoPrint/issues/1078). | ||
| * Some more defensive escaping for various settings in the UI (e.g. webcam URL) | ||
| * Switch to more error resilient saving of configuration files and other files | ||
| modified during runtime (save to temporary file & move). Should reduce risk | ||
| of file corruption. | ||
| * Downloading GCODE and STL files should now set more fitting `Content-Type` | ||
| headers (`text/plain` and `application/sla`) for better client side support | ||
| for "Open with" like usage scenarios. | ||
| * Selecting z-triggered timelapse mode will now inform about that not working | ||
| when printing from SD card. | ||
| * Software Update Plugin: Removed "The web interface will now be reloaded" | ||
| notification after successful update since that became obsolete with | ||
| introduction of the "Reload Now" overlay. | ||
| * Updated required version of `psutil` and `netifaces` dependencies. | ||
|
|
||
| ### Bug Fixes | ||
|
|
||
| * [#1057](https://github.com/foosel/OctoPrint/issues/1057) - Better error | ||
| resilience of the Software Update plugin against broken/incomplete update | ||
| configurations. | ||
| * [#1075](https://github.com/foosel/OctoPrint/issues/1075) - Fixed support | ||
| of `sudo` for installing plugins, but added big visible warning about it | ||
| as it's **not** recommended. | ||
| * [#1077](https://github.com/foosel/OctoPrint/issues/1077) - Do not hiccup | ||
| on [UTF-8 BOMs](https://en.wikipedia.org/wiki/Byte_order_mark) (or other | ||
| BOMs for that matter) at the beginning of GCODE files. | ||
| * Fixed an issue that caused user sessions to not be properly associated, | ||
| leading to Sessions getting duplicated, wrongly saved etc. | ||
| * Fixed internal server error (HTTP 500) response on REST API calls with | ||
| unset `Content-Type` header. | ||
| * Fixed an issue leading to drag-and-drop file uploads to trigger frontend | ||
| processing in various other file upload widgets. | ||
| * Fixed a documentation error. | ||
| * Fixed caching behaviour on GCODE/STL downloads, was setting the `ETag` | ||
| header improperly. | ||
| * Fixed GCODE viewer not properly detecting change of currently visualized | ||
| file on Windows systems. | ||
|
|
||
| ([Commits](https://github.com/foosel/OctoPrint/compare/1.2.6...1.2.7)) | ||
|
|
||
| ## 1.2.6 (2015-09-02) | ||
|
|
||
| ### Improvements | ||
| @@ -38,11 +38,25 @@ For information about how to go about contributions of any kind, please see the | ||
| Installation | ||
| ------------ | ||
|
|
||
| Installation instructions for installing from source for different operating systems can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki#assorted-guides). | ||
| Installation instructions for installing from source for different operating | ||
| systems can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki#assorted-guides). | ||
|
|
||
| If you want to run OctoPrint on a Raspberry Pi you might want to take a look at [OctoPi](https://github.com/guysoft/OctoPi) | ||
| which is a custom SD card image that includes OctoPrint plus dependencies. | ||
|
|
||
| The generic steps that should basically be done regardless of operating system | ||
| and runtime environment are the following (as *regular | ||
| user*, please keep your hands *off* of the `sudo` command here!) - this assumes | ||
| you already have Python 2.7, pip and virtualenv set up: | ||
|
|
||
| 1. Checkout OctoPrint: `git clone https://github.com/foosel/OctoPrint.git` | ||
| 2. Change into the OctoPrint folder: `cd OctoPrint` | ||
| 3. Create a user-owned virtual environment therein: `virtualenv --system-site-packages venv` | ||
| 4. Install OctoPrint *into that virtual environment*: `./venv/bin/python setup.py install` | ||
|
|
||
| You may then start the OctoPrint server via `/path/to/OctoPrint/venv/bin/octoprint`, see [Usage](#usage) | ||
| for details. | ||
|
|
||
| After installation, please make sure you follow the first-run wizard and set up | ||
| access control as necessary. If you want to not only be notified about new | ||
| releases but also be able to automatically upgrade to them from within | ||
| @@ -15,7 +15,7 @@ Log file management | ||
| Retrieve a list of available log files | ||
| ====================================== | ||
|
|
||
| .. http:post:: /api/logs | ||
| .. http:get:: /api/logs | ||
| Retrieve information regarding all log files currently available and regarding the disk space still available | ||
| in the system on the location the log files are being stored. | ||
| @@ -26,13 +26,13 @@ | ||
| "netaddr==0.7.17", | ||
| "watchdog==0.8.3", | ||
| "sarge==0.1.4", | ||
| "netifaces==0.8", | ||
| "netifaces==0.10", | ||
| "pylru==1.0.9", | ||
| "rsa==3.2", | ||
| "pkginfo==1.2.1", | ||
| "requests==2.7.0", | ||
| "semantic_version==2.4.2", | ||
| "psutil==3.1.1" | ||
| "psutil==3.2.1" | ||
| ] | ||
|
|
||
| # Additional requirements for optional install options | ||
| @@ -74,6 +74,9 @@ class Events(object): | ||
| # Timelapse | ||
| CAPTURE_START = "CaptureStart" | ||
| CAPTURE_DONE = "CaptureDone" | ||
| CAPTURE_FAILED = "CaptureFailed" | ||
| POSTROLL_START = "PostRollStart" | ||
| POSTROLL_END = "PostRollEnd" | ||
| MOVIE_RENDERING = "MovieRendering" | ||
| MOVIE_DONE = "MovieDone" | ||
| MOVIE_FAILED = "MovieFailed" | ||
| @@ -18,18 +18,23 @@ | ||
| from .storage import LocalFileStorage | ||
| from .util import AbstractFileWrapper, StreamWrapper, DiskFileWrapper | ||
|
|
||
| from collections import namedtuple | ||
|
|
||
| ContentTypeMapping = namedtuple("ContentTypeMapping", "extensions, content_type") | ||
| ContentTypeDetector = namedtuple("ContentTypeDetector", "extensions, detector") | ||
|
|
||
| extensions = dict( | ||
| ) | ||
|
|
||
| def full_extension_tree(): | ||
| result = dict( | ||
| # extensions for 3d model files | ||
| model=dict( | ||
| stl=["stl"] | ||
| stl=ContentTypeMapping(["stl"], "application/sla") | ||
| ), | ||
| # extensions for printable machine code | ||
| machinecode=dict( | ||
| gcode=["gcode", "gco", "g"] | ||
| gcode=ContentTypeMapping(["gcode", "gco", "g"], "text/plain") | ||
| ) | ||
| ) | ||
|
|
||
| @@ -68,8 +73,12 @@ def get_all_extensions(subtree=None): | ||
| for key, value in subtree.items(): | ||
| if isinstance(value, dict): | ||
| result += get_all_extensions(value) | ||
| elif isinstance(value, (ContentTypeMapping, ContentTypeDetector)): | ||
| result += value.extensions | ||
| elif isinstance(value, (list, tuple)): | ||
| result += value | ||
| elif isinstance(subtree, (ContentTypeMapping, ContentTypeDetector)): | ||
| result = subtree.extensions | ||
| elif isinstance(subtree, (list, tuple)): | ||
| result = subtree | ||
| return result | ||
| @@ -79,7 +88,9 @@ def get_path_for_extension(extension, subtree=None): | ||
| subtree = full_extension_tree() | ||
|
|
||
| for key, value in subtree.items(): | ||
| if isinstance(value, (list, tuple)) and extension in value: | ||
| if isinstance(value, (ContentTypeMapping, ContentTypeDetector)) and extension in value.extensions: | ||
| return [key] | ||
| elif isinstance(value, (list, tuple)) and extension in value: | ||
| return [key] | ||
| elif isinstance(value, dict): | ||
| path = get_path_for_extension(extension, subtree=value) | ||
| @@ -88,6 +99,23 @@ def get_path_for_extension(extension, subtree=None): | ||
|
|
||
| return None | ||
|
|
||
| def get_content_type_mapping_for_extension(extension, subtree=None): | ||
| if not subtree: | ||
| subtree = full_extension_tree() | ||
|
|
||
| for key, value in subtree.items(): | ||
| content_extension_matches = isinstance(value, (ContentTypeMapping, ContentTypeDetector)) and extension in value. extensions | ||
| list_extension_matches = isinstance(value, (list, tuple)) and extension in value | ||
|
|
||
| if content_extension_matches or list_extension_matches: | ||
| return value | ||
| elif isinstance(value, dict): | ||
| result = get_content_type_mapping_for_extension(extension, subtree=value) | ||
| if result is not None: | ||
| return result | ||
|
|
||
| return None | ||
|
|
||
| def valid_extension(extension, type=None): | ||
| if not type: | ||
| return extension in get_all_extensions() | ||
| @@ -106,6 +134,19 @@ def get_file_type(filename): | ||
| extension = extension[1:].lower() | ||
| return get_path_for_extension(extension) | ||
|
|
||
| def get_mime_type(filename): | ||
| _, extension = os.path.splitext(filename) | ||
| extension = extension[1:].lower() | ||
| mapping = get_content_type_mapping_for_extension(extension) | ||
| if mapping: | ||
| if isinstance(mapping, ContentTypeMapping) and mapping.content_type is not None: | ||
| return mapping.content_type | ||
| elif isinstance(mapping, ContentTypeDetector) and callable(mapping.detector): | ||
| result = mapping.detector(filename) | ||
| if result is not None: | ||
| return result | ||
| return "application/octet-stream" | ||
|
|
||
|
|
||
| class NoSuchStorage(Exception): | ||
| pass | ||
| @@ -7,6 +7,8 @@ | ||
|
|
||
| import io | ||
|
|
||
| from octoprint.util import atomic_write | ||
|
|
||
| class AbstractFileWrapper(object): | ||
| """ | ||
| Wrapper for file representations to save to storages. | ||
| @@ -85,7 +87,7 @@ def save(self, path): | ||
| """ | ||
| import shutil | ||
|
|
||
| with open(path, "wb") as dest: | ||
| with atomic_write(path, "wb") as dest: | ||
| with self.stream() as source: | ||
| shutil.copyfileobj(source, dest) | ||
|
|
||
| @@ -861,7 +861,7 @@ def initialize_implementations(self, additional_injects=None, additional_inject_ | ||
| 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))) | ||
| self.logger.info("Initialized {count} plugin implementation(s)".format(count=len(self.plugin_implementations))) | ||
|
|
||
| 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: | ||
| @@ -955,15 +955,13 @@ def log_all_plugins(self, show_bundled=True, bundled_str=(" (bundled)", ""), sho | ||
| self.logger.info("No plugins available") | ||
| else: | ||
| self.logger.info("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins), plugins="\n".join( | ||
| sorted( | ||
| map(lambda x: "| " + x.long_str(show_bundled=show_bundled, | ||
| bundled_strs=bundled_str, | ||
| show_location=show_location, | ||
| location_str=location_str, | ||
| show_enabled=show_enabled, | ||
| enabled_strs=enabled_str), | ||
| self.enabled_plugins.values()) | ||
| ) | ||
| map(lambda x: "| " + x.long_str(show_bundled=show_bundled, | ||
| bundled_strs=bundled_str, | ||
| show_location=show_location, | ||
| location_str=location_str, | ||
| show_enabled=show_enabled, | ||
| enabled_strs=enabled_str), | ||
| sorted(self.plugins.values(), key=lambda x: str(x).lower())) | ||
| ))) | ||
|
|
||
| def get_plugin(self, identifier, require_enabled=True): | ||
| @@ -393,7 +393,7 @@ def _load_profile(self, path): | ||
|
|
||
| def _save_profile(self, path, profile, allow_overwrite=True): | ||
| import yaml | ||
| with open(path, "wb") as f: | ||
| with octoprint.util.atomic_write(path, "wb") as f: | ||
| yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True) | ||
|
|
||
| def _convert_to_engine(self, profile_path, printer_profile, posX, posY): | ||
| @@ -82,14 +82,23 @@ def get_settings_defaults(self): | ||
| repository="http://plugins.octoprint.org/plugins.json", | ||
| repository_ttl=24*60, | ||
| pip=None, | ||
| pip_args=None, | ||
| dependency_links=False, | ||
| hidden=[] | ||
| ) | ||
|
|
||
| def on_settings_save(self, data): | ||
| old_pip = self._settings.get(["pip"]) | ||
| octoprint.plugin.SettingsPlugin.on_settings_save(self, data) | ||
| new_pip = self._settings.get(["pip"]) | ||
|
|
||
| self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60 | ||
| self._pip_caller.refresh = True | ||
| if old_pip != new_pip: | ||
| self._pip_caller.configured = new_pip | ||
| try: | ||
| self._pip_caller.trigger_refresh() | ||
| except: | ||
| self._pip_caller | ||
|
|
||
| ##~~ AssetPlugin | ||
|
|
||
| @@ -169,7 +178,20 @@ def on_api_get(self, request): | ||
| if "refresh_repository" in request.values and request.values["refresh_repository"] in valid_boolean_trues: | ||
| 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()) | ||
| return jsonify(plugins=result, | ||
| repository=dict( | ||
| available=self._repository_available, | ||
| plugins=self._repository_plugins | ||
| ), | ||
| os=self._get_os(), | ||
| octoprint=self._get_octoprint_version(), | ||
| pip=dict( | ||
| available=self._pip_caller.available, | ||
| command=self._pip_caller.command, | ||
| version=self._pip_caller.version_string, | ||
| use_sudo=self._pip_caller.use_sudo, | ||
| additional_args=self._settings.get(["pip_args"]) | ||
| )) | ||
|
|
||
| def on_api_command(self, command, data): | ||
| if not admin_permission.can(): | ||
| @@ -428,6 +450,10 @@ def _call_pip(self, args): | ||
| if self._pip_caller < self._pip_version_dependency_links: | ||
| args.remove("--process-dependency-links") | ||
|
|
||
| additional_args = self._settings.get(["pip_args"]) | ||
| if additional_args: | ||
| args.append(additional_args) | ||
|
|
||
| return self._pip_caller.execute(*args) | ||
|
|
||
| def _log_message(self, *lines): | ||
| @@ -510,7 +536,7 @@ def _fetch_repository_from_url(self): | ||
|
|
||
| try: | ||
| import json | ||
| with open(self._repository_cache_path, "w+b") as f: | ||
| with octoprint.util.atomic_write(self._repository_cache_path, "wb") as f: | ||
| json.dump(repo_data, f) | ||
| except Exception as e: | ||
| self._logger.exception("Error while saving repository data to {}: {}".format(self._repository_cache_path, str(e))) | ||
| @@ -603,7 +629,8 @@ def _to_external_representation(self, plugin): | ||
| pending_enable=(not plugin.enabled and plugin.key in self._pending_enable), | ||
| pending_disable=(plugin.enabled and plugin.key in self._pending_disable), | ||
| pending_install=(plugin.key in self._pending_install), | ||
| pending_uninstall=(plugin.key in self._pending_uninstall) | ||
| pending_uninstall=(plugin.key in self._pending_uninstall), | ||
| origin=plugin.origin.type | ||
| ) | ||
|
|
||
| __plugin_name__ = "Plugin Manager" | ||
Oops, something went wrong.