diff --git a/docs/api/datamodel.rst b/docs/api/datamodel.rst index 248c0eb1..076e7dbe 100644 --- a/docs/api/datamodel.rst +++ b/docs/api/datamodel.rst @@ -286,9 +286,9 @@ Files - Type - Description * - ``hash`` - - 1 + - 0..1 - String - - MD5 hash of the file + - MD5 hash of the file. Only available for ``local`` files. * - ``size`` - 0..1 - Number diff --git a/docs/api/files.rst b/docs/api/files.rst index e5e79dc5..76b80132 100644 --- a/docs/api/files.rst +++ b/docs/api/files.rst @@ -16,9 +16,14 @@ Retrieve all files Retrieve information regarding all files currently available and regarding the disk space still available locally in the system. + By default only returns the files and folders in the root directory. If the query parameter ``recursive`` + is provided and set to ``true``, returns all files and folders. + Returns a :ref:`Retrieve response `. - **Example**: + **Example 1**: + + Fetch only the files and folders from the root folder. .. sourcecode:: http @@ -35,6 +40,10 @@ Retrieve all files "files": [ { "name": "whistle_v2.gcode", + "path": "whistle_v2.gcode", + "type": "machinecode", + "typePath": ["machinecode", "gcode"], + "hash": "...", "size": 1468987, "date": 1378847754, "origin": "local", @@ -60,15 +69,140 @@ Retrieve all files }, { "name": "whistle_.gco", + "path": "whistle_.gco", + "type": "machinecode", + "typePath": ["machinecode", "gcode"], "origin": "sdcard", "refs": { "resource": "http://example.com/api/files/sdcard/whistle_.gco" } + }, + { + "name": "folderA", + "path": "folderA", + "type": "folder", + "typePath": ["folder"], + "children": [], + "size": 1334 + } + ], + "free": "3.2GB" + } + + **Example 2** + + Recursively fetch all files and folders. + + Fetch only the files and folders from the root folder. + + .. sourcecode:: http + + GET /api/files?recursive=true HTTP/1.1 + Host: example.com + X-Api-Key: abcdef... + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "files": [ + { + "name": "whistle_v2.gcode", + "path": "whistle_v2.gcode", + "type": "machinecode", + "typePath": ["machinecode", "gcode"], + "hash": "...", + "size": 1468987, + "date": 1378847754, + "origin": "local", + "refs": { + "resource": "http://example.com/api/files/local/whistle_v2.gcode", + "download": "http://example.com/downloads/files/local/whistle_v2.gcode" + }, + "gcodeAnalysis": { + "estimatedPrintTime": 1188, + "filament": { + "length": 810, + "volume": 5.36 + } + }, + "print": { + "failure": 4, + "success": 23, + "last": { + "date": 1387144346, + "success": true + } + } + }, + { + "name": "whistle_.gco", + "path": "whistle_.gco", + "type": "machinecode", + "typePath": ["machinecode", "gcode"], + "origin": "sdcard", + "refs": { + "resource": "http://example.com/api/files/sdcard/whistle_.gco" + } + }, + { + "name": "folderA", + "path": "folderA", + "type": "folder", + "typePath": ["folder"], + "children": [ + { + "name": "test.gcode", + "path": "folderA/test.gcode", + "type": "machinecode", + "typePath": ["machinecode", "gcode"], + "hash": "...", + "size": 1234, + "date": 1378847754, + "origin": "local", + "refs": { + "resource": "http://example.com/api/files/local/folderA/test.gcode", + "download": "http://example.com/downloads/files/local/folderA/test.gcode" + } + }, + { + "name": "subfolder", + "path": "folderA/subfolder", + "type": "folder", + "typePath": ["folder"], + "children": [ + { + "name": "test.gcode", + "path": "folderA/subfolder/test2.gcode", + "type": "machinecode", + "typePath": ["machinecode", "gcode"], + "hash": "...", + "size": 100, + "date": 1378847754, + "origin": "local", + "refs": { + "resource": "http://example.com/api/files/local/folderA/subfolder/test2.gcode", + "download": "http://example.com/downloads/files/local/folderA/subfolder/test2.gcode" + } + }, + ], + "size": 100, + "refs": { + "resource": "http://example.com/api/files/local/folderA/subfolder", + } + ], + "size": 1334, + "refs": { + "resource": "http://example.com/api/files/local/folderA", + } } ], "free": "3.2GB" } + :param recursive: If set to ``true``, return all files and folders recursively. Otherwise only return items on same level. :statuscode 200: No error .. _sec-api-fileops-retrievelocation: @@ -81,6 +215,9 @@ Retrieve files from specific location Retrieve information regarding the files currently available on the selected `location` and -- if targeting the ``local`` location -- regarding the disk space still available locally in the system. + By default only returns the files and folders in the root directory. If the query parameter ``recursive`` + is provided and set to ``true``, returns all files and folders. + Returns a :ref:`Retrieve response `. **Example**: @@ -100,6 +237,10 @@ Retrieve files from specific location "files": [ { "name": "whistle_v2.gcode", + "path": "whistle_v2.gcode", + "type": "machinecode", + "typePath": ["machinecode", "gcode"], + "hash": "...", "size": 1468987, "date": 1378847754, "origin": "local", @@ -130,6 +271,7 @@ Retrieve files from specific location :param location: The origin location from which to retrieve the files. Currently only ``local`` and ``sdcard`` are supported, with ``local`` referring to files stored in OctoPrint's ``uploads`` folder and ``sdcard`` referring to files stored on the printer's SD card (if available). + :param recursive: If set to ``true``, return all files and folders recursively. Otherwise only return items on same level. :statuscode 200: No error :statuscode 404: If `location` is neither ``local`` nor ``sdcard`` @@ -196,6 +338,8 @@ Upload file or create folder "local": { "name": "whistle_v2.gcode", "path": "whistle_v2.gcode", + "type": "machinecode", + "typePath": ["machinecode", "gcode"], "origin": "local", "refs": { "resource": "http://example.com/api/files/local/whistle_v2.gcode", @@ -324,6 +468,9 @@ Retrieve a specific file's or folder's information If the file is unknown, a :http:statuscode:`404` is returned. + If the targeted path is a folder, by default only its direct children will be returned. If ``recursive`` is + provided and set to ``true``, all sub folders and their children will be returned too. + On success, a :http:statuscode:`200` is returned, with a :ref:`file information item ` as the response body. @@ -368,7 +515,7 @@ Retrieve a specific file's or folder's information :param location: The location of the file for which to retrieve the information, either ``local`` or ``sdcard``. :param filename: The filename of the file for which to retrieve the information - :param children: Whether to include children of folders (true, default) or not (false) + :param recursive: If set to ``true``, return all files and folders recursively. Otherwise only return items on same level. :statuscode 200: No error :statuscode 404: If ``target`` is neither ``local`` nor ``sdcard``, ``sdcard`` but SD card support is disabled or the requested file was not found diff --git a/scripts/octoprint.init b/scripts/octoprint.init index 97601ebb..90cc936b 100644 --- a/scripts/octoprint.init +++ b/scripts/octoprint.init @@ -25,6 +25,10 @@ SCRIPTNAME=/etc/init.d/$PKGNAME # Read configuration variable file if it is present [ -r /etc/default/$PKGNAME ] && . /etc/default/$PKGNAME +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + # Exit if the DAEMON is not set if [ -z "$DAEMON" ] then @@ -38,10 +42,6 @@ fi # Load the VERBOSE setting and other rcS variables [ -f /etc/default/rcS ] && . /etc/default/rcS -# Define LSB log_* functions. -# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. -. /lib/lsb/init-functions - if [ -z "$START" -o "$START" != "yes" ] then log_warning_msg "Not starting $PKGNAME, edit /etc/default/$PKGNAME to start it." diff --git a/setup.py b/setup.py index 41e9a56d..6c0ac056 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,8 @@ INSTALL_REQUIRES = [ "feedparser>=5.2.1,<5.3", "chainmap>=1.0.2,<1.1", "future>=0.15,<0.16", - "scandir>=1.3,<1.4" + "scandir>=1.3,<1.4", + "websocket-client>=0.40,<0.41" ] # Additional requirements for optional install options diff --git a/src/octoprint/cli/__init__.py b/src/octoprint/cli/__init__.py index 826ebbc0..91c6b55b 100644 --- a/src/octoprint/cli/__init__.py +++ b/src/octoprint/cli/__init__.py @@ -129,7 +129,7 @@ from .config import config_commands sources=[server_commands, plugin_commands, dev_commands, client_commands, config_commands]) @standard_options() @legacy_options -@click.version_option(version=octoprint.__version__) +@click.version_option(version=octoprint.__version__, allow_from_autoenv=False) @click.pass_context def octo(ctx, debug, host, port, logging, daemon, pid, allow_root): @@ -146,7 +146,7 @@ def octo(ctx, debug, host, port, logging, daemon, pid, allow_root): "\"octoprint daemon start|stop|restart\" from now on") from octoprint.cli.server import daemon_command - ctx.invoke(daemon_command, debug=debug, pid=pid, daemon=daemon, allow_root=allow_root) + ctx.invoke(daemon_command, debug=debug, host=host, port=port, logging=logging, allow_root=allow_root, command=daemon, pid=pid) else: click.echo("Starting the server via \"octoprint\" is deprecated, " "please use \"octoprint serve\" from now on.") diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 1c11f42c..f26a21ae 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -511,7 +511,7 @@ class LocalFileStorage(StorageInterface): filepath = self.sanitize_path(filepath) path = self.sanitize_path(path) - return filepath.startswith(path) + return filepath == path or filepath.startswith(path + "/") def file_exists(self, path): path, name = self.sanitize(path) @@ -1123,11 +1123,16 @@ class LocalFileStorage(StorageInterface): # no hidden files and folders continue - entry_name = entry.name - entry_path = entry.path - entry_is_file = entry.is_file() - entry_is_dir = entry.is_dir() - entry_stat = entry.stat() + try: + entry_name = entry.name + entry_path = entry.path + entry_is_file = entry.is_file() + entry_is_dir = entry.is_dir() + entry_stat = entry.stat() + except: + # error while trying to fetch file metadata, that might be thanks to file already having + # been moved or deleted - ignore it and continue + continue try: new_entry_name, new_entry_path = self._sanitize_entry(entry_name, path, entry_path) diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index df030186..407395bd 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -125,7 +125,8 @@ def getSettings(): "pollWatched": s.getBoolean(["feature", "pollWatched"]), "ignoreIdenticalResends": s.getBoolean(["feature", "ignoreIdenticalResends"]), "modelSizeDetection": s.getBoolean(["feature", "modelSizeDetection"]), - "firmwareDetection": s.getBoolean(["feature", "firmwareDetection"]) + "firmwareDetection": s.getBoolean(["feature", "firmwareDetection"]), + "printCancelConfirmation": s.getBoolean(["feature", "printCancelConfirmation"]) }, "serial": { "port": connectionOptions["portPreference"], @@ -309,6 +310,7 @@ def _saveSettings(data): if "ignoreIdenticalResends" in data["feature"]: s.setBoolean(["feature", "ignoreIdenticalResends"], data["feature"]["ignoreIdenticalResends"]) if "modelSizeDetection" in data["feature"]: s.setBoolean(["feature", "modelSizeDetection"], data["feature"]["modelSizeDetection"]) if "firmwareDetection" in data["feature"]: s.setBoolean(["feature", "firmwareDetection"], data["feature"]["firmwareDetection"]) + if "printCancelConfirmation" in data["feature"]: s.setBoolean(["feature", "printCancelConfirmation"], data["feature"]["printCancelConfirmation"]) if "serial" in data.keys(): if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"]) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index f2302e54..db6acf60 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -1297,6 +1297,7 @@ def collect_core_assets(enable_gcodeviewer=True, preferred_stylesheet="css"): 'js/app/bindings/slimscrolledforeach.js', 'js/app/bindings/toggle.js', 'js/app/bindings/togglecontent.js', + 'js/app/bindings/valuewithinit.js', 'js/app/viewmodels/appearance.js', 'js/app/viewmodels/connection.js', 'js/app/viewmodels/control.js', diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index d43de3b6..8775a93f 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -200,7 +200,8 @@ default_settings = { "identicalResendsCountdown": 7, "supportFAsCommand": False, "modelSizeDetection": True, - "firmwareDetection": True + "firmwareDetection": True, + "printCancelConfirmation": True }, "folder": { "uploads": None, diff --git a/src/octoprint/static/js/app/bindings/valuewithinit.js b/src/octoprint/static/js/app/bindings/valuewithinit.js new file mode 100644 index 00000000..dce55826 --- /dev/null +++ b/src/octoprint/static/js/app/bindings/valuewithinit.js @@ -0,0 +1,11 @@ +ko.bindingHandlers.valueWithInit = { + init: function(element, valueAccessor, allBindingsAccessor, context) { + var observable = valueAccessor(); + var value = element.value; + + observable(value); + + ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor, context); + }, + update: ko.bindingHandlers.value.update +}; \ No newline at end of file diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index 86221ca4..80b1701e 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -478,6 +478,17 @@ $(function() { // reload overlay $("#reloadui_overlay_reload").click(function() { location.reload(); }); + var changeTab = function() + { + var hashtag = window.location.hash; + var tab = $('#tabs a[href="' + hashtag + '"]'); + if (tab.length) + { + tab.tab("show"); + onTabChange(hashtag); + } + } + //~~ view model binding var bindViewModels = function() { @@ -566,6 +577,15 @@ $(function() { callViewModels(allViewModels, "onBrowserTabVisibilityChange", [status]); }); + $(window).on("hashchange", function() { + changeTab(); + }); + + if (window.location.hash != "") + { + changeTab(); + } + log.info("Application startup complete"); }; diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 24695b2d..2563ebe8 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -708,11 +708,12 @@ $(function() { return false; } - if (entry["type"] == "folder" && entry["children"]) { + var success = entry["name"].toLocaleLowerCase().indexOf(query) > -1; + if (!success && entry["type"] == "folder" && entry["children"]) { return _.any(entry["children"], recursiveSearch); - } else { - return entry["name"].toLocaleLowerCase().indexOf(query) > -1; } + + return success; }; self.listHelper.changeSearchFunction(recursiveSearch); diff --git a/src/octoprint/static/js/app/viewmodels/loginstate.js b/src/octoprint/static/js/app/viewmodels/loginstate.js index 7a72a0cd..5b12fa77 100644 --- a/src/octoprint/static/js/app/viewmodels/loginstate.js +++ b/src/octoprint/static/js/app/viewmodels/loginstate.js @@ -76,6 +76,10 @@ $(function() { self.loginUser(""); self.loginPass(""); self.loginRemember(false); + + if (history && history.replaceState) { + history.replaceState({success: true}, document.title, window.location.pathname); + } }) .fail(function() { new PNotify({title: gettext("Login failed"), text: gettext("User unknown or wrong password"), type: "error"}); @@ -95,17 +99,18 @@ $(function() { }); }; - self.onLoginUserKeyup = function(data, event) { - if (event.keyCode == 13) { - self.elementPasswordInput.focus(); + self.prepareLogin = function(data, event) { + if(event && event.preventDefault) { + event.preventDefault(); } + self.login(); }; - self.onLoginPassKeyup = function(data, event) { - if (event.keyCode == 13) { - self.login(); + self.onKeyUp = function(data, event) { + if (event && event.keyCode == 13) { + $('#loginForm').submit(); } - }; + } self.onAllBound = function(allViewModels) { self.allViewModels = allViewModels; diff --git a/src/octoprint/static/js/app/viewmodels/printerstate.js b/src/octoprint/static/js/app/viewmodels/printerstate.js index 745d3551..075c2ba8 100644 --- a/src/octoprint/static/js/app/viewmodels/printerstate.js +++ b/src/octoprint/static/js/app/viewmodels/printerstate.js @@ -3,6 +3,7 @@ $(function() { var self = this; self.loginState = parameters[0]; + self.settings = parameters[1]; self.stateString = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined); @@ -288,18 +289,22 @@ $(function() { }; self.cancel = function() { - showConfirmationDialog({ - message: gettext("This will cancel your print."), - onproceed: function() { - OctoPrint.job.cancel(); - } - }); + if (!self.settings.feature_printCancelConfirmation()) { + OctoPrint.job.cancel(); + } else { + showConfirmationDialog({ + message: gettext("This will cancel your print."), + onproceed: function() { + OctoPrint.job.cancel(); + } + }); + }; }; } OCTOPRINT_VIEWMODELS.push([ PrinterStateViewModel, - ["loginStateViewModel"], + ["loginStateViewModel", "settingsViewModel"], ["#state_wrapper", "#drop_overlay"] ]); }); diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 43482ee1..7b9394e5 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -138,6 +138,7 @@ $(function() { self.feature_ignoreIdenticalResends = ko.observable(undefined); self.feature_modelSizeDetection = ko.observable(undefined); self.feature_firmwareDetection = ko.observable(undefined); + self.feature_printCancelConfirmation = ko.observable(undefined); self.serial_port = ko.observable(); self.serial_baudrate = ko.observable(); diff --git a/src/octoprint/templates/dialogs/settings/features.jinja2 b/src/octoprint/templates/dialogs/settings/features.jinja2 index 3d146167..1e7c8592 100644 --- a/src/octoprint/templates/dialogs/settings/features.jinja2 +++ b/src/octoprint/templates/dialogs/settings/features.jinja2 @@ -13,6 +13,13 @@ +
+
+ +
+
-