"""Normalize safe, pairwise Syncthing folders into local route records.""" from __future__ import annotations from datetime import datetime, timezone from pathlib import Path, PurePosixPath from typing import Any from archive_clients.config import ConfigError, RootMapping from archive_control.v1 import route_pb2 def discover_routes( configuration: dict[str, Any], local_device_id: str, roots: RootMapping, sparse_supported: bool, observed_at: datetime | None = None, ) -> list[route_pb2.LocalRoute]: folders = configuration.get("folders") if not isinstance(folders, list) or not local_device_id: raise ValueError("Syncthing configuration or local device ID is invalid") observed_at = observed_at or datetime.now(timezone.utc) routes = [] for folder in folders: if not isinstance(folder, dict): continue folder_id = folder.get("id") api_path = folder.get("path") if not isinstance(folder_id, str) or not folder_id: continue if not isinstance(api_path, str): continue try: normalized_api_path = _normalize_api_path(api_path, roots) local_path = roots.api_to_local(normalized_api_path) relative = normalized_api_path.relative_to(roots.api_root) except (ConfigError, ValueError): continue if not _lexically_within( local_path, roots.local_root_for_api(normalized_api_path.as_posix()) ): continue if relative == PurePosixPath("."): continue route = route_pb2.LocalRoute( route_id=folder_id, local_relative_path=relative.as_posix(), local_syncthing_device_id=local_device_id, writable=bool(folder.get("type") == "sendreceive"), sparse_supported=sparse_supported, # Ownership is set only from a durable provisioning journal. A # folder label is never sufficient deletion attribution. archive_control_created=False, ) route.observed_at.FromDatetime(observed_at) folder_type = folder.get("type") route.folder_type = { "sendreceive": route_pb2.SYNCTHING_FOLDER_TYPE_SEND_RECEIVE, "sendonly": route_pb2.SYNCTHING_FOLDER_TYPE_SEND_ONLY, "receiveonly": route_pb2.SYNCTHING_FOLDER_TYPE_RECEIVE_ONLY, }.get(folder_type, route_pb2.SYNCTHING_FOLDER_TYPE_OTHER) raw_devices = folder.get("devices") device_ids = [] if not isinstance(raw_devices, list) else [ item.get("deviceID") for item in raw_devices if isinstance(item, dict) and isinstance(item.get("deviceID"), str) ] peers = sorted(set(device_ids) - {local_device_id}) route.peer_syncthing_device_ids.extend(peers) if folder.get("paused") is True: route.state = route_pb2.ROUTE_STATE_PAUSED route.detail = "Syncthing folder is paused" elif folder_type != "sendreceive": route.state = route_pb2.ROUTE_STATE_UNSUPPORTED route.detail = "route folder is not sendreceive" elif ( not isinstance(raw_devices, list) or len(raw_devices) != 2 or len(device_ids) != 2 or len(set(device_ids)) != 2 ): route.state = route_pb2.ROUTE_STATE_UNSUPPORTED route.detail = "route folder must contain exactly two devices" elif local_device_id not in device_ids: route.state = route_pb2.ROUTE_STATE_UNSUPPORTED route.detail = "route folder does not include the local device" else: route.state = route_pb2.ROUTE_STATE_DISCOVERED routes.append(route) return sorted(routes, key=lambda route: route.route_id) def _lexically_within(path: Path, root: Path) -> bool: try: path.relative_to(root) return True except ValueError: return False def _normalize_api_path( api_path: str, roots: RootMapping ) -> PurePosixPath: """Resolve Syncthing's home-relative folder notation under api_root.""" candidate = PurePosixPath(api_path) if candidate.parts and candidate.parts[0] == "~": candidate = roots.api_root.joinpath(*candidate.parts[1:]) return candidate