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') }}: