synology_apm_repo.sdk.api.repository module

Repository: one opened repository’s catalog/provider facade, part of the Repository Layer (the Session/Repository/Catalog split CLI/TUI code imports directly). Session (discovery/lifetime) lives in the sibling api.session module; Catalog/Frame (one catalog’s own workload/version/provider operations) live in the sibling api.catalog module — this module holds Repository itself plus the meeting points that need both (resolve()’s canonical/human-ref dispatch, in particular).

class synology_apm_repo.sdk.api.repository.Repository(store, layout, keys, key_verification, *, encrypted=None)

Bases: object

One opened repository: cheap metadata plus the catalog/provider calls that turn it into a browsable tree. Never constructed directly by callers — obtained from Session.discover/Session.open.

property layout: RepositoryLayout
owns_repo_path(repo_path)

Whether repo_path — a NodeRef.repo_path, stamped from whichever catalog-level RepoLayout a node’s own provider actually opened (see catalog_repo_layouts()) — names one of this repository’s own catalogs. Never just comparing against self.layout.repo_root: that’s the bucket-level root, which diverges from a catalog’s own root once self.layout.catalog_ids is enumerable (layout.repo_root stays the bucket root while each catalog’s own root gains an @ActiveProtectData/<repo-id> suffix) — the case Session._repo_for_ref needs this for.

property is_encrypted: bool | None

Whether this repository is actually encrypted, or None when that genuinely couldn’t be determined — thin delegation to KeyManager.is_encrypted.

property key_status: KeyStatus

The precomputed KeyStatus — a plain, no-I/O property; thin delegation to KeyManager.status.

property key_verification: KeyVerification | None

The GCM-unwrap verification result, or None when no key was ever provided — thin delegation to KeyManager.verification.

async set_key(key_string)

Try a new key string against this repository — the interactive “paste a key, see if it’s correct” workflow. On success, this Repository starts using the new key for every subsequent call: every already-opened DedupRepo is closed and replaced (so a stale Catalog a caller obtained before this call fails cleanly on its next use rather than silently keeping the old key’s data); a catalog not yet opened simply picks up the new key on its own eventual first open. On failure, every already-open connection is left exactly as it was — a rejected attempt reports key_status == INVALID, never reverting to NO_KEY_PROVIDED or corrupting what was already open.

The one place that legitimately spans both key state (self._key_manager) and catalog lifecycle (self._open_catalogs, via _reopen_catalogs_under_new_key) — KeyManager itself owns only the pure key/status state and has no state for the catalog-reopen half of this, so that orchestration stays here instead.

async catalogs()

Every catalog this repository holds — a vault’s own connection_config rows (sharing one physical dedup pool), or one per independently-opened object-storage sibling repo-id — wrapped uniformly as Catalog. Opens every not-yet-opened catalog concurrently (asyncio.gather, the same pattern units/device_pcps.py already uses for concurrent PC/PS fragment opens) rather than one at a time; each opened catalog’s own connections() listing is already resolved once, by _open_catalog_resources, and cached on its _OpenCatalog bundle for this DedupRepo’s whole lifetime, so no I/O happens here beyond opening a not-yet-opened sibling.

Never gated on the key, unlike Catalog.workloads()/ versions() (each given self._require_key_verified as a callback, so they still gate before doing anything a wrong/ missing key would make meaningless): the rows this reads (connection_config, via connections()) are genuinely unencrypted, plaintext regardless of encryption, and opening a DedupRepo itself never requires a key either (DedupRepo.open() only raises for a key that was given and didn’t resolve, never for no key at all) — so nothing here actually needs one. This also preserves the TUI’s own existing flow: catalog names show up before any key prompt, which only appears once the user actually opens one.

A specific catalog’s own DedupRepo.open() failure — a corrupt repo_info, a rejected key (KeyMismatchError), a transient storage I/O error, or a caller cancelling this call — is surfaced by re-raising it immediately, the same as any other method on this class: a caller must never mistake “one sibling is broken” for “this bucket only has N-1 catalogs.” This applies uniformly, asyncio.CancelledError included — no exception raised while opening a sibling is ever treated as “skip it and keep going.

async catalog_by_id(catalog_id)

Resolve exactly the one Catalog matching catalog_id — opening only the specific DedupRepo``(s) that could possibly match, never every sibling the way ``catalogs()’s own full listing does. For OBJECT_STORE, catalog_id directly names one specific RepoLayout’s own repo_id, so every other sibling is skipped without ever opening it; for VAULT (whose RepoLayout.repo_id is always None) there’s only ever the one index to check anyway. A candidate that can’t be ruled out by repo_id alone but then fails to open (a corrupt repo_info, a rejected key) is not treated as “not it, keep looking” — the failure is re-raised immediately, the same as catalogs(). Used by canonical-ref resolution (_version_for_canonical_ref) and by a caller (e.g. the TUI, re-fetching one already-known catalog after set_key()) that wants one specific catalog without paying for catalogs()’s own full-bucket listing — a hot enough path (every CLI ls/tree/cat/export on a canonical ref, and Repository.resolve()’s own CANONICAL branch) that opening every sibling just to find the one already-known id would be real, avoidable I/O on an object-storage bucket with several.

workload_is_supported(workload)

A plain, no-I/O check for whether workload has a chance at an application-layer provider — True doesn’t guarantee every individual version actually resolves, only that the workload type is one provider_for/saas_provider_for recognize at all. The one place the CLI’s doctor command needs this, so this is the one facade method exposing it — never import units.dispatch directly from CLI/TUI code.

async file_map_tree()

The diagnostic fallback axis of last resort — browsable even when catalog metadata is missing or unhelpful (e.g. an empty copy_meta_file). Always the first catalog (_catalog_layouts[0], opened here if not already) — a RAW ref’s grammar carries no catalog segment at all, so this doesn’t disambiguate between object-storage siblings; a pre-existing limitation of the diagnostic-only raw axis.

async invalidate_directory_cache()

Drop every cached directory listing for every catalog this repository has opened so far — for a caller (the TUI’s “refresh” action) that wants its next provider/catalog call to re-scan the store instead of answering from whatever was listed earlier this session. A catalog not yet opened has nothing cached to drop.

async resolve(ref, *, object_db_id=None)

Turn a NodeRef (or its string form) into a live node/unit, re-derived from cheap catalog/provider-tree calls rather than stored anywhere. Callable directly here — the common one-repository-per-run CLI case; Session.resolve adds picking the right repository among several open ones. ref.repo_path is ignored entirely (canonical/human refs never need it).

async version_for_ref(node_ref)

Resolve a canonical ref’s cat:/wl:/ver: prefix down to its owning Catalog and Version — the first half of what resolve does for a canonical ref, exposed on its own for callers that need the version’s own provider rather than one specific node inside it (e.g. the CLI’s ls/tree, which list a resolved node’s children). The Catalog is part of the result (not just the Version) because building a provider needs to know which catalog’s own DedupRepo to dispatch against — no longer implied by “the repository’s one dedup catalog” now that a repository can hold several.

async walk_human_ref(segments, *, object_db_id=None)

Walk a human ref’s segments as far as they go, through the four levels catalog -> workload -> version -> item tree, raising NotFoundError the moment a segment doesn’t match anything at its level. object_db_id is forwarded to provider once segments reaches a version — it’s a no-op until then.

The shared primitive behind both resolve’s human-ref branch (drains straight to a leaf/subtree) and cli/browse.py’s ls/tree breadcrumbs (which must also stop at an intermediate depth that doesn’t yet name a complete Node/ RestorableUnit, so a bare catalog or workload name is not itself an error here).

Only this first level (picking the right Catalog) lives here — the remaining three levels (workload/version/item) are Catalog.walk_human_ref’s own job, since they no longer need anything from Repository once the catalog is chosen.

async verify(level=VerifyLevel.QUICK, *, progress=None)

Integrity check over every catalog this repository holds — each distinct DedupRepo verified exactly once (not once per Catalog): a vault’s own connection_config rows all share one physical pool, so naively looping Catalog.verify() per row would re-run the identical whole-pool check that many times. Opens every not-yet-opened catalog first (same concurrent-open shape as catalogs(), including the same “a specific catalog’s own open failure aborts the call” posture — silently completing an integrity check that quietly skipped one whole catalog would misrepresent what was actually verified), then runs units.verify_reachable.verify_reachable() once per opened instance and concatenates every Finding — the top-down, reachability-scoped walk (Catalog -> Workload -> Version -> that version’s own composition records). progress is optional and forwarded to each call as-is.

Gated on _require_key_verified() the same as Catalog. workloads()/versions() — units.catalog.versions()’s own browsable-status filter silently drops every row it can’t decrypt version_spec for (indistinguishable, from inside that filter, from a genuinely non-browsable version), so an encrypted repository verified without a key would otherwise walk zero versions and report a misleadingly clean result instead of refusing to run.

At FULL level with more than one catalog to verify, shares one multiprocess executor across every verify_reachable() call instead of letting each spin one up independently — but only when every catalog actually resolves to the identical PoolDescriptor (same store, pool_root, vault key): a vault’s own sibling catalogs always share one physical pool, so this is the common case, but an object-storage repository’s sibling repo-ids can each be a genuinely separate store/pool — when they differ, this falls back to each verify_reachable() call building (and tearing down) its own executor, exactly as it already does with no shared one given.

async close()

Close every tracked provider and DedupRepo, on every path including errors.

Idempotent: a second call is a no-op. Session.close() closes every repository it ever yielded, including one a caller already closed early itself to release its resources ahead of the rest of a longer-running session – a caller doing that doesn’t need its own bookkeeping to avoid a redundant second close.

This never touches this repository’s own ObjectStore – by design, since one discover()/discover_remote() call’s repositories can share one store, so only Session (which tracks every store across every repository) can tell whether it’s safe to release. A caller that wants the store released early too, not just this repository’s own catalog/provider state, should call Session.close_repo() instead of a bare repo.close().