From 2a239823300571d40d62a3c97ba9f7fec6d77ada Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 8 May 2025 19:44:05 +0100 Subject: [PATCH 1/7] Added purge_empty_scans() method --- .../things/smart_scan.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 839d64b5..4919ef3c 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -1141,3 +1141,25 @@ 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) + image_list = [ + f + for f in os.listdir(scan_folder) + if os.path.isfile(os.path.join(scan_folder, f)) and "json" not in f + ] + + logger.info(image_list) + + if len(image_list) == 0: + self.delete_scan(scan.name) From 0df363a1bec8494387e964581f9521dd5c02b405 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 9 May 2025 12:12:12 +0100 Subject: [PATCH 2/7] Run purge_empty_scans() at the end of each scan --- src/openflexure_microscope_server/things/smart_scan.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 4919ef3c..9c7baa84 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -527,6 +527,8 @@ class SmartScanThing(Thing): ) raise e finally: + # Whether or not this scan succeeded, remove any scan folders containing zero images + self.purge_empty_scans(logger=self._scan_logger) if self._capture_thread: # If the capture thread had an error, we capture it here try: From 3a45c68ef3fed5160debd8532ae5d2ed74b99fd7 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 13 May 2025 12:23:01 +0100 Subject: [PATCH 3/7] Try Except around deleting scans to check for PermissionErrors --- .../things/smart_scan.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 9c7baa84..df14bc2e 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -843,7 +843,7 @@ class SmartScanThing(Thing): "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 @@ -851,7 +851,10 @@ class SmartScanThing(Thing): Use with extreme caution. """ for scan in self.scans: - self.delete_scan(scan.name) + try: + self.delete_scan(scan.name) + except PermissionError: + logger.warning(f"Could not delete scan {scan.name}. Check folder permissions") @property def latest_preview_stitch_path(self): @@ -1161,7 +1164,8 @@ class SmartScanThing(Thing): if os.path.isfile(os.path.join(scan_folder, f)) and "json" not in f ] - logger.info(image_list) - if len(image_list) == 0: - self.delete_scan(scan.name) + try: + self.delete_scan(scan.name) + except PermissionError: + logger.warning(f"Could not delete scan {scan.name}. Check folder permissions") From 0e3c71b958a886fbfcbfbaebffb15912cf413e7c Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 13 May 2025 12:36:06 +0100 Subject: [PATCH 4/7] Removed from finally block --- .../things/smart_scan.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index df14bc2e..2a58ba64 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -527,8 +527,6 @@ class SmartScanThing(Thing): ) raise e finally: - # Whether or not this scan succeeded, remove any scan folders containing zero images - self.purge_empty_scans(logger=self._scan_logger) if self._capture_thread: # If the capture thread had an error, we capture it here try: @@ -550,6 +548,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): """ @@ -843,7 +844,7 @@ class SmartScanThing(Thing): "delete", "scans", ) - def delete_all_scans(self, logger:InvocationLogger) -> 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 @@ -854,7 +855,9 @@ class SmartScanThing(Thing): try: self.delete_scan(scan.name) except PermissionError: - logger.warning(f"Could not delete scan {scan.name}. Check folder permissions") + logger.warning( + f"Could not delete scan {scan.name}. Check folder permissions" + ) @property def latest_preview_stitch_path(self): @@ -1168,4 +1171,6 @@ class SmartScanThing(Thing): try: self.delete_scan(scan.name) except PermissionError: - logger.warning(f"Could not delete scan {scan.name}. Check folder permissions") + logger.warning( + f"Could not delete scan {scan.name}. Check folder permissions" + ) From 19d72bef1cefe4793e63766af4749c78de1346b8 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 14 May 2025 16:11:02 +0100 Subject: [PATCH 5/7] Error handling in _delete_scan(), which is now handled in GUI main.js --- .../things/smart_scan.py | 34 +++++++++++-------- webapp/src/main.js | 10 ++++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 2a58ba64..a3c11eed 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -829,7 +829,7 @@ class SmartScanThing(Thing): 404: {"description": "Scan not found"}, }, ) - def delete_scan(self, scan_name: str) -> None: + def delete_scan(self, scan_name: str, logger: InvocationLogger) -> None: """Delete all files from a scan. This endpoint allows scans to be deleted from disk. @@ -837,8 +837,10 @@ class SmartScanThing(Thing): 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) + raise HTTPException(400, "Scan not found") + result = self._delete_scan(path, logger) + if not result: + raise HTTPException(400, "Couldn't delete scan, check log for details") @fastapi_endpoint( "delete", @@ -852,12 +854,19 @@ class SmartScanThing(Thing): Use with extreme caution. """ for scan in self.scans: - try: - self.delete_scan(scan.name) - except PermissionError: - logger.warning( - f"Could not delete scan {scan.name}. Check folder permissions" - ) + self.delete_scan(scan.name, 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): @@ -1168,9 +1177,4 @@ class SmartScanThing(Thing): ] if len(image_list) == 0: - try: - self.delete_scan(scan.name) - except PermissionError: - logger.warning( - f"Could not delete scan {scan.name}. Check folder permissions" - ) + self.delete_scan(scan.name) 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}`; From 4a44dceac6d8eaa0b73fd33983a66e7541bfe855 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 20 May 2025 16:36:57 +0100 Subject: [PATCH 6/7] More clear variable name, use _delete_scan to avoid calling fastapi-endpoint in loops --- .../things/smart_scan.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index a3c11eed..42f9a4e5 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -830,16 +830,18 @@ class SmartScanThing(Thing): }, ) def delete_scan(self, scan_name: str, logger: InvocationLogger) -> None: - """Delete all files from a scan. + """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}") + logger.info(f"can't find {path}") raise HTTPException(400, "Scan not found") - result = self._delete_scan(path, logger) - if not result: + 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( @@ -854,7 +856,8 @@ class SmartScanThing(Thing): Use with extreme caution. """ for scan in self.scans: - self.delete_scan(scan.name, logger) + 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: @@ -1177,4 +1180,5 @@ class SmartScanThing(Thing): ] if len(image_list) == 0: - self.delete_scan(scan.name) + path = os.path.join(self.base_scan_dir, scan.name) + self._delete_scan(path, logger) From 6c8c8bf358f562330b289359776696135baf6665 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Tue, 20 May 2025 17:04:48 +0000 Subject: [PATCH 7/7] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Julian Stirling --- .../things/smart_scan.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 42f9a4e5..17c95e66 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -1173,12 +1173,14 @@ class SmartScanThing(Thing): # 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) - image_list = [ - f - for f in os.listdir(scan_folder) - if os.path.isfile(os.path.join(scan_folder, f)) and "json" not in f - ] + 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 len(image_list) == 0: + if not images_found: path = os.path.join(self.base_scan_dir, scan.name) self._delete_scan(path, logger)