From b79d5e670abdc2741b8af19502148378b49e4df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 24 Oct 2014 19:59:59 +0200 Subject: [PATCH 01/14] Tornado's StaticFileHandler now supports content streaming, use that instead of our own implementation Also allow for definition of access validation and serving as attachment though. Hopefully also fixes #606, since that was possibly called by our own implementation of content streaming, which now has been removed completely. --- src/octoprint/server/util/tornado.py | 69 ++++------------------------ 1 file changed, 8 insertions(+), 61 deletions(-) diff --git a/src/octoprint/server/util/tornado.py b/src/octoprint/server/util/tornado.py index b88fe27e..cecc8f69 100644 --- a/src/octoprint/server/util/tornado.py +++ b/src/octoprint/server/util/tornado.py @@ -24,6 +24,7 @@ import tornado.httpclient import tornado.http1connection import tornado.iostream import tornado.tcpserver +import tornado.util import octoprint.util @@ -707,8 +708,6 @@ class CustomHTTP1ConnectionParameters(tornado.http1connection.HTTP1ConnectionPar class LargeResponseHandler(tornado.web.StaticFileHandler): - CHUNK_SIZE = 16 * 1024 - def initialize(self, path, default_filename=None, as_attachment=False, access_validation=None): tornado.web.StaticFileHandler.initialize(self, os.path.abspath(path), default_filename) self._as_attachment = as_attachment @@ -717,70 +716,18 @@ class LargeResponseHandler(tornado.web.StaticFileHandler): def get(self, path, include_body=True): if self._access_validation is not None: self._access_validation(self.request) - - path = self.parse_url_path(path) - abspath = os.path.abspath(os.path.join(self.root, path)) - # os.path.abspath strips a trailing / - # it needs to be temporarily added back for requests to root/ - if not (abspath + os.path.sep).startswith(self.root): - raise tornado.web.HTTPError(403, "%s is not in root static directory", path) - if os.path.isdir(abspath) and self.default_filename is not None: - # need to look at the request.path here for when path is empty - # but there is some prefix to the path that was already - # trimmed by the routing - if not self.request.path.endswith("/"): - self.redirect(self.request.path + "/") - return - abspath = os.path.join(abspath, self.default_filename) - if not os.path.exists(abspath): - raise tornado.web.HTTPError(404) - if not os.path.isfile(abspath): - raise tornado.web.HTTPError(403, "%s is not a file", path) - - stat_result = os.stat(abspath) - modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME]) - - self.set_header("Last-Modified", modified) - - mime_type, encoding = mimetypes.guess_type(abspath) - if mime_type: - self.set_header("Content-Type", mime_type) - - cache_time = self.get_cache_time(path, modified, mime_type) - - if cache_time > 0: - self.set_header("Expires", datetime.datetime.utcnow() + - datetime.timedelta(seconds=cache_time)) - self.set_header("Cache-Control", "max-age=" + str(cache_time)) - - self.set_extra_headers(path) - - # Check the If-Modified-Since, and don't send the result if the - # content has not been modified - ims_value = self.request.headers.get("If-Modified-Since") - if ims_value is not None: - date_tuple = email.utils.parsedate(ims_value) - if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple)) - if if_since >= modified: - self.set_status(304) - return - - if not include_body: - assert self.request.method == "HEAD" - self.set_header("Content-Length", stat_result[stat.ST_SIZE]) - else: - with open(abspath, "rb") as file: - while True: - data = file.read(LargeResponseHandler.CHUNK_SIZE) - if not data: - break - self.write(data) - self.flush() + result = tornado.web.StaticFileHandler.get(self, path, include_body=include_body) + return result def set_extra_headers(self, path): if self._as_attachment: self.set_header("Content-Disposition", "attachment") + @classmethod + def get_content_version(cls, abspath): + import os + import stat + return os.stat(abspath)[stat.ST_MTIME] ##~~ URL Forward Handler for forwarding requests to a preconfigured static URL From e149689249660ce8341057e1e044b2edf311b7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 24 Oct 2014 20:11:58 +0200 Subject: [PATCH 02/14] Enable logging of tornado errors by default, to make sure octoprint.log will contain meaningful entries if something goes wrong in our tornado customization --- src/octoprint/server/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index efc56ecf..754a682c 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -413,6 +413,9 @@ class Server(): "level": "CRITICAL", "handlers": ["serialFile"], "propagate": False + }, + "tornado.application": { + "level": "ERROR" } }, "root": { From ce67e28f9624dc05d2379f5cff9397e0b6f92e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 Oct 2014 09:35:55 +0100 Subject: [PATCH 03/14] Generate the salt used for hashing user passwords individually for each server instance --- CHANGELOG.md | 1 + src/octoprint/server/api/__init__.py | 2 +- src/octoprint/settings.py | 1 + src/octoprint/users.py | 42 ++++++++++++++++++++++++++-- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f7ccd1..f0760dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ server start and written back into ``config.yaml`` * Event subscriptions are now enabled by default (it was an accident that they weren't) * Generate the key used for session hashing individually for each server instance +* Generate the salt used for hashing user passwords individually for each server instance ### Bug Fixes diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 816d5187..613590a5 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -120,7 +120,7 @@ def login(): user = octoprint.server.userManager.findUser(username) if user is not None: - if user.check_password(octoprint.users.UserManager.createPasswordHash(password)): + if octoprint.server.userManager.checkPassword(username, password): login_user(user, remember=remember) identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id())) return jsonify(user.asDict()) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 68ba8b8f..0f70641f 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -114,6 +114,7 @@ default_settings = { }, "accessControl": { "enabled": True, + "salt": None, "userManager": "octoprint.users.FilebasedUserManager", "userfile": None, "autologinLocal": False, diff --git a/src/octoprint/users.py b/src/octoprint/users.py index 58f2f732..dc6e28f8 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -1,6 +1,9 @@ # coding=utf-8 +from __future__ import absolute_import + __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" from flask.ext.login import UserMixin from flask.ext.principal import Identity @@ -15,8 +18,38 @@ class UserManager(object): valid_roles = ["user", "admin"] @staticmethod - def createPasswordHash(password): - return hashlib.sha512(password + "mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW").hexdigest() + def createPasswordHash(password, salt=None): + if not salt: + salt = settings().get(["accessControl", "salt"]) + if salt is None: + import string + from random import choice + chars = string.ascii_lowercase + string.ascii_uppercase + string.digits + salt = "".join(choice(chars) for _ in xrange(32)) + settings().set(["accessControl", "salt"], salt) + settings().save() + + return hashlib.sha512(password + salt).hexdigest() + + def checkPassword(self, username, password): + user = self.findUser(username) + if not user: + return False + + hash = UserManager.createPasswordHash(password) + if user.check_password(hash): + # new hash matches, correct password + return True + else: + # new hash doesn't match, but maybe the old one does, so check that! + oldHash = UserManager.createPasswordHash(password, salt="mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW") + if user.check_password(oldHash): + # old hash matches, we migrate the stored password hash to the new one and return True since it's the correct password + self.changeUserPassword(username, password) + return True + else: + # old hash doesn't match either, wrong password + return False def addUser(self, username, password, active, roles): pass @@ -97,7 +130,10 @@ class FilebasedUserManager(UserManager): self._dirty = False self._load() - def addUser(self, username, password, active=False, roles=["user"], apikey=None): + def addUser(self, username, password, active=False, roles=None, apikey=None): + if not roles: + roles = ["user"] + if username in self._users.keys(): raise UserAlreadyExists(username) From 644329c0b8e72346f61a07d488679d12b2b00863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 27 Oct 2014 09:48:54 +0100 Subject: [PATCH 04/14] Preparing release of 1.1.1 --- CHANGELOG.md | 2 +- README.md | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0760dd4..503e66aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # OctoPrint Changelog -## 1.1.1 (Unreleased) +## 1.1.1 (2014-10-27) ### Improvements diff --git a/README.md b/README.md index f219d122..ac80de2e 100644 --- a/README.md +++ b/README.md @@ -29,43 +29,45 @@ OctoPrint via `setup.py`: python setup.py install -You should also do this after pulling from the repository, since the dependencies might have changed. +You should also do this every time after pulling from the repository, since the dependencies might have changed. OctoPrint currently only supports Python 2.7. Usage ----- -From the source directory you can start the server via +Running the `setup.py` script installs the `octoprint` script in your Python installation's scripts folder +(which depending on whether you installed OctoPrint globally or into a virtual env will be on your `PATH` or not). The +following usage examples assume that said `octoprint` script is on your `PATH`. - ./run +You can start the server via + + octoprint By default it binds to all interfaces on port 5000 (so pointing your browser to `http://127.0.0.1:5000` will do the trick). If you want to change that, use the additional command line parameters `host` and `port`, which accept the host ip to bind to and the numeric port number respectively. If for example you want the server to only listen on the local interface on port 8080, the command line would be - ./run --host=127.0.0.1 --port=8080 + octoprint --host=127.0.0.1 --port=8080 Alternatively, the host and port on which to bind can be defined via the configuration. If you want to run OctoPrint as a daemon (only supported on Linux), use - ./run --daemon {start|stop|restart} [--pid PIDFILE] + octoprint --daemon {start|stop|restart} [--pid PIDFILE] If you do not supply a custom pidfile location via `--pid PIDFILE`, it will be created at `/tmp/octoprint.pid`. You can also specify the configfile or the base directory (for basing off the `uploads`, `timelapse` and `logs` folders), e.g.: - ./run --config /path/to/another/config.yaml --basedir /path/to/my/basedir + octoprint --config /path/to/another/config.yaml --basedir /path/to/my/basedir -See `run --help` for further information. - -Running the `setup.py` script also installs the `octoprint` startup script in your Python installation's scripts folder -(which depending on whether you installed OctoPrint globally or into a virtual env will be on your `PATH` or not). The -examples above also work with that startup script as it excepts the same parameters as `run`. +See `octoprint --help` for further information. +OctoPrint also ships with a `run` script in its source directory. You can also invoke that to start up the server, it +takes the same command line arguments as the `octoprint` script. Configuration ------------- @@ -75,4 +77,5 @@ which is located at `~/.octoprint` on Linux, at `%APPDATA%/OctoPrint` on Windows at `~/Library/Application Support/OctoPrint` on MacOS. A comprehensive overview of all available configuration settings can be found -[on the wiki](https://github.com/foosel/OctoPrint/wiki/Configuration). +[on the wiki](https://github.com/foosel/OctoPrint/wiki/Configuration). Please note that the most commonly used +configuration settings can also easily be edited from OctoPrint's settings dialog. From 44cc9e7db81037a170c1c4a77894a0daab2fcede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 29 Oct 2014 11:57:43 +0100 Subject: [PATCH 05/14] Cura Plugin: Filament diameter and hotend temperature where not correctly extracted from profile and supplied to CuraEngine, resulting in low print quality (using the default of 2.85mm filament diameter doesn't work well for 1.75mm machines...) --- src/octoprint/plugins/cura/profile.py | 134 +++++++++++++++----------- 1 file changed, 76 insertions(+), 58 deletions(-) diff --git a/src/octoprint/plugins/cura/profile.py b/src/octoprint/plugins/cura/profile.py index 60ff045f..04b42c53 100644 --- a/src/octoprint/plugins/cura/profile.py +++ b/src/octoprint/plugins/cura/profile.py @@ -457,55 +457,70 @@ class Profile(object): @classmethod def merge_profile(cls, profile, overrides=None): - import copy - - result = copy.deepcopy(defaults) - for k in result.keys(): - profile_value = None - override_value = None - - if k in profile: - profile_value = profile[k] - if overrides and k in overrides: - override_value = overrides[k] - - if profile_value is None and override_value is None: - # neither override nor profile, no need to handle this key further - continue - - if k in ("filament_diameter", "print_temperature", "start_gcode", "end_gcode"): - # the array fields need some special treatment. Basically something like this: - # - # override_value: [None, "b"] - # profile_value : ["a" , None, "c"] - # default_value : ["d" , "e" , "f", "g"] - # - # should merge to something like this: - # - # ["a" , "b" , "c", "g"] - # - # So override > profile > default, if neither override nor profile value are available - # the default value should just be left as is - - for x in xrange(len(result[k])): - if override_value is not None and x < len(override_value) and override_value[x] is not None: - # we have an override value for this location, so we use it - result[k][x] = override_value[x] - elif profile_value is not None and x < len(profile_value) and profile_value[x] is not None: - # we have a profile value for this location, so we use it - result[k][x] = profile_value[x] - - else: - # just change the result value to the override_value if available, otherwise to the profile_value if - # that is given, else just leave as is - if override_value is not None: - result[k] = override_value - elif profile_value is not None: - result[k] = profile_value + result = dict() + for key in defaults.keys(): + r = cls.merge_profile_key(key, profile, overrides=overrides) + if r is not None: + result[key] = r return result - def __init__(self, profile): - self.profile = profile + @classmethod + def merge_profile_key(cls, key, profile, overrides=None): + profile_value = None + override_value = None + + if not key in defaults: + return None + import copy + result = copy.deepcopy(defaults[key]) + + if key in profile: + profile_value = profile[key] + if overrides and key in overrides: + override_value = overrides[key] + + if profile_value is None and override_value is None: + # neither override nor profile, no need to handle this key further + return None + + if key in ("filament_diameter", "print_temperature", "start_gcode", "end_gcode"): + # the array fields need some special treatment. Basically something like this: + # + # override_value: [None, "b"] + # profile_value : ["a" , None, "c"] + # default_value : ["d" , "e" , "f", "g"] + # + # should merge to something like this: + # + # ["a" , "b" , "c", "g"] + # + # So override > profile > default, if neither override nor profile value are available + # the default value should just be left as is + + for x in xrange(len(result)): + if override_value is not None and x < len(override_value) and override_value[x] is not None: + # we have an override value for this location, so we use it + result[x] = override_value[x] + elif profile_value is not None and x < len(profile_value) and profile_value[x] is not None: + # we have a profile value for this location, so we use it + result[x] = profile_value[x] + + else: + # just change the result value to the override_value if available, otherwise to the profile_value if + # that is given, else just leave as is + if override_value is not None: + result = override_value + elif profile_value is not None: + result = profile_value + + return result + + def __init__(self, profile, overrides=None): + self._profile = self.__class__.merge_profile(profile, overrides=overrides) + + def profile(self): + import copy + return copy.deepcopy(self._profile) def get(self, key): if key in ("machine_width", "machine_depth", "machine_center_is_zero"): @@ -547,7 +562,7 @@ class Profile(object): if not match: return 0.0 - diameters = defaults["filament_diameter"] + diameters = self._get("filament_diameter") if not match.group(1): return diameters[0] index = int(match.group(1)) @@ -560,7 +575,7 @@ class Profile(object): if not match: return 0.0 - temperatures = defaults["print_temperature"] + temperatures = self._get("print_temperature") if not match.group(1): return temperatures[0] index = int(match.group(1)) @@ -569,12 +584,15 @@ class Profile(object): return temperatures[index] else: - if key in self.profile: - return self.profile[key] - elif key in defaults: - return defaults[key] - else: - return None + return self._get(key) + + def _get(self, key): + if key in self._profile: + return self._profile[key] + elif key in defaults: + return defaults[key] + else: + return None def get_int(self, key, default=None): value = self.get(key) @@ -622,8 +640,8 @@ class Profile(object): def get_gcode_template(self, key): extruder_count = s.globalGetInt(["printerParameters", "numExtruders"]) - if key in self.profile: - gcode = self.profile[key] + if key in self._profile: + gcode = self._profile[key] else: gcode = defaults[key] @@ -646,7 +664,7 @@ class Profile(object): import copy profile = copy.deepcopy(defaults) - profile.update(self.profile) + profile.update(self._profile) for key in ("print_temperature", "print_temperature2", "print_temperature3", "print_temperature4", "filament_diameter", "filament_diameter2", "filament_diameter3", "filament_diameter4"): profile[key] = self.get(key) From 0aac7813e44d32532e83b3a3b0660bf7c3025903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 29 Oct 2014 12:16:49 +0100 Subject: [PATCH 06/14] More work on slicing integration: - be able to handle slicers which can't report progress - distinguish between registered and configured slicers (e.g. to allow uploading of profiles via the settings before having saved the path to the executable) --- docs/events/index.rst | 1 + src/octoprint/plugin/types.py | 1 + src/octoprint/plugins/cura/__init__.py | 3 ++- src/octoprint/slicing/__init__.py | 19 +++++++++++++------ src/octoprint/static/js/app/dataupdater.js | 6 +++++- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/events/index.rst b/docs/events/index.rst index 9a1d5310..f0a55169 100644 --- a/docs/events/index.rst +++ b/docs/events/index.rst @@ -328,6 +328,7 @@ SlicingStarted * ``stl``: the STL's filename * ``gcode``: the sliced GCODE's filename + * ``progressAvailable``: true if progress information via the ``slicingProgress`` push update will be available, false if not SlicingDone The slicing of a file has completed. diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 12e41dab..28747317 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -199,6 +199,7 @@ class SlicerPlugin(Plugin): type=None, name=None, same_device=True, + progress_report=False ) def get_slicer_profile_options(self): diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index 9e963919..9cebe18e 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -194,7 +194,8 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, return dict( type="cura", name="CuraEngine", - same_device=True + same_device=True, + progress_report=True ) def get_slicer_default_profile(self): diff --git a/src/octoprint/slicing/__init__.py b/src/octoprint/slicing/__init__.py index 0cd4ae17..fa44a8b7 100644 --- a/src/octoprint/slicing/__init__.py +++ b/src/octoprint/slicing/__init__.py @@ -73,22 +73,26 @@ class SlicingManager(object): @property def slicing_enabled(self): - return len(self.registered_slicers) > 0 + return len(self.configured_slicers) > 0 @property def registered_slicers(self): return self._slicers.keys() + @property + def configured_slicers(self): + return map(lambda slicer: slicer.get_slicer_properties()["type"], filter(lambda slicer: slicer.is_slicer_configured(), self._slicers.values())) + @property def default_slicer(self): slicer_name = settings().get(["slicing", "defaultSlicer"]) - if slicer_name in self.registered_slicers: + if slicer_name in self.configured_slicers: return slicer_name else: return None - def get_slicer(self, slicer): - return self._slicers[slicer] if slicer in self._slicers else None + def get_slicer(self, slicer, require_configured=True): + return self._slicers[slicer] if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()) else None def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None): if callback_args is None: @@ -96,8 +100,11 @@ class SlicingManager(object): if callback_kwargs is None: callback_kwargs = dict() - if not slicer_name in self.registered_slicers: - error = "No such slicer: {slicer_name}".format(**locals()) + if not slicer_name in self.configured_slicers: + if not slicer_name in self.registered_slicers: + error = "No such slicer: {slicer_name}".format(**locals()) + else: + error = "Slicer not configured: {slicer_name}".format(**locals()) callback_kwargs.update(dict(_error=error)) callback(*callback_args, **callback_kwargs) return False, error diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index f134fe86..9a709017 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -181,7 +181,11 @@ function DataUpdater(allViewModels) { } else if (type == "SlicingStarted") { gcodeUploadProgress.addClass("progress-striped").addClass("active"); gcodeUploadProgressBar.css("width", "100%"); - gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0})); + if (payload.progressAvailable) { + gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0})); + } else { + gcodeUploadProgressBar.text(gettext("Slicing ...")); + } } else if (type == "SlicingDone") { gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); gcodeUploadProgressBar.css("width", "0%"); From f8955c0d1bf784a48d8f653878142bc49591aaa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 29 Oct 2014 12:17:26 +0100 Subject: [PATCH 07/14] Fix of file type detection --- src/octoprint/filemanager/__init__.py | 11 +++++++---- tests/filemanager/test_filemanager.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index 6c27752a..1d73288b 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -200,9 +200,11 @@ class FileManager(object): if dest_job_key in self._slicing_jobs: del self._slicing_jobs[dest_job_key] + slicer = self._slicing_manager.get_slicer(slicer_name) + import time start_time = time.time() - eventManager().fire(Events.SLICING_STARTED, {"stl": source_path, "gcode": dest_path}) + eventManager().fire(Events.SLICING_STARTED, {"stl": source_path, "gcode": dest_path, "progressAvailable": slicer.get_slicer_properties()["progress_report"] if slicer else False}) import tempfile f = tempfile.NamedTemporaryFile(suffix=".gco", delete=False) @@ -266,10 +268,11 @@ class FileManager(object): def add_file(self, destination, path, file_object, links=None, allow_overwrite=False): file_path = self._storage(destination).add_file(path, file_object, links=links, allow_overwrite=allow_overwrite) absolute_path = self._storage(destination).get_absolute_path(file_path) - file_type = get_file_type(file_path)[-1] - queue_entry = QueueEntry(file_path, file_type, destination, absolute_path) - self._analysis_queue.enqueue(queue_entry, high_priority=True) + file_type = get_file_type(absolute_path) + if file_type: + queue_entry = QueueEntry(file_path, file_type[-1], destination, absolute_path) + self._analysis_queue.enqueue(queue_entry, high_priority=True) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) return file_path diff --git a/tests/filemanager/test_filemanager.py b/tests/filemanager/test_filemanager.py index a7f1ddca..4201ac23 100644 --- a/tests/filemanager/test_filemanager.py +++ b/tests/filemanager/test_filemanager.py @@ -157,7 +157,7 @@ class FileManagerTest(unittest.TestCase): self.file_manager.slice("some_slicer", octoprint.filemanager.FileDestinations.LOCAL, "source.file", octoprint.filemanager.FileDestinations.LOCAL, "dest.file", callback=callback, callback_args=callback_args) # assert that events where fired - expected_events = [mock.call(octoprint.filemanager.Events.SLICING_STARTED, {"stl": "source.file", "gcode": "dest.file"}), + expected_events = [mock.call(octoprint.filemanager.Events.SLICING_STARTED, {"stl": "source.file", "gcode": "dest.file", "progressAvailable": False}), mock.call(octoprint.filemanager.Events.SLICING_DONE, {"stl": "source.file", "gcode": "dest.file", "time": 15.694000005722046})] self.fire_event.call_args_list = expected_events From 62667de4a8ae9351a974493dc1d83843a0513293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 30 Oct 2014 12:22:58 +0100 Subject: [PATCH 08/14] Fix: Don't try to create a SessionUser for anonymous users --- src/octoprint/users.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/octoprint/users.py b/src/octoprint/users.py index ddbc9805..2d01a53e 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -7,6 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms from flask.ext.login import UserMixin from flask.ext.principal import Identity +from werkzeug.local import LocalProxy import hashlib import os import yaml @@ -27,7 +28,9 @@ class UserManager(object): def login_user(self, user): self._cleanup_sessions() - if user is None: + if user is None \ + or (isinstance(user, LocalProxy) and not isinstance(user._get_current_object(), User)) \ + or (not isinstance(user, LocalProxy) and not isinstance(user, User)): return None if not isinstance(user, SessionUser): From ca6364e5d4b96ba27157078900a2304be8b31c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 31 Oct 2014 14:08:52 +0100 Subject: [PATCH 09/14] Correctly interpret Smoothieware temperature data for multiple extruders Fixes #633 --- CHANGELOG.md | 2 ++ src/octoprint/settings.py | 1 + src/octoprint/util/comm.py | 12 +++++++++--- src/octoprint/util/virtual.py | 3 +++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 906dc724..7ff95e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ * [#435](https://github.com/foosel/OctoPrint/issues/435) - Always interpret negative duration (e.g. for print time left) as 0 +* [#633](https://github.com/foosel/OctoPrint/issues/633) - Correctly interpret temperature lines from multi extruder + setups under Smoothieware * Various fixes of bugs in newly introduced features and improvements: * [#625](https://github.com/foosel/OctoPrint/pull/625) - Newly added GCODE files were not being added to the analysis queue diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 68b90c6a..ff05e9d3 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -172,6 +172,7 @@ default_settings = { "includeCurrentToolInTemps": True, "hasBed": True, "repetierStyleTargetTemperature": False, + "smoothieTemperatureReporting": False, "extendedSdFileList": False } } diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 9292592c..f05a777f 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -577,8 +577,8 @@ class MachineCom(object): maxToolNum, parsedTemps = self._parseTemperatures(line) # extruder temperatures - if not "T0" in parsedTemps.keys() and "T" in parsedTemps.keys(): - # only single reporting, "T" is our one and only extruder temperature + if not "T0" in parsedTemps.keys() and not "T1" in parsedTemps.keys() and "T" in parsedTemps.keys(): + # no T1 so only single reporting, "T" is our one and only extruder temperature toolNum, actual, target = parsedTemps["T"] if target is not None: @@ -588,7 +588,13 @@ class MachineCom(object): self._temp[0] = (actual, oldTarget) else: self._temp[0] = (actual, None) - elif "T0" in parsedTemps.keys(): + elif not "T0" in parsedTemps.keys() and "T" in parsedTemps.keys(): + # Smoothieware sends multi extruder temperature data this way: "T: T1: ..." and therefore needs some special treatment... + _, actual, target = parsedTemps["T"] + del parsedTemps["T"] + parsedTemps["T0"] = (0, actual, target) + + if "T0" in parsedTemps.keys(): for n in range(maxToolNum + 1): tool = "T%d" % n if not tool in parsedTemps.keys(): diff --git a/src/octoprint/util/virtual.py b/src/octoprint/util/virtual.py index 021bc5b6..239954af 100644 --- a/src/octoprint/util/virtual.py +++ b/src/octoprint/util/virtual.py @@ -213,6 +213,9 @@ class VirtualPrinter(): allTemps.append((i, self.temp[i], self.targetTemp[i])) allTempsString = " ".join(map(lambda x: "T%d:%.2f /%.2f" % x if includeTarget else "T%d:%.2f" % (x[0], x[1]), allTemps)) + if settings().getBoolean(["devel", "virtualPrinter", "smoothieTemperatureReporting"]): + allTempsString = allTempsString.replace("T0:", "T:") + if settings().getBoolean(["devel", "virtualPrinter", "hasBed"]): if includeTarget: allTempsString = "B:%.2f /%.2f %s" % (self.bedTemp, self.bedTargetTemp, allTempsString) From 87234bda0586c1e0b89ef93cfe6bfd1f651dcbb1 Mon Sep 17 00:00:00 2001 From: Colin Wallace Date: Mon, 3 Nov 2014 05:50:09 +0000 Subject: [PATCH 10/14] Fix missing branch field in DEFAULT dict Should fix an error raised on line 951 (SHORT_VERSION_PY % DEFAULT) during Octoprint setup when there's no version info. More info here: https://groups.google.com/forum/#!topic/deltabot/8udyUsJ1c9M --- versioneer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versioneer.py b/versioneer.py index 86c9a811..f88eb53e 100644 --- a/versioneer.py +++ b/versioneer.py @@ -809,7 +809,7 @@ def get_versions(default={}, verbose=False): """ -DEFAULT = {"version": "unknown", "full": "unknown"} +DEFAULT = {"version": "unknown", "full": "unknown", "branch": ""} def versions_from_file(filename): versions = {} From ff96e2e4d66fe6648fac2435486d403033086826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 3 Nov 2014 21:39:49 +0100 Subject: [PATCH 11/14] Also interpret lines starting with "!!" as errors from the firmware (see http://reprap.org/wiki/G-code#Replies_from_the_RepRap_machine_to_the_host_computer) --- CHANGELOG.md | 1 + src/octoprint/util/comm.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff95e3a..8530de58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ * The "Slicing done" notification is now colored green ([#558](https://github.com/foosel/OctoPrint/issues/558)). * File management now supports STL files as first class citizens (including UI adjustments to allow management of uploaded STL files including removal and reslicing) and also allows folders (not yet supported by UI) +* Also interpret lines starting with "!!" as errors ### Bug Fixes diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index f05a777f..7c0af4d7 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -986,7 +986,7 @@ class MachineCom(object): def _handleErrors(self, line): # No matter the state, if we see an error, goto the error state and store the error for reference. - if line.startswith('Error:'): + if line.startswith('Error:') or line.startswith('!!'): #Oh YEAH, consistency. # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!" From 5acb04c18061abb18e6b6610d9b4e9436d477318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 4 Nov 2014 10:07:46 +0100 Subject: [PATCH 12/14] Fixed a couple of more missing branch fields and added Changelog entry --- CHANGELOG.md | 7 +++++++ src/octoprint/_version.py | 4 ++-- versioneer.py | 22 ++++++++++++---------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 503e66aa..e6fafd52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # OctoPrint Changelog +## 1.1.2 (Unreleased) + +### Bug Fixes + +* [#634](https://github.com/foosel/OctoPrint/pull/634) - Fixed missing `branch` fields in version dicts generated + by versioneer + ## 1.1.1 (2014-10-27) ### Improvements diff --git a/src/octoprint/_version.py b/src/octoprint/_version.py index 3808a4f8..35090f5b 100644 --- a/src/octoprint/_version.py +++ b/src/octoprint/_version.py @@ -214,7 +214,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose=False): print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % (root, dirname, parentdir_prefix)) return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} + return {"version": dirname[len(parentdir_prefix):], "full": "", "branch": ""} tag_prefix = "" parentdir_prefix = "" @@ -249,7 +249,7 @@ def parse_lookup_file(root, lookup_path=None): break return lookup -def get_versions(default={"version": "unknown", "full": ""}, lookup_path=None, verbose=False): +def get_versions(default={"version": "unknown", "full": "", "branch": "unknown"}, lookup_path=None, verbose=False): # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which diff --git a/versioneer.py b/versioneer.py index f88eb53e..6b473324 100644 --- a/versioneer.py +++ b/versioneer.py @@ -473,7 +473,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose=False): print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %% (root, dirname, parentdir_prefix)) return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} + return {"version": dirname[len(parentdir_prefix):], "full": "", "branch": ""} tag_prefix = "%(TAG_PREFIX)s" parentdir_prefix = "%(PARENTDIR_PREFIX)s" @@ -508,7 +508,7 @@ def parse_lookup_file(root, lookup_path=None): break return lookup -def get_versions(default={"version": "unknown", "full": ""}, lookup_path=None, verbose=False): +def get_versions(default={"version": "unknown", "full": "", "branch": "unknown"}, lookup_path=None, verbose=False): # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which @@ -649,7 +649,8 @@ def versions_from_expanded_variables(variables, tag_prefix, verbose=False): if verbose: print("no suitable tags, using full revision id") return { "version": variables["full"].strip(), - "full": variables["full"].strip() } + "full": variables["full"].strip(), + "branch": ""} def versions_from_lookup(lookup, root, verbose=False): @@ -741,7 +742,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose=False): print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % (root, dirname, parentdir_prefix)) return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} + return {"version": dirname[len(parentdir_prefix):], "full": "", "branch": ""} import os.path import sys @@ -801,15 +802,13 @@ SHORT_VERSION_PY = """ version_version = '%(version)s' version_full = '%(full)s' -version_branch = %(branch)r +version_branch = '%(branch)s' def get_versions(default={}, verbose=False): - if version_branch: - return {'version': version_version, 'full': version_full, 'branch': version_branch} - return {'version': version_version, 'full': version_full} + return {'version': version_version, 'full': version_full, 'branch': version_branch} """ -DEFAULT = {"version": "unknown", "full": "unknown", "branch": ""} +DEFAULT = {"version": "unknown", "full": "unknown", "branch": "unknown"} def versions_from_file(filename): versions = {} @@ -824,6 +823,9 @@ def versions_from_file(filename): mo = re.match("version_full = '([^']+)'", line) if mo: versions["full"] = mo.group(1) + mo = re.match("version_branch = '([^']+)'", line) + if mo: + versions["branch"] = mo.group(1) f.close() return versions @@ -866,7 +868,7 @@ def parse_lookup_file(root, lookup_path=None): return lookup def get_versions(default=DEFAULT, verbose=False): - # returns dict with two keys: 'version' and 'full' + # returns dict with three keys: 'version', 'full' and 'branch' assert versionfile_source is not None, "please set versioneer.versionfile_source" assert tag_prefix is not None, "please set versioneer.tag_prefix" assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix" From 9e014eba1feffde11ed0601d9c911b8cac9f3fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 4 Nov 2014 10:36:35 +0100 Subject: [PATCH 13/14] Added removal of pyc files (and their parent folders if they are empty after that) to the python setup.py clean command --- setup.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/setup.py b/setup.py index d6b6cb4b..26876883 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,26 @@ def package_data_dirs(source, sub_folders): return dirs +def _recursively_handle_files(directory, file_matcher, folder_handler=None, file_handler=None): + applied_handler = False + + for filename in os.listdir(directory): + path = os.path.join(directory, filename) + + if file_handler is not None and file_matcher(filename): + file_handler(path) + applied_handler = True + + elif os.path.isdir(path): + sub_applied_handler = _recursively_handle_files(path, file_matcher, folder_handler=folder_handler, file_handler=file_handler) + if sub_applied_handler: + applied_handler = True + + if folder_handler is not None: + folder_handler(path, sub_applied_handler) + + return applied_handler + class CleanCommand(Command): description = "clean build artifacts" user_options = [] @@ -45,14 +65,37 @@ class CleanCommand(Command): pass def run(self): + # build folder if os.path.exists('build'): print "Deleting build directory" shutil.rmtree('build') + + # eggs eggs = glob.glob('OctoPrint*.egg-info') for egg in eggs: print "Deleting %s directory" % egg shutil.rmtree(egg) + # pyc files + def delete_folder_if_empty(path, applied_handler): + if not applied_handler: + return + if len(os.listdir(path)) == 0: + shutil.rmtree(path) + print "Deleted %s since it was empty" % path + + def delete_file(path): + os.remove(path) + print "Deleted %s" % path + + import fnmatch + _recursively_handle_files( + os.path.abspath("src"), + lambda name: fnmatch.fnmatch(name.lower(), "*.pyc"), + folder_handler=delete_folder_if_empty, + file_handler=delete_file + ) + class NewTranslation(Command): description = "create a new translation" From 16801ad3e1c55fa4862433fbb4f1b60d141ba1ff Mon Sep 17 00:00:00 2001 From: Michael Ang Date: Fri, 7 Nov 2014 00:24:05 +0100 Subject: [PATCH 14/14] Disable autocapitalization for username Makes it easier to login on iPhone (Mobile Safari) --- src/octoprint/templates/index.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index cdf9e7cc..ff568434 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -100,7 +100,7 @@