From 08de33e27d21715e6be2b6fc964c92107b3511ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 1 Jul 2016 17:51:21 +0200 Subject: [PATCH] Add path into file and folder entries & allow non recursive listings --- src/octoprint/filemanager/storage.py | 28 +++- src/octoprint/server/api/files.py | 144 +++++++++--------- .../static/js/app/viewmodels/files.js | 46 ++++-- 3 files changed, 131 insertions(+), 87 deletions(-) diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index 7552e5c1..c506f1a9 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -77,14 +77,20 @@ class StorageInterface(object): { "some_folder": { + "name": "some_folder", + "path": "some_folder", "type": "folder", "children": { "some_sub_folder": { + "name": "some_sub_folder", + "path": "some_folder/some_sub_folder", "type": "folder", "typePath": ["folder"], "children": { ... } }, "some_file.gcode": { + "name": "some_file.gcode", + "path": "some_folder/some_file.gcode", "type": "machinecode", "typePath": ["machinecode", "gcode"], "hash": "", @@ -94,6 +100,8 @@ class StorageInterface(object): ... } "test.gcode": { + "name": "test.gcode", + "path": "test.gcode", "type": "machinecode", "typePath": ["machinecode", "gcode"], "hash": "", @@ -101,6 +109,8 @@ class StorageInterface(object): ... }, "test.stl": { + "name": "test.stl", + "path": "test.stl", "type": "model", "typePath": ["model", "stl"], "hash": "", @@ -1023,7 +1033,7 @@ class LocalFileStorage(StorageInterface): if metadata_dirty: self._save_metadata(path, metadata) - def _list_folder(self, path, filter=None, recursive=True): + def _list_folder(self, path, base="", filter=None, recursive=True): metadata = self._get_metadata(path) if not metadata: metadata = dict() @@ -1036,6 +1046,7 @@ class LocalFileStorage(StorageInterface): continue entry_path = os.path.join(path, entry) + path_in_location = entry if not base else base + entry # file handling if os.path.isfile(entry_path): @@ -1059,6 +1070,7 @@ class LocalFileStorage(StorageInterface): extended_entry_data = dict() extended_entry_data.update(entry_data) extended_entry_data["name"] = entry + extended_entry_data["path"] = path_in_location extended_entry_data["type"] = file_type extended_entry_data["typePath"] = type_path stat = os.stat(entry_path) @@ -1069,14 +1081,17 @@ class LocalFileStorage(StorageInterface): result[entry] = extended_entry_data # folder recursion - elif os.path.isdir(entry_path) and recursive: - sub_result = self._list_folder(entry_path, filter=filter, recursive=recursive) + elif os.path.isdir(entry_path): entry_data = dict( name=entry, + path=path_in_location, type="folder", - type_path=["folder"], - children=sub_result + type_path=["folder"] ) + if recursive: + sub_result = self._list_folder(entry_path, base=path_in_location + "/", filter=filter, + recursive=recursive) + entry_data["children"] = sub_result if not filter or filter(entry, entry_data): def get_size(): @@ -1090,7 +1105,8 @@ class LocalFileStorage(StorageInterface): # only add folders passing the optional filter extended_entry_data = dict() extended_entry_data.update(entry_data) - extended_entry_data["size"] = get_size() + if recursive: + extended_entry_data["size"] = get_size() result[entry] = extended_entry_data diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index 74255c15..7aeb5554 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -43,7 +43,7 @@ def readGcodeFilesForOrigin(origin): recursive = False if "recursive" in request.values: - recursive = request.values["recursive"] == 'true' + recursive = request.values["recursive"] in valid_boolean_trues files = _getFileList(origin, recursive=recursive) @@ -54,47 +54,18 @@ def readGcodeFilesForOrigin(origin): return jsonify(files=files) -def _getFileDetails(origin, path): - files = _getFileList(origin, recursive=True) - path = path.split('/') - - if len(path) == 1: - # shortcut for files in the root folder - name = path[0] - for f in files: - if f["name"] == name: - return f +def _getFileDetails(origin, path, recursive=True): + parent, path = fileManager.split_path(origin, path) + files = _getFileList(origin, path=parent, recursive=recursive) + for f in files: + if f["name"] == path: + return f + else: return None - node = files - while path: - segment = path.pop(0) - for f in node: - if not f["name"] == segment: - # wrong name => next! - continue - if not path: - # no path left and name matches => found it! - return f - - if not f["type"] == "folder": - # path left but not a folder => that doesn't work - return None - - # we'll use this folder's children as the next iteration - node = f["children"] - break - else: - # nothing matched the name, we can't find it - return None - - # nothing returned until now => not found - return None - - -def _getFileList(origin, filter=None, recursive=False): +def _getFileList(origin, path=None, filter=None, recursive=False): if origin == FileDestinations.SDCARD: sdFileList = printer.get_sd_files() @@ -117,26 +88,33 @@ def _getFileList(origin, filter=None, recursive=False): 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=recursive)[origin].values() + files = fileManager.list_files(origin, path=path, filter=filter_func, recursive=recursive)[origin].values() def analyse_recursively(files, path=None): if path is None: path = "" - for file in files: - file["origin"] = FileDestinations.LOCAL + for file_or_folder in files: + file_or_folder["origin"] = FileDestinations.LOCAL - if file["type"] == "folder": - file["children"] = analyse_recursively(file["children"].values(), path + file["name"] + "/") + if file_or_folder["type"] == "folder": + if "children" in file_or_folder: + file_or_folder["children"] = analyse_recursively(file_or_folder["children"].values(), path + file_or_folder["name"] + "/") + + file_or_folder.update({ + "refs": { + "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=path + file_or_folder["name"], _external=True) + } + }) else: - if "analysis" in file and octoprint.filemanager.valid_file_type(file["name"], type="gcode"): - file["gcodeAnalysis"] = file["analysis"] - del file["analysis"] + if "analysis" in file_or_folder and octoprint.filemanager.valid_file_type(file_or_folder["name"], type="gcode"): + file_or_folder["gcodeAnalysis"] = file_or_folder["analysis"] + del file_or_folder["analysis"] - if "history" in file and octoprint.filemanager.valid_file_type(file["name"], type="gcode"): + if "history" in file_or_folder and octoprint.filemanager.valid_file_type(file_or_folder["name"], type="gcode"): # convert print log - history = file["history"] - del file["history"] + history = file_or_folder["history"] + del file_or_folder["history"] success = 0 failure = 0 last = None @@ -156,12 +134,12 @@ def _getFileList(origin, filter=None, recursive=False): ) if "printTime" in last: prints["last"]["printTime"] = last["printTime"] - file["prints"] = prints + file_or_folder["prints"] = prints - file.update({ + file_or_folder.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"] + "resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=path + file_or_folder["name"], _external=True), + "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + path + file_or_folder["name"] } }) @@ -251,7 +229,7 @@ def uploadGcodeFile(target): return make_response("Can not upload file %s, wrong format?" % upload.filename, 415) if "path" in request.values and request.values["path"]: - # FileDestinations.LOCAL = should normally be target, but can't because SDCard handling isn't implemented yet + # we currently only support uploads to sdcard via local, so first target is local instead of "target" futurePath = fileManager.sanitize_path(FileDestinations.LOCAL, request.values["path"]) # prohibit overwriting currently selected file while it's being printed @@ -284,7 +262,6 @@ 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) - # FileDestinations.LOCAL = should normally be target, but can't because SDCard handling isn't implemented yet futureFullPath = fileManager.join_path(FileDestinations.LOCAL, futurePath, futureFilename) try: @@ -316,7 +293,8 @@ def uploadGcodeFile(target): location = url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True) files.update({ FileDestinations.LOCAL: { - "name": filename, + "name": futureFilename, + "path": filename, "origin": FileDestinations.LOCAL, "refs": { "resource": location, @@ -330,6 +308,7 @@ def uploadGcodeFile(target): files.update({ FileDestinations.SDCARD: { "name": sdFilename, + "path": sdFilename, "origin": FileDestinations.SDCARD, "refs": { "resource": location @@ -340,6 +319,7 @@ def uploadGcodeFile(target): r = make_response(jsonify(files=files, done=done), 201) r.headers["Location"] = location return r + elif "foldername" in request.values: foldername = request.values["foldername"] @@ -350,29 +330,49 @@ def uploadGcodeFile(target): if not futureName or not futurePath: return make_response("Can't create a folder with an empty name", 400) + if "path" in request.values and request.values["path"]: + futurePath = fileManager.sanitize_path(FileDestinations.LOCAL, + request.values["path"]) + futureFullPath = fileManager.join_path(target, futurePath, futureName) if octoprint.filemanager.valid_file_type(futureName): return make_response("Can't create a folder named %s, please try another name" % futureName, 409) try: - fileManager.add_folder(target, futureFullPath) + added_folder = fileManager.add_folder(target, futureFullPath) except octoprint.filemanager.storage.StorageError as e: if e.code == octoprint.filemanager.storage.StorageError.INVALID_DIRECTORY: return make_response("Could not create folder {}, invalid directory".format(futureName)) else: return make_response("Could not create folder {}".format(futureName)) + + location = url_for(".readGcodeFile", + target=FileDestinations.LOCAL, + filename=added_folder, + _external=True) + folder = dict(name=futureName, + path=added_folder, + origin=target, + refs=dict(resource=location)) + + r = make_response(jsonify(folder=folder, done=True), 201) + r.headers["Location"] = location + return r + else: return make_response("No file to upload and no folder to create", 400) - return NO_CONTENT - @api.route("/files//", methods=["GET"]) def readGcodeFile(target, filename): if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]: return make_response("Unknown target: %s" % target, 404) - file = _getFileDetails(target, filename) + recursive = False + if "recursive" in request.values: + recursive = request.values["recursive"] in valid_boolean_trues + + file = _getFileDetails(target, filename, recursive=recursive) if not file: return make_response("File not found on '%s': %s" % (target, filename), 404) @@ -453,17 +453,18 @@ def gcodeFileCommand(filename, target): name, _ = os.path.splitext(filename) gcode_name = name + ".gco" + full_path = gcode_name if "path" in data and data["path"]: - gcode_name = fileManager.join_path(target, data["path"], gcode_name) + full_path = fileManager.join_path(target, data["path"], gcode_name) else: path, _ = fileManager.split_path(target, filename) if path: - gcode_name = fileManager.join_path(target, path, gcode_name) + full_path = fileManager.join_path(target, path, gcode_name) # prohibit overwriting the file that is currently being printed currentOrigin, currentFilename = _getCurrentFile() - if currentFilename == gcode_name and currentOrigin == target and (printer.is_printing() or printer.is_paused()): - make_response("Trying to slice into file that is currently being printed: %s" % gcode_name, 409) + if currentFilename == full_path and currentOrigin == target and (printer.is_printing() or printer.is_paused()): + make_response("Trying to slice into file that is currently being printed: %s" % full_path, 409) if "profile" in data.keys() and data["profile"]: profile = data["profile"] @@ -500,35 +501,36 @@ def gcodeFileCommand(filename, target): for key in override_keys: overrides[key[len("profile."):]] = data[key] - def slicing_done(target, gcode_name, select_after_slicing, print_after_slicing): + def slicing_done(target, path, select_after_slicing, print_after_slicing): if select_after_slicing or print_after_slicing: sd = False if target == FileDestinations.SDCARD: - filenameToSelect = gcode_name + filenameToSelect = path sd = True else: - filenameToSelect = fileManager.path_on_disk(target, gcode_name) + filenameToSelect = fileManager.path_on_disk(target, path) printer.select_file(filenameToSelect, sd, print_after_slicing) try: - fileManager.slice(slicer, target, filename, target, gcode_name, + fileManager.slice(slicer, target, filename, target, full_path, profile=profile, printer_profile_id=printerProfile, position=position, overrides=overrides, callback=slicing_done, - callback_args=(target, gcode_name, select_after_slicing, print_after_slicing)) + callback_args=(target, full_path, select_after_slicing, print_after_slicing)) except octoprint.slicing.UnknownProfile: return make_response("Profile {profile} doesn't exist".format(**locals()), 400) files = {} - location = url_for(".readGcodeFile", target=target, filename=gcode_name, _external=True) + location = url_for(".readGcodeFile", target=target, filename=full_path, _external=True) result = { "name": gcode_name, + "path": full_path, "origin": FileDestinations.LOCAL, "refs": { "resource": location, - "download": url_for("index", _external=True) + "downloads/files/" + target + "/" + gcode_name + "download": url_for("index", _external=True) + "downloads/files/" + target + "/" + full_path } } diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 8789405f..8c7324f1 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -166,9 +166,9 @@ $(function() { } else { self.listHelper.selectItem(function(item) { if (item.type == "folder") { - return _.startsWith(filename, OctoPrint.files.pathForElement(item) + "/"); + return _.startsWith(filename, item.path + "/"); } else { - return OctoPrint.files.pathForElement(item) == filename; + return item.path == filename; } }); } @@ -198,7 +198,7 @@ $(function() { if (self._otherRequestInProgress) return; self._otherRequestInProgress = true; - OctoPrint.files.list({ data: { recursive: true} }) + OctoPrint.files.list(true) .done(function(response) { self.fromResponse(response, filenameToFocus, locationToFocus, switchToPath); }) @@ -243,7 +243,7 @@ $(function() { }; self.changeFolder = function(data) { - self.currentPath(OctoPrint.files.pathForElement(data)); + self.currentPath(data.path); self.listHelper.updateItems(data.children); self.highlightCurrentFilename(); }; @@ -255,7 +255,7 @@ $(function() { }; self.changeFolderByPath = function(path) { - var element = OctoPrint.files.elementByPath(path, { children: self.allItems() }); + var element = self.elementByPath(path); if (element) { self.currentPath(path); self.listHelper.updateItems(element.children); @@ -268,6 +268,7 @@ $(function() { self.showAddFolderDialog = function() { if (self.addFolderDialog) { + self.addFolderName(""); self.addFolderDialog.modal("show"); } }; @@ -287,7 +288,7 @@ $(function() { if (!file) { return; } - OctoPrint.files.select(file.origin, OctoPrint.files.pathForElement(file)) + OctoPrint.files.select(file.origin, file.path) .done(function() { var withinPrintDimensions = self.evaluatePrintDimensions(file, true); if (withinPrintDimensions && printAfterLoad) { @@ -315,9 +316,9 @@ $(function() { filenameToFocus = fileToFocus.name; } - OctoPrint.files.delete(file.origin, OctoPrint.files.pathForElement(file)) + OctoPrint.files.delete(file.origin, file.path) .done(function() { - self.requestData(undefined, filenameToFocus, OctoPrint.files.pathForElement(file.parent)); + self.requestData(undefined, filenameToFocus, (file.parent ? file.parent.path : "")); }) }; @@ -326,7 +327,7 @@ $(function() { return; } - self.slicing.show(file.origin, OctoPrint.files.pathForElement(file), true); + self.slicing.show(file.origin, file.path, true); }; self.initSdCard = function() { @@ -556,6 +557,31 @@ $(function() { return false; }; + self.elementByPath = function(path, root) { + root = root || {children: self.allItems()}; + + var recursiveSearch = function(location, element) { + if (location.length == 0) { + return element; + } + + if (!element.hasOwnProperty("children")) { + return undefined; + } + + var name = location.shift(); + for (var i = 0; i < element.children.length; i++) { + if (name == element.children[i].name) { + return recursiveSearch(location, element.children[i]); + } + } + + return undefined; + }; + + return recursiveSearch(path.split("/"), root); + }; + self.onUserLoggedIn = function(user) { self.uploadButton.fileupload("enable"); if (self.uploadSdButton) { @@ -645,7 +671,7 @@ $(function() { }; self.onEventTransferDone = function(payload) { - self.requestData(payload.remote, "sdcard"); + self.requestData(undefined, payload.remote, "sdcard"); }; self.onServerConnect = self.onServerReconnect = function(payload) {