diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index babee5b3..1a67a1f6 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -404,7 +404,7 @@ class LocalFileStorage(StorageInterface): from slugify import Slugify self._slugify = Slugify() - self._slugify.safe_chars = "-_.() " + self._slugify.safe_chars = "-_.()[] " self._old_metadata = None self._initialize_metadata() @@ -779,7 +779,11 @@ class LocalFileStorage(StorageInterface): if "/" in name or "\\" in name: raise ValueError("name must not contain / or \\") - return self._slugify(name).replace(" ", "_") + result = self._slugify(name).replace(" ", "_") + if result and result != "." and result != ".." and result[0] == ".": + # hidden files under *nix + result = result[1:] + return result def sanitize_path(self, path): """ @@ -787,8 +791,11 @@ class LocalFileStorage(StorageInterface): relative path elements (e.g. ``..``) and sanitizes folder names using :func:`sanitize_name`. Final path is the absolute path including leading ``basefolder`` path. """ - if path[0] == "/" or path[0] == ".": + if path[0] == "/": path = path[1:] + elif path[0] == "." and path[1] == "/": + path = path[2:] + path_elements = path.split("/") joined_path = self.basefolder for path_element in path_elements: @@ -1052,6 +1059,28 @@ class LocalFileStorage(StorageInterface): entry_path = os.path.join(path, entry) path_in_location = entry if not base else base + entry + sanitized = self.sanitize_name(entry) + if sanitized != entry: + # entry is not sanitized yet, let's take care of that + sanitized_path = os.path.join(path, sanitized) + sanitized_name, sanitized_ext = os.path.splitext(sanitized) + + counter = 1 + while os.path.exists(sanitized_path): + counter += 1 + sanitized = self.sanitize_name("{}_({}){}".format(sanitized_name, counter, sanitized_ext)) + sanitized_path = os.path.join(path, sanitized) + + try: + shutil.move(entry_path, sanitized_path) + + self._logger.info("Sanitized \"{}\" to \"{}\"".format(entry_path, sanitized_path)) + entry = sanitized + entry_path = sanitized_path + except: + self._logger.exception("Error while trying to rename \"{}\" to \"{}\", ignoring file".format(entry_path, sanitized_path)) + continue + # file handling if os.path.isfile(entry_path): type_path = octoprint.filemanager.get_file_type(entry) diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index ec0d1f65..0b2da702 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -796,15 +796,16 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback): printTimeLeft = dumbTotalPrintTime - cleanedPrintTime printTimeLeftOrigin = "linear" - elif progress > self._timeEstimationForceDumbFromPercent or \ - cleanedPrintTime * 60 >= self._timeEstimationForceDumbAfterMin: - # more than x% or y min printed and still no real estimate, ok, we'll use the dumb variant :/ - printTimeLeft = dumbTotalPrintTime - cleanedPrintTime + else: printTimeLeftOrigin = "linear" + if progress > self._timeEstimationForceDumbFromPercent or \ + cleanedPrintTime >= self._timeEstimationForceDumbAfterMin * 60: + # more than x% or y min printed and still no real estimate, ok, we'll use the dumb variant :/ + printTimeLeft = dumbTotalPrintTime - cleanedPrintTime - if printTimeLeft < 0: + if printTimeLeft is not None and printTimeLeft < 0: # shouldn't actually happen, but let's make sure - return None, None + printTimeLeft = None return printTimeLeft, printTimeLeftOrigin diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index a26f9566..421ff4c7 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -363,7 +363,7 @@ function bytesFromSize(size) { function formatDuration(seconds) { if (!seconds) return "-"; - if (seconds < 0) return "00:00:00"; + if (seconds < 1) return "00:00:00"; var s = seconds % 60; var m = (seconds % 3600) / 60; @@ -373,8 +373,7 @@ function formatDuration(seconds) { } function formatFuzzyEstimation(seconds, base) { - if (!seconds) return "-"; - if (seconds < 0) return "-"; + if (!seconds || seconds < 1) return "-"; var m; if (base != undefined) { @@ -387,6 +386,118 @@ function formatFuzzyEstimation(seconds, base) { return m.fromNow(true); } +function formatFuzzyPrintTime(totalSeconds) { + /** + * Formats a print time estimate in a very fuzzy way. + * + * Accuracy decreases the higher the estimation is: + * + * * less than 30s: "a couple of seconds" + * * 30s to a minute: "less than a minute" + * * 1 to 30min: rounded to full minutes, above 30s is minute + 1 ("27 minutes", "2 minutes") + * * 30min to 40min: "40 minutes" + * * 40min to 50min: "50 minutes" + * * 50min to 1h: "1 hour" + * * 1 to 12h: rounded to half hours, 15min to 45min is ".5", above that hour + 1 ("4 hours", "2.5 hours") + * * 12 to 24h: rounded to full hours, above 30min is hour + 1, over 23.5h is "1 day" + * * Over a day: rounded to half days, 8h to 16h is ".5", above that days + 1 ("1 day", "4 days", "2.5 days") + */ + + if (!totalSeconds || totalSeconds < 1) return "-"; + + var d = moment.duration(totalSeconds, "seconds"); + + var seconds = d.seconds(); + var minutes = d.minutes(); + var hours = d.hours(); + var days = d.asDays(); + + var replacements = { + days: days, + hours: hours, + minutes: minutes, + seconds: seconds, + totalSeconds: totalSeconds + }; + + var text = "-"; + + if (days >= 1) { + // days + if (hours >= 16) { + replacements.days += 1; + text = gettext("%(days)d days"); + } else if (hours >= 8 && hours < 16) { + text = gettext("%(days)d.5 days"); + } else { + if (days == 1) { + text = gettext("%(days)d day"); + } else { + text = gettext("%(days)d days"); + } + } + } else if (hours >= 1) { + // only hours + if (hours < 12) { + if (minutes < 15) { + // less than .15 => .0 + if (hours == 1) { + text = gettext("%(hours)d hour"); + } else { + text = gettext("%(hours)d hours"); + } + } else if (minutes >= 15 && minutes < 45) { + // between .25 and .75 => .5 + text = gettext("%(hours)d.5 hours"); + } else { + // over .75 => hours + 1 + replacements.hours += 1; + text = gettext("%(hours)d hours"); + } + } else { + if (hours == 23 && minutes > 30) { + // over 23.5 hours => 1 day + text = gettext("1 day"); + } else { + if (minutes > 30) { + // over .5 => hours + 1 + replacements.hours += 1; + } + text = gettext("%(hours)d hours"); + } + } + } else if (minutes >= 1) { + // only minutes + if (minutes < 2) { + if (seconds < 30) { + text = gettext("a minute"); + } else { + text = gettext("2 minutes"); + } + } else if (minutes < 30) { + if (seconds > 30) { + replacements.minutes += 1; + } + text = gettext("%(minutes)d minutes"); + } else if (minutes <= 40) { + text = gettext("40 minutes"); + } else if (minutes <= 50) { + text = gettext("50 minutes"); + } else { + text = gettext("1 hour"); + } + } else { + // only seconds + if (seconds < 30) { + text = gettext("a couple of seconds"); + } else { + text = gettext("less than a minute"); + } + } + + return _.sprintf(text, replacements); +} + function formatDate(unixTimestamp) { if (!unixTimestamp) return "-"; return moment.unix(unixTimestamp).format(gettext(/* L10N: Date format */ "YYYY-MM-DD HH:mm")); diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 5db73bee..750c7dec 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -559,7 +559,7 @@ $(function() { } } } - output += gettext("Estimated Print Time") + ": " + formatDuration(data["gcodeAnalysis"]["estimatedPrintTime"]) + "
"; + output += gettext("Estimated Print Time") + ": " + formatFuzzyPrintTime(data["gcodeAnalysis"]["estimatedPrintTime"]) + "
"; } if (data["prints"] && data["prints"]["last"]) { output += gettext("Last Printed") + ": " + formatTimeAgo(data["prints"]["last"]["date"]) + "
"; diff --git a/src/octoprint/static/js/app/viewmodels/gcode.js b/src/octoprint/static/js/app/viewmodels/gcode.js index c7053a72..922d7977 100644 --- a/src/octoprint/static/js/app/viewmodels/gcode.js +++ b/src/octoprint/static/js/app/viewmodels/gcode.js @@ -433,7 +433,7 @@ $(function() { } else { var output = []; output.push(gettext("Model size") + ": " + model.width.toFixed(2) + "mm × " + model.depth.toFixed(2) + "mm × " + model.height.toFixed(2) + "mm"); - output.push(gettext("Estimated total print time") + ": " + formatFuzzyEstimation(model.printTime)); + output.push(gettext("Estimated total print time") + ": " + formatFuzzyPrintTime(model.printTime)); output.push(gettext("Estimated layer height") + ": " + model.layerHeight.toFixed(2) + gettext("mm")); output.push(gettext("Layer count") + ": " + model.layersPrinted.toFixed(0) + " " + gettext("printed") + ", " + model.layersTotal.toFixed(0) + " " + gettext("visited")); @@ -471,7 +471,7 @@ $(function() { } } } - output.push(gettext("Print time for layer") + ": " + formatFuzzyEstimation(layer.printTime)); + output.push(gettext("Print time for layer") + ": " + formatFuzzyPrintTime(layer.printTime)); self.ui_layerInfo(output.join("
")); diff --git a/src/octoprint/static/js/app/viewmodels/printerstate.js b/src/octoprint/static/js/app/viewmodels/printerstate.js index 411d1248..08747b10 100644 --- a/src/octoprint/static/js/app/viewmodels/printerstate.js +++ b/src/octoprint/static/js/app/viewmodels/printerstate.js @@ -53,9 +53,9 @@ $(function() { self.estimatedPrintTimeString = ko.pureComputed(function() { if (self.lastPrintTime()) - return formatFuzzyEstimation(self.lastPrintTime()); + return formatFuzzyPrintTime(self.lastPrintTime()); if (self.estimatedPrintTime()) - return formatFuzzyEstimation(self.estimatedPrintTime()); + return formatFuzzyPrintTime(self.estimatedPrintTime()); return "-"; }); self.byteString = ko.pureComputed(function() { @@ -79,17 +79,17 @@ $(function() { if (!self.printTime() || !(self.isPrinting() || self.isPaused())) { return "-"; } else { - return gettext("Calculating..."); + return gettext("Still stabilizing..."); } } else { - return formatFuzzyEstimation(self.printTimeLeft()); + return formatFuzzyPrintTime(self.printTimeLeft()); } }); self.printTimeLeftOriginString = ko.pureComputed(function() { var value = self.printTimeLeftOrigin(); switch (value) { case "linear": { - return gettext("Based on a linear approximation (accuracy highly dependent on the model)"); + return gettext("Based on a linear approximation (very low accuracy, especially at the beginning of the print)"); } case "analysis": { return gettext("Based on the estimate from analysis of file (medium accuracy)"); diff --git a/src/octoprint/templates/sidebar/state.jinja2 b/src/octoprint/templates/sidebar/state.jinja2 index a0200c16..d330d337 100644 --- a/src/octoprint/templates/sidebar/state.jinja2 +++ b/src/octoprint/templates/sidebar/state.jinja2 @@ -1,15 +1,16 @@ -{{ _('Machine State') }}:
+{{ _('State') }}:

-{{ _('File') }}:  (SD)
-{{ _('Timelapse') }}:
+{{ _('File') }}:  (SD)
+{{ _('Timelapse') }}:
-
+
-{{ _('Approx. Total Print Time') }}:
+{{ _('Approx. Total Print Time') }}:

-{{ _('Print Time') }}:
-{{ _('Print Time Left') }}:
-{{ _('Printed') }}:
+{{ _('Print Time') }}:
+{{ _('Print Time Left') }}:
+{{ _('Printed') }}:
+
diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index be0b17c2..3140d68b 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -1674,6 +1674,10 @@ class MachineCom(object): with self._sendNextLock: while self._active: # we loop until we've actually enqueued a line for sending + if self._state != self.STATE_PRINTING: + # we are no longer printing, return false + return False + line = self._getNext() if line is None: # end of file, return false