diff --git a/backend/handler/sync/ssh_handler.py b/backend/handler/sync/ssh_handler.py index d054060c2..5f5cb3316 100644 --- a/backend/handler/sync/ssh_handler.py +++ b/backend/handler/sync/ssh_handler.py @@ -11,6 +11,7 @@ device's sync_config. from __future__ import annotations +import functools import hashlib import os import tempfile @@ -51,12 +52,12 @@ class SSHSyncHandler: self.keys_path = Path(SYNC_SSH_KEYS_PATH) try: self.keys_path.mkdir(parents=True, exist_ok=True) - except OSError: - log.warning( - f"Could not create or access {self.keys_path}; " - "push-pull sync will be unavailable until the directory is writable " - "or keys are mounted at this path." - ) + except OSError as e: + raise RuntimeError( + f"SSH keys directory {self.keys_path} is not writable ({e}). " + "Mount a writable volume at this path, or set SYNC_SSH_KEYS_PATH " + "to a writable location, to use push-pull sync." + ) from e def _resolve_key_path(self, device_id: str, sync_config: dict) -> str | None: """Resolve the SSH key path for a device. @@ -231,4 +232,12 @@ class SSHSyncHandler: log.info(f"Deleted remote file: {remote_path}") -ssh_sync_handler = SSHSyncHandler() +@functools.cache +def get_ssh_sync_handler() -> SSHSyncHandler: + """Lazily instantiate the SSH sync handler on first use. + + Deferred so that startup doesn't fail when push-pull sync is unconfigured + or the keys directory is not writable. The handler raises a clear error + at call time if the directory cannot be created. + """ + return SSHSyncHandler() diff --git a/backend/tasks/sync_push_pull_task.py b/backend/tasks/sync_push_pull_task.py index a59bf6b82..8ab05c263 100644 --- a/backend/tasks/sync_push_pull_task.py +++ b/backend/tasks/sync_push_pull_task.py @@ -21,7 +21,7 @@ from handler.database import ( ) from handler.filesystem import fs_asset_handler from handler.sync.comparison import compare_save_state -from handler.sync.ssh_handler import ssh_sync_handler +from handler.sync.ssh_handler import get_ssh_sync_handler from logger.formatter import highlight as hl from logger.logger import log from models.device import Device, SyncMode @@ -65,6 +65,7 @@ async def run_push_pull_sync( async def _sync_device(device: Device, session_id: int | None = None) -> dict: """Perform push-pull sync for a single device.""" + ssh_sync_handler = get_ssh_sync_handler() sync_config = device.sync_config or {} if not sync_config.get("ssh_host"): log.warning(f"Push-pull device {device.id} has no ssh_host configured") @@ -227,6 +228,7 @@ async def _process_remote_save( remote_save, ) -> str: """Process a single remote save file. Returns action taken.""" + ssh_sync_handler = get_ssh_sync_handler() # Look up platform platform = db_platform_handler.get_platform_by_fs_slug(remote_save.platform_slug) if not platform: @@ -340,6 +342,7 @@ async def _push_missing_saves( save_directories: list[dict], ) -> int: """Push server saves that are missing from the device.""" + ssh_sync_handler = get_ssh_sync_handler() pushed = 0 # Build set of remote filenames per platform diff --git a/env.template b/env.template index b6c5b0f80..d413fc19e 100644 --- a/env.template +++ b/env.template @@ -87,7 +87,8 @@ SCHEDULED_CONVERT_IMAGES_TO_WEBP_CRON=0 4 * * * # Cron expression for scheduled ENABLE_SCHEDULED_RETROACHIEVEMENTS_PROGRESS_SYNC=false # Enable scheduled RetroAchievements progress sync SCHEDULED_RETROACHIEVEMENTS_PROGRESS_SYNC_CRON=0 4 * * * # Cron expression for scheduled RetroAchievements sync -# Sync +# Sync — both modes use $ROMM_BASE_PATH/sync by default. Mount a writable +# volume at that path when enabling either feature. ENABLE_SYNC_FOLDER_WATCHER=false # Watch the sync folder and trigger scans on change SYNC_FOLDER_SCAN_DELAY=2 # Delay in minutes before scanning after a sync folder change ENABLE_SYNC_PUSH_PULL=false # Enable scheduled sync push/pull