diff --git a/backend/handler/filesystem/base_handler.py b/backend/handler/filesystem/base_handler.py index 8ce4c8679..968fd73b5 100644 --- a/backend/handler/filesystem/base_handler.py +++ b/backend/handler/filesystem/base_handler.py @@ -276,7 +276,7 @@ class FSHandler: # Async thread-safe directory listing lock = await self._get_file_lock(str(target_directory)) async with lock: - if not target_directory.exists() or not target_directory.is_dir(): + if not target_directory.is_dir(): raise FileNotFoundError( f"Path does not exist or is not a directory: {str(target_directory)}" ) @@ -300,7 +300,7 @@ class FSHandler: # Async thread-safe directory removal lock = await self._get_file_lock(str(target_directory)) async with lock: - if not target_directory.exists() or not target_directory.is_dir(): + if not target_directory.is_dir(): raise FileNotFoundError( f"Path does not exist or is not a directory: {str(target_directory)}" ) @@ -414,7 +414,7 @@ class FSHandler: # Async thread-safe file read lock = await self._get_file_lock(str(full_path)) async with lock: - if not full_path.exists() or not full_path.is_file(): + if not full_path.is_file(): raise FileNotFoundError(f"File not found: {full_path}") async with await open_file(full_path, "rb") as f: @@ -442,11 +442,42 @@ class FSHandler: # Async thread-safe file stream lock = await self._get_file_lock(str(full_path)) async with lock: - if not full_path.exists() or not full_path.is_file(): + if not full_path.is_file(): raise FileNotFoundError(f"File not found: {full_path}") return await open_file(full_path, "rb") + async def copy_file(self, source_full_path: Path, dest_path: str) -> None: + """ + Copy a file from source to destination. + + Args: + source_full_path: Absolute path to the source file + dest_path: Relative path to the destination file + + Raises: + FileNotFoundError: If source file does not exist + ValueError: If destination path is invalid + """ + if not source_full_path or not dest_path: + raise ValueError("Source and destination paths cannot be empty") + + # Validate and normalize path + dest_full_path = self.validate_path(dest_path) + + # Use locks for both source and destination + source_lock = await self._get_file_lock(str(source_full_path)) + dest_lock = await self._get_file_lock(str(dest_full_path)) + + # Async thread-safe file copy + async with source_lock, dest_lock: + if not source_full_path.is_file(): + raise FileNotFoundError(f"Source file not found: {source_full_path}") + + # Create destination directory if needed + dest_full_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(str(source_full_path), str(dest_full_path)) + async def move_file_or_folder(self, source_path: str, dest_path: str) -> None: """ Move a file from source to destination. @@ -479,7 +510,6 @@ class FSHandler: # Create destination directory if needed dest_full_path.parent.mkdir(parents=True, exist_ok=True) - shutil.move(str(source_full_path), str(dest_full_path)) async def remove_file(self, file_path: str) -> None: @@ -528,7 +558,7 @@ class FSHandler: # Async thread-safe directory listing lock = await self._get_file_lock(str(full_path)) async with lock: - if not full_path.exists() or not full_path.is_dir(): + if not full_path.is_dir(): raise FileNotFoundError(f"Directory not found: {full_path}") return [f for _, f in iter_files(str(full_path), recursive=False)] @@ -552,7 +582,7 @@ class FSHandler: # Async thread-safe existence check lock = await self._get_file_lock(str(full_path)) async with lock: - return full_path.exists() and full_path.is_file() + return full_path.is_file() async def get_file_size(self, file_path: str) -> int: """ @@ -576,7 +606,7 @@ class FSHandler: # Async thread-safe file size retrieval lock = await self._get_file_lock(str(full_path)) async with lock: - if not full_path.exists() or not full_path.is_file(): + if not full_path.is_file(): raise FileNotFoundError(f"File not found: {full_path}") return full_path.stat().st_size diff --git a/backend/handler/filesystem/resources_handler.py b/backend/handler/filesystem/resources_handler.py index 3e8c5584b..3a8b016e3 100644 --- a/backend/handler/filesystem/resources_handler.py +++ b/backend/handler/filesystem/resources_handler.py @@ -1,6 +1,5 @@ import gzip import os -import shutil from io import BytesIO from pathlib import Path @@ -69,7 +68,7 @@ class FSResourcesHandler(FSHandler): size: size of the cover """ cover_file = f"{entity.fs_resources_path}/cover" - await self.make_directory(f"{cover_file}") + await self.make_directory(cover_file) # Handle file:// URLs for gamelist.xml if url_cover.startswith("file://"): @@ -77,13 +76,16 @@ class FSResourcesHandler(FSHandler): file_path = Path(url_cover[7:]) # Remove "file://" prefix if file_path.exists(): # Copy the file to the resources directory - dest_path = self.validate_path(f"{cover_file}/{size.value}.png") - shutil.copy2(file_path, dest_path) + dest_path = f"{cover_file}/{size.value}.png" + await self.copy_file(file_path, dest_path) if ENABLE_SCHEDULED_CONVERT_IMAGES_TO_WEBP: - self.image_converter.convert_to_webp(dest_path, force=True) + self.image_converter.convert_to_webp( + self.validate_path(f"{cover_file}/{size.value}.png"), + force=True, + ) else: - log.warning(f"File not found: {file_path}") + log.warning(f"Cover file not found: {file_path}") return None except Exception as exc: log.error(f"Unable to copy cover file {url_cover}: {str(exc)}") @@ -245,8 +247,7 @@ class FSResourcesHandler(FSHandler): file_path = Path(url_screenhot[7:]) # Remove "file://" prefix if file_path.exists(): # Copy the file to the resources directory - dest_path = self.validate_path(f"{screenshot_path}/{idx}.jpg") - shutil.copy2(file_path, dest_path) + await self.copy_file(file_path, f"{screenshot_path}/{idx}.jpg") else: log.warning(f"Screenshot file not found: {file_path}") return None @@ -357,8 +358,7 @@ class FSResourcesHandler(FSHandler): file_path = Path(url_manual[7:]) # Remove "file://" prefix if file_path.exists(): # Copy the file to the resources directory - dest_path = self.validate_path(f"{manual_path}/{rom.id}.pdf") - shutil.copy2(file_path, dest_path) + await self.copy_file(file_path, f"{manual_path}/{rom.id}.pdf") else: log.warning(f"Manual file not found: {file_path}") return None @@ -466,12 +466,12 @@ class FSResourcesHandler(FSHandler): ) -> str: return os.path.join("roms", str(platform_id), str(rom_id), media_type.value) - async def store_media_file(self, url: str, path: str) -> None: + async def store_media_file(self, url: str, dest_path: str) -> None: httpx_client = ctx_httpx_client.get() - directory, filename = os.path.split(path) + directory, filename = os.path.split(dest_path) - if await self.file_exists(path): - log.debug(f"Media file {path} already exists, skipping download") + if await self.file_exists(dest_path): + log.debug(f"Media file {dest_path} already exists, skipping download") return # Ensure destination directory exists @@ -482,9 +482,7 @@ class FSResourcesHandler(FSHandler): try: file_path = Path(url[7:]) # Remove "file://" prefix if file_path.exists(): - # Validate the destination path - dest_path = self.validate_path(path) - shutil.copy2(file_path, dest_path) + await self.copy_file(file_path, dest_path) except Exception as exc: log.error(f"Unable to copy media file {url}: {str(exc)}") return None diff --git a/backend/handler/metadata/gamelist_handler.py b/backend/handler/metadata/gamelist_handler.py index 6f0ee0d23..01c975fca 100644 --- a/backend/handler/metadata/gamelist_handler.py +++ b/backend/handler/metadata/gamelist_handler.py @@ -326,27 +326,15 @@ class GamelistHandler(MetadataHandler): gamelist_metadata=rom_metadata, ) - platform_dir = fs_platform_handler.get_plaform_fs_structure( - platform.fs_slug - ) - # Choose which cover style to use - cover_path = rom_metadata["box2d_url"] or rom_metadata["image_url"] - if cover_path: - cover_path_path = fs_platform_handler.validate_path( - f"{platform_dir}/{cover_path}" - ) - rom_data["url_cover"] = f"file://{str(cover_path_path)}" + cover_url = rom_metadata["box2d_url"] or rom_metadata["image_url"] + if cover_url: + rom_data["url_cover"] = cover_url # Grab the manual - if ( - rom_metadata["manual_url"] - and MetadataMediaType.MANUAL in preferred_media_types - ): - manual_path = fs_platform_handler.validate_path( - f"{platform_dir}/{rom_metadata['manual_url']}" - ) - rom_data["url_manual"] = f"file://{str(manual_path)}" + manual_url = rom_metadata["manual_url"] + if manual_url and MetadataMediaType.MANUAL in preferred_media_types: + rom_data["url_manual"] = manual_url # Build list of screenshot URLs url_screenshots = [] @@ -354,18 +342,12 @@ class GamelistHandler(MetadataHandler): rom_metadata["screenshot_url"] and MetadataMediaType.SCREENSHOT in preferred_media_types ): - screenshot_path = fs_platform_handler.validate_path( - f"{platform_dir}/{rom_metadata['screenshot_url']}" - ) - url_screenshots.append(f"file://{str(screenshot_path)}") + url_screenshots.append(rom_metadata["screenshot_url"]) if ( rom_metadata["title_screen_url"] and MetadataMediaType.TITLE_SCREEN in preferred_media_types ): - title_screen_path = fs_platform_handler.validate_path( - f"{platform_dir}/{rom_metadata['title_screen_url']}" - ) - url_screenshots.append(f"file://{str(title_screen_path)}") + url_screenshots.append(rom_metadata["title_screen_url"]) rom_data["url_screenshots"] = url_screenshots # Store by filename for matching diff --git a/frontend/src/views/Scan.vue b/frontend/src/views/Scan.vue index 5afebb60d..8fe9650e8 100644 --- a/frontend/src/views/Scan.vue +++ b/frontend/src/views/Scan.vue @@ -276,7 +276,9 @@ async function stopScan() { :fs-slug="item.raw.fs_slug" :size="20" /> - {{ item.raw.name }} +
+ {{ item.raw.name }} +