feat: add read-only resource and route discovery
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""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:
|
||||
local_path = roots.api_to_local(api_path)
|
||||
relative = PurePosixPath(api_path).relative_to(roots.api_root)
|
||||
except (ConfigError, ValueError):
|
||||
continue
|
||||
if not _lexically_within(local_path, roots.local_root):
|
||||
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
|
||||
Reference in New Issue
Block a user