mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
feat(fs): hardlink import/export assets when possible, harden sync init
Importer (gamelist/launchbox file:// flows) and exporters (gamelist.xml, metadata.pegasus.txt local exports) now hardlink media assets when source and destination share a filesystem, falling back transparently to a copy on EXDEV / EPERM / EOPNOTSUPP / EMLINK / EACCES (cross-device, FAT32, exFAT, network mounts, etc.). Saves disk space and is effectively instantaneous on large files (videos, manuals, miximages). Covers keep a real copy (allow_link=False) because _store_cover resizes the small cover in place via PIL.Image.save, which would truncate the shared inode and corrupt the user's source image. Also makes FSSyncHandler tolerate a missing/unwritable /romm/sync at startup: an OSError from mkdir now logs a warning instead of crashing the whole app at module-import time. Sync calls still fail at use time if the mount remains broken — the right place to surface the error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,8 +16,9 @@ from anyio import open_file
|
||||
from starlette.datastructures import UploadFile
|
||||
|
||||
from config.config_manager import config_manager as cm
|
||||
from logger.logger import log
|
||||
from models.base import FILE_NAME_MAX_LENGTH
|
||||
from utils.filesystem import iter_directories, iter_files
|
||||
from utils.filesystem import iter_directories, iter_files, link_or_copy_file
|
||||
|
||||
TAG_REGEX = re.compile(r"\(([^)]+)\)|\[([^]]+)\]")
|
||||
EXTENSION_REGEX = re.compile(r"\.(([a-z]+\.)*\w+)$")
|
||||
@@ -148,13 +149,22 @@ class Asset(Enum):
|
||||
|
||||
|
||||
class FSHandler:
|
||||
def __init__(self, base_path: str):
|
||||
def __init__(self, base_path: str, tolerate_missing_base: bool = False):
|
||||
self.base_path = Path(base_path).resolve()
|
||||
self._locks: dict[str, asyncio.Lock] = {}
|
||||
self._lock_mutex = asyncio.Lock()
|
||||
|
||||
# Create base directory synchronously during initialization
|
||||
self.base_path.mkdir(parents=True, exist_ok=True)
|
||||
# Create base directory synchronously during initialization.
|
||||
try:
|
||||
self.base_path.mkdir(parents=True, exist_ok=True)
|
||||
except OSError:
|
||||
if not tolerate_missing_base:
|
||||
raise
|
||||
|
||||
log.warning(
|
||||
f"Could not create or access {self.base_path}; "
|
||||
"feature will be unavailable until the directory is writable."
|
||||
)
|
||||
|
||||
async def _get_file_lock(self, file_path: str) -> asyncio.Lock:
|
||||
"""Get or create a lock for a specific file path."""
|
||||
@@ -487,13 +497,23 @@ class FSHandler:
|
||||
|
||||
return await open_file(full_path, "rb")
|
||||
|
||||
async def copy_file(self, source_full_path: Path, dest_path: str) -> None:
|
||||
async def copy_file(
|
||||
self,
|
||||
source_full_path: Path,
|
||||
dest_path: str,
|
||||
allow_link: bool = True,
|
||||
) -> 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
|
||||
allow_link: If True (default), try a hardlink first and fall back to
|
||||
a copy when the link isn't possible (cross-device, unsupported
|
||||
filesystem, etc.). Pass False when the caller will mutate the
|
||||
destination in place, since mutating a hardlinked file also
|
||||
mutates the source — see `_store_cover`'s resize step.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If source file does not exist
|
||||
@@ -518,7 +538,10 @@ class FSHandler:
|
||||
# Create destination directory if needed
|
||||
dest_parent_anyio_path = AnyioPath(str(dest_full_path.parent))
|
||||
await dest_parent_anyio_path.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(str(source_full_path), str(dest_full_path))
|
||||
if allow_link:
|
||||
link_or_copy_file(source_full_path, dest_full_path)
|
||||
else:
|
||||
shutil.copy2(str(source_full_path), str(dest_full_path))
|
||||
|
||||
async def move_file_or_folder(self, source_path: str, dest_path: str) -> None:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user