Source code for exlab_wizard.sync.verifier
"""SHA-256 verifier wrapper around ``rclone check --download``.
Rclone-only NAS sync migration (2026-05-26). The verifier is now a thin
adapter over :meth:`exlab_wizard.sync.transports.rclone.RcloneDriver.check`
that translates a :class:`CheckResult` into the :class:`VerifyResult`
shape the queue worker already consumes. The actual integrity guarantee
comes from ``rclone check --download --combined`` -- rclone streams the
remote files back to the wizard, hashes them locally, and writes a
``=/*/+/-/!`` line per file to a tempfile that the driver parses.
The Slot A SHA-256 capture (the durable
``sync_state.json:files[*].verified_sha256`` field) is owned by
:func:`exlab_wizard.sync.nas_client._compute_local_shas` -- the verifier
itself never sees a local SHA. This keeps the verifier a leaf abstraction
that depends only on the rclone driver, and keeps the SHA-capture logic
local to the one code path that has access to the freshly-read local
bytes.
The previous Python SHA pipeline and the durable
``<run>/.exlab-wizard/checksums.sha256`` artefact are gone (the spec
chose `rclone check` as the authority on integrity at sync time).
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
from exlab_wizard.logging import get_logger
from exlab_wizard.sync.transports import TransportError, TransportErrorKind
if TYPE_CHECKING:
from exlab_wizard.sync.transports.rclone import CheckResult, RcloneDriver
__all__ = ["Verifier", "VerifyResult"]
_log = get_logger(__name__)
[docs]
@dataclass(frozen=True, slots=True)
class VerifyResult:
"""Outcome of one ``rclone check --download`` pass against a remote.
``ok`` is True iff no files differ, none are missing on the
destination, and the rclone subprocess reported no per-file errors.
Files in ``mismatched`` / ``missing`` / ``errors`` are run-relative
POSIX paths drawn from the ``--combined`` output. ``extra`` lists
files present on the destination but not in the source -- it does
not flip ``ok`` (it is informational, mirroring the pre-migration
contract).
``error_kind`` is set when the rclone subprocess itself failed
(auth / network / unknown) before producing usable combined
output. The queue worker keys off this field to route through the
spec §7.1.5 retry policy.
"""
ok: bool
mismatched: tuple[str, ...] = ()
missing: tuple[str, ...] = ()
extra: tuple[str, ...] = ()
errors: tuple[str, ...] = ()
error_kind: TransportErrorKind | None = None
# Verified rel-paths (the rclone ``=`` lines). The worker uses this
# set when crediting files into ``sync_state.json`` so the reconcile
# path does not have to re-derive it from ``job.files`` minus the
# bad subsets.
verified: tuple[str, ...] = ()
[docs]
@classmethod
def from_check_result(cls, check_result: CheckResult) -> VerifyResult:
"""Translate a successful ``rclone check`` into a :class:`VerifyResult`.
``ok`` is True iff nothing differs, nothing is missing on the
destination, and rclone reported no per-file errors. ``extra``
(present on the destination only) is carried for reporting but
does not flip ``ok``. The single source of truth for the
``CheckResult -> VerifyResult`` mapping shared by the queue
worker, the ``force_verify`` path, and :meth:`Verifier.verify`.
"""
ok = not check_result.differ and not check_result.missing_on_dst and not check_result.errors
return cls(
ok=ok,
mismatched=check_result.differ,
missing=check_result.missing_on_dst,
extra=check_result.extra_on_dst,
errors=check_result.errors,
verified=check_result.equal,
)
[docs]
class Verifier:
"""Verifier: ``rclone check`` wrapper, no Python SHA pipeline.
Constructed with an :class:`RcloneDriver`; production callers pass
the same driver instance the push path uses so subprocess settings
(binary path, etc.) stay consistent. Tests can pass a stub driver
whose ``check`` returns a canned :class:`CheckResult`.
"""
def __init__(self, driver: RcloneDriver | None = None) -> None:
if driver is None:
from exlab_wizard.sync.transports.rclone import RcloneDriver as _Driver
driver = _Driver()
self._driver = driver
[docs]
async def verify(
self,
run_path: Path,
remote: str,
*,
files_from: Path,
) -> VerifyResult:
"""Run ``rclone check --download`` over ``files_from`` and translate.
Raises :class:`TransportError` from the driver only on a spawn
failure (the rclone binary is missing); every other failure mode
-- auth / network / hash-mismatch -- is folded into the returned
:class:`VerifyResult`.
"""
try:
check_result = await self._driver.check(
run_path,
remote,
files_from=files_from,
)
except TransportError as exc:
_log.warning("rclone check transport error: %s", exc)
return VerifyResult(
ok=False,
error_kind=exc.error_kind,
)
return VerifyResult.from_check_result(check_result)