From c3ddbd0b1b26066037cd7896c500cb8b71efc01d Mon Sep 17 00:00:00 2001 From: Salandora Date: Fri, 9 Oct 2015 02:37:42 +0200 Subject: [PATCH] Added create and remove folder API commands --- src/octoprint/filemanager/__init__.py | 3 + src/octoprint/filemanager/storage.py | 12 + src/octoprint/server/api/files.py | 309 +++++++++++++++----------- 3 files changed, 196 insertions(+), 128 deletions(-) diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index 3e2d5657..a3f0a73a 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -340,6 +340,9 @@ class FileManager(object): def get_busy_files(self): return self._slicing_jobs.keys() + def file_in_path(self, destination, path, file): + return self._storage(destination).file_in_path(path, file) + def file_exists(self, destination, path): return self._storage(destination).file_exists(path) diff --git a/src/octoprint/filemanager/storage.py b/src/octoprint/filemanager/storage.py index f67ef68c..28c0fccb 100644 --- a/src/octoprint/filemanager/storage.py +++ b/src/octoprint/filemanager/storage.py @@ -34,6 +34,15 @@ class StorageInterface(object): return yield + def file_in_path(self, path, filepath): + """ + Returns whether the file indicated by ``file`` is inside ``path`` or not. + :param string path: the path to check + :param string filepath: path to the file + :return: ``True`` if the file is inside the path, ``False`` otherwise + """ + return NotImplementedError() + def file_exists(self, path): """ Returns whether the file indicated by ``path`` exists or not. @@ -419,6 +428,9 @@ class LocalFileStorage(StorageInterface): for sub_entry in self._analysis_backlog_generator(absolute_path): yield self.join_path(entry, sub_entry[0]), sub_entry[1], sub_entry[2] + def file_in_path(self, path, filepath): + return filepath.startswith(path) + def file_exists(self, path): path, name = self.sanitize(path) file_path = os.path.join(path, name) diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index 2804f5d5..908e387e 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -157,148 +157,178 @@ def _verifyFileExists(origin, filename): return fileManager.file_exists(origin, filename) +def _verifyFolderExists(origin, foldername): + if origin == FileDestinations.SDCARD: + return False + else: + return fileManager.folder_exists(origin, foldername) + + +def _verifyFolderNotBusy(target, foldername): + busy_files = fileManager.get_busy_files() + for item in busy_files: + if target == item[0] and fileManager.file_in_path(target, foldername, item[1]): + return False + + return True + @api.route("/files/", methods=["POST"]) @restricted_access def uploadGcodeFile(target): - if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]: - return make_response("Unknown target: %s" % target, 404) - input_name = "file" input_upload_name = input_name + "." + settings().get(["server", "uploads", "nameSuffix"]) input_upload_path = input_name + "." + settings().get(["server", "uploads", "pathSuffix"]) if input_upload_name in request.values and input_upload_path in request.values: + if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]: + return make_response("Unknown target: %s" % target, 404) + upload = octoprint.filemanager.util.DiskFileWrapper(request.values[input_upload_name], request.values[input_upload_path]) - else: - return make_response("No file included", 400) - # Store any additional user data the caller may have passed. - userdata = None - if "userdata" in request.values: - import json + # Store any additional user data the caller may have passed. + userdata = None + if "userdata" in request.values: + import json + try: + userdata = json.loads(request.values["userdata"]) + except: + return make_response("userdata contains invalid JSON", 400) + + if target == FileDestinations.SDCARD and not settings().getBoolean(["feature", "sdSupport"]): + return make_response("SD card support is disabled", 404) + + sd = target == FileDestinations.SDCARD + selectAfterUpload = "select" in request.values.keys() and request.values["select"] in valid_boolean_trues + printAfterSelect = "print" in request.values.keys() and request.values["print"] in valid_boolean_trues + + if sd: + # validate that all preconditions for SD upload are met before attempting it + if not (printer.is_operational() and not (printer.is_printing() or printer.is_paused())): + return make_response("Can not upload to SD card, printer is either not operational or already busy", 409) + if not printer.is_sd_ready(): + return make_response("Can not upload to SD card, not yet initialized", 409) + + # 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 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: - userdata = json.loads(request.values["userdata"]) + futurePath, futureFilename = fileManager.sanitize(target, upload.filename) except: - return make_response("userdata contains invalid JSON", 400) + futurePath = None + futureFilename = None - if target == FileDestinations.SDCARD and not settings().getBoolean(["feature", "sdSupport"]): - return make_response("SD card support is disabled", 404) + if futureFilename is None: + return make_response("Can not upload file %s, wrong format?" % upload.filename, 415) - sd = target == FileDestinations.SDCARD - selectAfterUpload = "select" in request.values.keys() and request.values["select"] in valid_boolean_trues - printAfterSelect = "print" in request.values.keys() and request.values["print"] in valid_boolean_trues + if "path" in request.values: + futurePath = fileManager.sanitize_path(target, request.values["path"]) - if sd: - # validate that all preconditions for SD upload are met before attempting it - if not (printer.is_operational() and not (printer.is_printing() or printer.is_paused())): - return make_response("Can not upload to SD card, printer is either not operational or already busy", 409) - if not printer.is_sd_ready(): - return make_response("Can not upload to SD card, not yet initialized", 409) + futureFullPath = fileManager.join_path(target, futurePath, futureFilename) - # 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 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"] + # prohibit overwriting currently selected file while it's being printed + 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) - # determine future filename of file to be uploaded, abort if it can't be uploaded - try: - futurePath, futureFilename = fileManager.sanitize(target, upload.filename) - except: - futurePath = None - futureFilename = None + def fileProcessingFinished(filename, absFilename, destination): + """ + Callback for when the file processing (upload, optional slicing, addition to analysis queue) has + finished. - if futureFilename is None: - return make_response("Can not upload file %s, wrong format?" % upload.filename, 415) + Depending on the file's destination triggers either streaming to SD card or directly calls selectAndOrPrint. + """ - if "path" in request.values: - futurePath = fileManager.sanitize_path(target, request.values["path"]) + if destination == FileDestinations.SDCARD and octoprint.filemanager.valid_file_type(filename, "gcode"): + return filename, printer.add_sd_file(filename, absFilename, selectAndOrPrint) + else: + selectAndOrPrint(filename, absFilename, destination) + return filename - futureFullPath = fileManager.join_path(target, futurePath, futureFilename) + def selectAndOrPrint(filename, absFilename, destination): + """ + Callback for when the file is ready to be selected and optionally printed. For SD file uploads this is only + the case after they have finished streaming to the printer, which is why this callback is also used + for the corresponding call to addSdFile. - # prohibit overwriting currently selected file while it's being printed - 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) + Selects the just uploaded file if either selectAfterUpload or printAfterSelect are True, or if the + exact file is already selected, such reloading it. + """ + 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) - def fileProcessingFinished(filename, absFilename, destination): - """ - Callback for when the file processing (upload, optional slicing, addition to analysis queue) has - finished. - - Depending on the file's destination triggers either streaming to SD card or directly calls selectAndOrPrint. - """ - - if destination == FileDestinations.SDCARD and octoprint.filemanager.valid_file_type(filename, "gcode"): - return filename, printer.add_sd_file(filename, absFilename, selectAndOrPrint) + 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"): + filename = added_file + done = True else: - selectAndOrPrint(filename, absFilename, destination) - return filename + filename = fileProcessingFinished(added_file, fileManager.path_on_disk(FileDestinations.LOCAL, added_file), target) + done = True - def selectAndOrPrint(filename, absFilename, destination): - """ - Callback for when the file is ready to be selected and optionally printed. For SD file uploads this is only - the case after they have finished streaming to the printer, which is why this callback is also used - for the corresponding call to addSdFile. + if userdata is not None: + # upload included userdata, add this now to the metadata + fileManager.set_additional_metadata(FileDestinations.LOCAL, added_file, "userdata", userdata) - Selects the just uploaded file if either selectAfterUpload or printAfterSelect are True, or if the - exact file is already selected, such reloading it. - """ - 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) + sdFilename = None + if isinstance(filename, tuple): + filename, sdFilename = filename - 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"): - filename = added_file - done = True - else: - filename = fileProcessingFinished(added_file, fileManager.path_on_disk(FileDestinations.LOCAL, added_file), target) - done = True + eventManager.fire(Events.UPLOAD, {"file": filename, "target": target}) - if userdata is not None: - # upload included userdata, add this now to the metadata - fileManager.set_additional_metadata(FileDestinations.LOCAL, added_file, "userdata", userdata) - - sdFilename = None - if isinstance(filename, tuple): - filename, sdFilename = filename - - eventManager.fire(Events.UPLOAD, {"file": filename, "target": target}) - - files = {} - location = url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True) - files.update({ - FileDestinations.LOCAL: { - "name": filename, - "origin": FileDestinations.LOCAL, - "refs": { - "resource": location, - "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + filename - } - } - }) - - if sd and sdFilename: - location = url_for(".readGcodeFile", target=FileDestinations.SDCARD, filename=sdFilename, _external=True) + files = {} + location = url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=filename, _external=True) files.update({ - FileDestinations.SDCARD: { - "name": sdFilename, - "origin": FileDestinations.SDCARD, + FileDestinations.LOCAL: { + "name": filename, + "origin": FileDestinations.LOCAL, "refs": { - "resource": location + "resource": location, + "download": url_for("index", _external=True) + "downloads/files/" + FileDestinations.LOCAL + "/" + filename } } }) - r = make_response(jsonify(files=files, done=done), 201) - r.headers["Location"] = location - return r + if sd and sdFilename: + location = url_for(".readGcodeFile", target=FileDestinations.SDCARD, filename=sdFilename, _external=True) + files.update({ + FileDestinations.SDCARD: { + "name": sdFilename, + "origin": FileDestinations.SDCARD, + "refs": { + "resource": location + } + } + }) + + r = make_response(jsonify(files=files, done=done), 201) + r.headers["Location"] = location + return r + else: + if "foldername" not in request.json: + return make_response("No path information or no file included", 409) + + if not target in [FileDestinations.LOCAL]: + return make_response("Unknown target: %s" % target, 404) + + futurePath, futureName = fileManager.sanitize(target, request.json["foldername"]) + 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) + + added_folder = fileManager.add_folder(target, futureFullPath) + if added_folder is None: + return make_response("Could not create folder %s" % futureName, 500) + + return NO_CONTENT @api.route("/files//", methods=["GET"]) @@ -464,29 +494,52 @@ def gcodeFileCommand(filename, target): @api.route("/files//", methods=["DELETE"]) @restricted_access def deleteGcodeFile(filename, target): - if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]: - return make_response("Unknown target: %s" % target, 404) + if not _verifyFileExists(target, filename) and not _verifyFolderExists(target, filename): + return make_response("File/Folder not found on '%s': %s" % (target, filename), 404) - if not _verifyFileExists(target, filename): - return make_response("File not found on '%s': %s" % (target, filename), 404) + if _verifyFileExists(target, filename): + if not target in [FileDestinations.LOCAL, FileDestinations.SDCARD]: + return make_response("Unknown target: %s" % target, 404) - # prohibit deleting files that are currently in use - currentOrigin, currentFilename = _getCurrentFile() - if currentFilename == filename and currentOrigin == target and (printer.is_printing() or printer.is_paused()): - make_response("Trying to delete file that is currently being printed: %s" % filename, 409) + # prohibit deleting files that are currently in use + currentOrigin, currentFilename = _getCurrentFile() - if (target, filename) in fileManager.get_busy_files(): - make_response("Trying to delete a file that is currently in use: %s" % filename, 409) + if currentFilename is not None and currentFilename == filename and currentOrigin == target and (printer.is_printing() or printer.is_paused()): + return make_response("Trying to delete file that is currently being printed: %s" % filename, 409) - # deselect the file if it's currently selected - if currentFilename is not None and filename == currentFilename: - printer.unselect_file() + if not _verifyFolderNotBusy(target, filename): + return make_response("Trying to delete a file that is currently in use: %s" % filename, 409) - # delete it - if target == FileDestinations.SDCARD: - printer.delete_sd_file(filename) - else: - fileManager.remove_file(target, filename) + # deselect the file if it's currently selected + if currentFilename is not None and filename == currentFilename: + printer.unselect_file() + + # delete it + if target == FileDestinations.SDCARD: + printer.delete_sd_file(filename) + else: + fileManager.remove_file(target, filename) + + elif _verifyFolderExists(target, filename): + if not target in [FileDestinations.LOCAL]: + return make_response("Unknown target: %s" % target, 404) + + folderpath = filename + # prohibit deleting folders that are currently in use + currentOrigin, currentFilename = _getCurrentFile() + + if currentFilename is not None and fileManager.file_in_path(target, folderpath, currentFilename) and currentOrigin == target and (printer.is_printing() or printer.is_paused()): + return make_response("Trying to delete a folder that contains a file that is currently being printed: %s" % folderpath, 409) + + if not _verifyFolderNotBusy(target, folderpath): + return make_response("Trying to delete a folder that contains a file that is currently in use: %s" % folderpath, 409) + + # deselect the file if it's currently selected + if currentFilename is not None and fileManager.file_in_path(target, folderpath, currentFilename): + printer.unselect_file() + + # delete it + fileManager.remove_folder(target, folderpath) return NO_CONTENT