From fb2b44f3c13f6d903dfb16dee18e3ec4c44c209d Mon Sep 17 00:00:00 2001 From: Salandora Date: Mon, 7 Sep 2015 10:32:28 +0200 Subject: [PATCH 01/25] Changes to the API to enchance folder support --- src/octoprint/filemanager/storage.py | 20 +++- src/octoprint/server/api/files.py | 137 +++++++++++++++++---------- 2 files changed, 106 insertions(+), 51 deletions(-) diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 9f238cf9..273107ff 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -977,13 +977,29 @@ class LocalFileStorage(StorageInterface): # folder recursion elif os.path.isdir(entry_path) and recursive: - sub_result = self._list_folder(entry_path, filter=filter) - result[entry] = dict( + sub_result = self._list_folder(entry_path, filter=filter, recursive=recursive) + entry_data = dict( name=entry, type="folder", children=sub_result ) + if not filter or filter(entry, entry_data): + def get_size(start_path): + total_size = 0 + for dirpath, dirnames, filenames in os.walk(start_path): + for f in filenames: + fp = os.path.join(dirpath, f) + total_size += os.path.getsize(fp) + return total_size + + # only add folders passing the optional filter + extended_entry_data = dict() + extended_entry_data.update(entry_data) + extended_entry_data["size"] = get_size(entry_path) + + result[entry] = extended_entry_data + # TODO recreate links if we have metadata less entries # save metadata diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index 33ae4efd..2804f5d5 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -26,10 +26,16 @@ import psutil @api.route("/files", methods=["GET"]) def readGcodeFiles(): filter = None + recursive = False if "filter" in request.values: filter = request.values["filter"] - files = _getFileList(FileDestinations.LOCAL, filter=filter) + + if "recursive" in request.values: + recursive = request.values["recursive"] == 'true' + + files = _getFileList(FileDestinations.LOCAL, filter=filter, recursive=recursive) files.extend(_getFileList(FileDestinations.SDCARD)) + usage = psutil.disk_usage(settings().getBaseFolder("uploads")) return jsonify(files=files, free=usage.free, total=usage.total) @@ -39,7 +45,11 @@ def readGcodeFilesForOrigin(origin): if origin not in [FileDestinations.LOCAL, FileDestinations.SDCARD]: return make_response("Unknown origin: %s" % origin, 404) - files = _getFileList(origin) + recursive = False + if "recursive" in request.values: + recursive = request.values["recursive"] == 'true' + + files = _getFileList(origin, recursive=recursive) if origin == FileDestinations.LOCAL: usage = psutil.disk_usage(settings().getBaseFolder("uploads")) @@ -48,15 +58,24 @@ def readGcodeFilesForOrigin(origin): return jsonify(files=files) -def _getFileDetails(origin, filename): - files = _getFileList(origin) - for file in files: - if file["name"] == filename: - return file - return None +def _getFileDetails(origin, path): + files = _getFileList(origin, recursive=True) + path = path.split('/') + + def recursive_get_filedetails(files, path): + for file in files: + if file["name"] == path[0]: + if len(path) > 1: + return recursive_get_filedetails(file["children"], path[1:]) + else: + return file + + return None + + return recursive_get_filedetails(files, path) -def _getFileList(origin, filter=None): +def _getFileList(origin, filter=None, recursive=False): if origin == FileDestinations.SDCARD: sdFileList = printer.get_sd_files() @@ -78,45 +97,56 @@ def _getFileList(origin, filter=None): filter_func = None if filter: filter_func = lambda entry, entry_data: octoprint.filemanager.valid_file_type(entry, type=filter) - files = fileManager.list_files(origin, filter=filter_func, recursive=False)[origin].values() - for file in files: - file["origin"] = FileDestinations.LOCAL - if "analysis" in file and octoprint.filemanager.valid_file_type(file["name"], type="gcode"): - file["gcodeAnalysis"] = file["analysis"] - del file["analysis"] + files = fileManager.list_files(origin, filter=filter_func, recursive=recursive)[origin].values() - if "history" in file and octoprint.filemanager.valid_file_type(file["name"], type="gcode"): - # convert print log - history = file["history"] - del file["history"] - success = 0 - failure = 0 - last = None - for entry in history: - success += 1 if "success" in entry and entry["success"] else 0 - failure += 1 if "success" in entry and not entry["success"] else 0 - if not last or ("timestamp" in entry and "timestamp" in last and entry["timestamp"] > last["timestamp"]): - last = entry - if last: - prints = dict( - success=success, - failure=failure, - last=dict( - success=last["success"], - date=last["timestamp"] + def recursive_analysis(files, path): + for file in files: + file["origin"] = FileDestinations.LOCAL + + if file["type"] == "folder": + file["children"] = recursive_analysis(file["children"].values(), path + file["name"] + "/") + + if "analysis" in file and octoprint.filemanager.valid_file_type(file["name"], type="gcode"): + file["gcodeAnalysis"] = file["analysis"] + del file["analysis"] + + if "history" in file and octoprint.filemanager.valid_file_type(file["name"], type="gcode"): + # convert print log + history = file["history"] + del file["history"] + success = 0 + failure = 0 + last = None + for entry in history: + success += 1 if "success" in entry and entry["success"] else 0 + failure += 1 if "success" in entry and not entry["success"] else 0 + if not last or ("timestamp" in entry and "timestamp" in last and entry["timestamp"] > last["timestamp"]): + last = entry + if last: + prints = dict( + success=success, + failure=failure, + last=dict( + success=last["success"], + date=last["timestamp"] + ) ) - ) - if "printTime" in last: - prints["last"]["printTime"] = last["printTime"] - file["prints"] = prints + if "printTime" in last: + prints["last"]["printTime"] = last["printTime"] + file["prints"] = prints + + file.update({ + "refs": { + "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=path + file["name"], _external=True), + "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + path + file["name"] + } + }) + + return files + + files = recursive_analysis(files, "") - file.update({ - "refs": { - "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=file["name"], _external=True), - "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + file["name"] - } - }) return files @@ -166,24 +196,33 @@ def uploadGcodeFile(target): # determine current job currentFilename = None + currentFullPath = None currentOrigin = None currentJob = printer.get_current_job() if currentJob is not None and "file" in currentJob.keys(): currentJobFile = currentJob["file"] - if "name" in currentJobFile.keys() and "origin" in currentJobFile.keys(): - currentFilename = currentJobFile["name"] + if currentJobFile is not None and "name" in currentJobFile.keys() and "origin" in currentJobFile.keys() and currentJobFile["name"] is not None and currentJobFile["origin"] is not None: + currentPath, currentFilename = fileManager.sanitize(currentJobFile["origin"], currentJobFile["name"]) + currentFullPath = fileManager.join_path(target, currentPath, currentFilename) currentOrigin = currentJobFile["origin"] # determine future filename of file to be uploaded, abort if it can't be uploaded try: - futureFilename = fileManager.sanitize_name(FileDestinations.LOCAL, upload.filename) + futurePath, futureFilename = fileManager.sanitize(target, upload.filename) except: + futurePath = None futureFilename = None + if futureFilename is None: return make_response("Can not upload file %s, wrong format?" % upload.filename, 415) + if "path" in request.values: + futurePath = fileManager.sanitize_path(target, request.values["path"]) + + futureFullPath = fileManager.join_path(target, futurePath, futureFilename) + # prohibit overwriting currently selected file while it's being printed - if futureFilename == currentFilename and target == currentOrigin and printer.is_printing() or printer.is_paused(): + if futureFullPath == currentFullPath and target == currentOrigin and printer.is_printing() or printer.is_paused(): return make_response("Trying to overwrite file that is currently being printed: %s" % currentFilename, 409) def fileProcessingFinished(filename, absFilename, destination): @@ -212,7 +251,7 @@ def uploadGcodeFile(target): if octoprint.filemanager.valid_file_type(added_file, "gcode") and (selectAfterUpload or printAfterSelect or (currentFilename == filename and currentOrigin == destination)): printer.select_file(absFilename, destination == FileDestinations.SDCARD, printAfterSelect) - added_file = fileManager.add_file(FileDestinations.LOCAL, upload.filename, upload, allow_overwrite=True) + added_file = fileManager.add_file(FileDestinations.LOCAL, futureFullPath, upload, allow_overwrite=True) if added_file is None: return make_response("Could not upload the file %s" % upload.filename, 500) if octoprint.filemanager.valid_file_type(added_file, "stl"): From 5f3ee7dfdabca37f3020e2c95cb7b888645e9d4f Mon Sep 17 00:00:00 2001 From: Salandora Date: Mon, 7 Sep 2015 10:34:47 +0200 Subject: [PATCH 02/25] Changes to the WebUI to support folders --- src/octoprint/static/js/app/main.js | 6 +- .../static/js/app/viewmodels/files.js | 147 +++++++++++++++--- src/octoprint/templates/sidebar/files.jinja2 | 29 +++- .../templates/sidebar/files_header.jinja2 | 4 + 4 files changed, 159 insertions(+), 27 deletions(-) diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index c1237505..753f371d 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -223,11 +223,15 @@ $(function() { }, update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { setTimeout(function() { - $(element).slimScroll({scrollBy: 0}); + if (element.nodeName == "#comment") + $(element.parentElement).slimScroll({scrollBy: 0}); + else + $(element).slimScroll({scrollBy: 0}); }, 10); return ko.bindingHandlers.foreach.update(element, valueAccessor(), allBindings, viewModel, bindingContext); } }; + ko.virtualElements.allowedBindings.slimScrolledForeach = true; ko.bindingHandlers.qrcode = { update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 885f5a40..6e3522e4 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -54,6 +54,10 @@ $(function() { self.uploadButton = undefined; + self.allItems = ko.observable(undefined); + self.listStyle = ko.observable("folders_files"); + self.currentPath = ko.observable(""); + // initialize list helper self.listHelper = new ItemListHelper( "gcodeFiles", @@ -78,28 +82,42 @@ $(function() { } }, { - "printed": function(file) { - return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0); + "printed": function(data) { + return !(data["prints"] && data["prints"]["success"] && data["prints"]["success"] > 0) || (data["type"] && data["type"] == "folder"); }, - "sd": function(file) { - return file["origin"] && file["origin"] == "sdcard"; + "sd": function(data) { + return data["origin"] && data["origin"] == "sdcard"; }, - "local": function(file) { - return !(file["origin"] && file["origin"] == "sdcard"); + "local": function(data) { + return !(data["origin"] && data["origin"] == "sdcard"); }, - "machinecode": function(file) { - return file["type"] && file["type"] == "machinecode"; + "machinecode": function(data) { + return data["type"] && (data["type"] == "machinecode" || data["type"] == "folder"); }, - "model": function(file) { - return file["type"] && file["type"] == "model"; + "model": function(data) { + return data["type"] && (data["type"] == "model" || data["type"] == "folder"); + }, + "emptyFolder": function(data) { + return data["type"] && data["type"] != "folder" || (data["size"] && data["size"] != 0); } }, "name", - [], + ["emptyFolder"], [["sd", "local"], ["machinecode", "model"]], 0 ); + self.foldersOnlyList = ko.dependentObservable(function() { + filter = function(data) { return data["type"] && data["type"] == "folder"; }; + var items = _.filter(self.listHelper.paginatedItems(), filter); + return items; + }); + self.filesOnlyList = ko.dependentObservable(function() { + filter = function(data) { return data["type"] && data["type"] != "folder"; }; + var items = _.filter(self.listHelper.paginatedItems(), filter); + return items; + }); + self.isLoadActionPossible = ko.computed(function() { return self.loginState.isUser() && !self.isPrinting() && !self.isPaused() && !self.isLoading(); }); @@ -142,7 +160,7 @@ $(function() { }; self._otherRequestInProgress = false; - self.requestData = function(filenameToFocus, locationToFocus) { + self.requestData = function(filenameToFocus, locationToFocus, switchToPath) { if (self._otherRequestInProgress) return; self._otherRequestInProgress = true; @@ -150,8 +168,9 @@ $(function() { url: API_BASEURL + "files", method: "GET", dataType: "json", + data: {"recursive": true}, success: function(response) { - self.fromResponse(response, filenameToFocus, locationToFocus); + self.fromResponse(response, filenameToFocus, locationToFocus, switchToPath); self._otherRequestInProgress = false; }, error: function() { @@ -160,13 +179,32 @@ $(function() { }); }; - self.fromResponse = function(response, filenameToFocus, locationToFocus) { + self.fromResponse = function(response, filenameToFocus, locationToFocus, switchToPath) { var files = response.files; - _.each(files, function(element, index, list) { + recursiveCheck = function(element, index, list) { + if (!element.hasOwnProperty("parent")) element.parent = { children: list, parent: undefined }; if (!element.hasOwnProperty("size")) element.size = undefined; if (!element.hasOwnProperty("date")) element.date = undefined; - }); - self.listHelper.updateItems(files); + + if (element.type == "folder") + { + for (var i = 0; i < element.children.length; i++) { + element.children[i].parent = element; + recursiveCheck(element.children[i], i, element.children); + } + } + }; + _.each(files, recursiveCheck); + + self.allItems(files); + self.currentPath(""); + self.listHelper.addFilter("emptyFolder"); + if (!switchToPath) { + self.listHelper.updateItems(files); + } + else { + self.changeFolderByPath(switchToPath); + } if (filenameToFocus) { // got a file to scroll to @@ -191,6 +229,52 @@ $(function() { self.highlightFilename(self.printerState.filename()); }; + self.changeFolder = function(data) { + self.currentPath(self.pathByElement(data)); + self.listHelper.updateItems(data.children); + }; + self.changeFolderByPath = function(path) { + var element = self.elementByPath(path, { children: self.allItems() }); + if (element) { + self.currentPath(path); + self.listHelper.updateItems(element.children); + } + else{ + self.currentPath(""); + self.listHelper.updateItems(self.allItems()); + } + } + + self.pathByElement = function(element) { + if (!element || element.parent == undefined) + return ""; + + recursivePath = function(element, path) { + if (element.parent !== undefined) + return recursivePath(element.parent, element.name + "/" + path); + + return path; + }; + return recursivePath(element.parent, element.name); + }; + self.elementByPath = function(path, startElement) { + recursiveSearch = function(path, element) { + if (path.length == 0) + return element; + + var name = path.shift(); + for(var i = 0; i< startElement.children.length; i++) { + if (name == startElement.children[i].name) { + return recursivePath(path, startElement.children[i]); + } + } + + return undefined; + }; + + return recursiveSearch(path.split("/"), startElement); + }; + self.loadFile = function(file, printAfterLoad) { if (!file || !file.refs || !file.refs.hasOwnProperty("resource")) return; @@ -206,11 +290,22 @@ $(function() { self.removeFile = function(file) { if (!file || !file.refs || !file.refs.hasOwnProperty("resource")) return; + var index = self.listHelper.paginatedItems().indexOf(file) + 1; + if (index >= self.listHelper.paginatedItems().length) + index = index - 2; + if (index < 0) + index = 0; + + var filenameToFocus = undefined; + var fileToFocus = self.listHelper.paginatedItems()[index]; + if (fileToFocus) + filenameToFocus = fileToFocus.name; + $.ajax({ url: file.refs.resource, type: "DELETE", success: function() { - self.requestData(); + self.requestData(undefined, filenameToFocus, self.pathByElement(file.parent)); } }); }; @@ -352,7 +447,7 @@ $(function() { }; self.onDataUpdaterReconnect = function() { - self.requestData(); + self.requestData(undefined, undefined, self.currentPath()); }; self.onUserLoggedIn = function(user) { @@ -397,7 +492,7 @@ $(function() { filename = data.result.files.local.name; location = "local"; } - self.requestData(filename, location); + self.requestData(filename, location, self.currentPath()); if (_.endsWith(filename.toLowerCase(), ".stl")) { self.slicing.show(location, filename); @@ -442,6 +537,8 @@ $(function() { done: gcode_upload_done, fail: gcode_upload_fail, progressall: gcode_upload_progress + }).bind('fileuploadsubmit', function(e, data) { + data.formData = { path: self.currentPath() }; }); } @@ -464,6 +561,8 @@ $(function() { done: gcode_upload_done, fail: gcode_upload_fail, progressall: gcode_upload_progress + }).bind('fileuploadsubmit', function(e, data) { + data.formData = { path: self.currentPath() }; }); } @@ -585,20 +684,20 @@ $(function() { self.onEventUpdatedFiles = function(payload) { if (payload.type == "gcode") { - self.requestData(); + self.requestData(undefined, undefined, self.currentPath()); } }; self.onEventSlicingDone = function(payload) { - self.requestData(); + self.requestData(undefined, undefined, self.currentPath()); }; self.onEventMetadataAnalysisFinished = function(payload) { - self.requestData(); + self.requestData(undefined, undefined, self.currentPath()); }; self.onEventMetadataStatisticsUpdated = function(payload) { - self.requestData(); + self.requestData(undefined, undefined, self.currentPath()); }; } diff --git a/src/octoprint/templates/sidebar/files.jinja2 b/src/octoprint/templates/sidebar/files.jinja2 index 39b4d9c8..5b128027 100644 --- a/src/octoprint/templates/sidebar/files.jinja2 +++ b/src/octoprint/templates/sidebar/files.jinja2 @@ -1,8 +1,32 @@ -
+
+ +
{{ _('Back') }}
+ + + +
+ + +
+ + + + +
+ + +
+ + + + +
+ +
diff --git a/src/octoprint/templates/sidebar/files_header.jinja2 b/src/octoprint/templates/sidebar/files_header.jinja2 index 3a4810a0..1e7eebc7 100644 --- a/src/octoprint/templates/sidebar/files_header.jinja2 +++ b/src/octoprint/templates/sidebar/files_header.jinja2 @@ -3,6 +3,10 @@