refactor(sync): make SSH sync handler lazy-initialized

The module-level SSHSyncHandler() singleton ran filesystem side effects
(mkdir on SYNC_SSH_KEYS_PATH) at import time, which meant even default
installs with push-pull sync disabled would touch /romm/sync — and the
previous tolerate-and-warn fallback could leave users wondering why
sync silently does nothing.

Replace the eager singleton with a functools.cache'd factory. The
handler is now constructed on first use, so:

  - Default-install users (ENABLE_SYNC_PUSH_PULL=false, no manual sync
    triggered) never touch /romm/sync.
  - Users opting in get a clear, actionable RuntimeError pointing at
    the unwritable path and the env var to override, at the call site
    rather than buried in a startup stack trace.

Also document in env.template that enabling either sync mode requires
a writable volume at $ROMM_BASE_PATH/sync.
This commit is contained in:
Claude
2026-05-24 14:49:53 +00:00
parent 358e91d33c
commit 1890958ff2
3 changed files with 22 additions and 9 deletions
+16 -7
View File
@@ -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()
+4 -1
View File
@@ -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
+2 -1
View File
@@ -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