diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 962310fb..92964145 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -588,6 +588,9 @@ class SmartScanThing(Thing): self._return_to_starting_position() self._perform_final_stitch() + # Remove any scan folders containing zero images + self.purge_empty_scans(logger=self._scan_logger) + @_scan_running def _main_scan_loop(self): """ @@ -868,22 +871,26 @@ class SmartScanThing(Thing): 404: {"description": "Scan not found"}, }, ) - def delete_scan(self, scan_name: str) -> None: - """Delete all files from a scan. + def delete_scan(self, scan_name: str, logger: InvocationLogger) -> None: + """Delete the folder for the specified scan. This endpoint allows scans to be deleted from disk. + + Takes the scan name to delete, and the Invocation Logger """ path = os.path.join(self.base_scan_dir, scan_name) if not os.path.isdir(path): - print(f"can't find {path}") - raise HTTPException(404, "Scan not found") - shutil.rmtree(path) + logger.info(f"can't find {path}") + raise HTTPException(400, "Scan not found") + deleted_scan_success = self._delete_scan(path, logger) + if not deleted_scan_success: + raise HTTPException(400, "Couldn't delete scan, check log for details") @fastapi_endpoint( "delete", "scans", ) - def delete_all_scans(self) -> None: + def delete_all_scans(self, logger: InvocationLogger) -> None: """Delete all the scans on the microscope **This will irreversibly remove all scanned data from the @@ -891,7 +898,20 @@ class SmartScanThing(Thing): Use with extreme caution. """ for scan in self.scans: - self.delete_scan(scan.name) + path = os.path.join(self.base_scan_dir, scan.name) + self._delete_scan(path, logger) + + @thing_action + def _delete_scan(self, scan_path, logger: InvocationLogger) -> bool: + try: + shutil.rmtree(scan_path) + return True + except Exception as e: + logger.warning( + "Attempted to delete scan " + scan_path + ", which failed." + " Server sent response" + str(e) + ) + return False @property def latest_preview_stitch_path(self): @@ -1179,3 +1199,26 @@ class SmartScanThing(Thing): """List the relative paths of all files and folders in the zip folder specified""" scan_zip = zipfile.ZipFile(zip_path) return [os.path.normpath(i) for i in scan_zip.namelist()] + + @thing_action + def purge_empty_scans(self, logger: InvocationLogger) -> None: + """ + Delete all scan folders containing no images at the top level + """ + scan_list = self.scans + + # Filter out scans with no top level files, ignoring JSON files + # JSON is ignored as it's created before any images are captured + for scan in scan_list: + scan_folder = os.path.join(self.base_scan_dir, scan.name, IMG_DIR_NAME) + images_found = False + for fname in os.listdir(scan_folder): + fpath = os.path.join(scan_folder, fname) + if os.path.isfile(fpath) and IMAGE_REGEX.search(fname): + images_found = True + # break as soon as an image is found. + break + + if not images_found: + path = os.path.join(self.base_scan_dir, scan.name) + self._delete_scan(path, logger) diff --git a/webapp/src/main.js b/webapp/src/main.js index c95e5869..ed49dbc1 100644 --- a/webapp/src/main.js +++ b/webapp/src/main.js @@ -144,10 +144,16 @@ Vue.mixin({ if (error.response) { // If the response is a nicely formatted JSON response from the server if (error.response.data.message) { - return `${error.response.status}: ${error.response.data.message}`; + return `${error.response.data.message}`; + } + if (error.response.data.detail) { + return `${error.response.data.detail}`; } // If the response is just some generic error response - return `${error.response.status}: ${error.response.data}`; + if (error.response.data){ + return `${error.response.data}`; + } + return `${error.response}`; } // If we have an error object with a message, use that if (error.message) return `${error.message}`;