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:
objectOne 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— aNodeRef.repo_path, stamped from whichever catalog-levelRepoLayouta node’s own provider actually opened (seecatalog_repo_layouts()) — names one of this repository’s own catalogs. Never just comparing againstself.layout.repo_root: that’s the bucket-level root, which diverges from a catalog’s own root onceself.layout.catalog_idsis enumerable (layout.repo_rootstays the bucket root while each catalog’s own root gains an@ActiveProtectData/<repo-id>suffix) — the caseSession._repo_for_refneeds this for.
- property is_encrypted: bool | None¶
Whether this repository is actually encrypted, or
Nonewhen that genuinely couldn’t be determined — thin delegation toKeyManager.is_encrypted.
- property key_status: KeyStatus¶
The precomputed
KeyStatus— a plain, no-I/O property; thin delegation toKeyManager.status.
- property key_verification: KeyVerification | None¶
The GCM-unwrap verification result, or
Nonewhen no key was ever provided — thin delegation toKeyManager.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
Repositorystarts using the new key for every subsequent call: every already-openedDedupRepois closed and replaced (so a staleCataloga 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 reportskey_status == INVALID, never reverting toNO_KEY_PROVIDEDor 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) —KeyManageritself 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_configrows (sharing one physical dedup pool), or one per independently-opened object-storage sibling repo-id — wrapped uniformly asCatalog. Opens every not-yet-opened catalog concurrently (asyncio.gather, the same patternunits/device_pcps.pyalready uses for concurrent PC/PS fragment opens) rather than one at a time; each opened catalog’s ownconnections()listing is already resolved once, by_open_catalog_resources, and cached on its_OpenCatalogbundle for thisDedupRepo’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 givenself._require_key_verifiedas a callback, so they still gate before doing anything a wrong/ missing key would make meaningless): the rows this reads (connection_config, viaconnections()) are genuinely unencrypted, plaintext regardless of encryption, and opening aDedupRepoitself 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 corruptrepo_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.CancelledErrorincluded — 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
Catalogmatchingcatalog_id— opening only the specificDedupRepo``(s) that could possibly match, never every sibling the way ``catalogs()’s own full listing does. ForOBJECT_STORE,catalog_iddirectly names one specificRepoLayout’s ownrepo_id, so every other sibling is skipped without ever opening it; forVAULT(whoseRepoLayout.repo_idis alwaysNone) there’s only ever the one index to check anyway. A candidate that can’t be ruled out byrepo_idalone but then fails to open (a corruptrepo_info, a rejected key) is not treated as “not it, keep looking” — the failure is re-raised immediately, the same ascatalogs(). Used by canonical-ref resolution (_version_for_canonical_ref) and by a caller (e.g. the TUI, re-fetching one already-known catalog afterset_key()) that wants one specific catalog without paying forcatalogs()’s own full-bucket listing — a hot enough path (every CLIls/tree/cat/exporton a canonical ref, andRepository.resolve()’s ownCANONICALbranch) 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
workloadhas a chance at an application-layer provider —Truedoesn’t guarantee every individual version actually resolves, only that the workload type is oneprovider_for/saas_provider_forrecognize at all. The one place the CLI’sdoctorcommand needs this, so this is the one facade method exposing it — never importunits.dispatchdirectly 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) — aRAWref’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.resolveadds picking the right repository among several open ones.ref.repo_pathis 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 owningCatalogandVersion— the first half of whatresolvedoes for a canonical ref, exposed on its own for callers that need the version’s ownproviderrather than one specific node inside it (e.g. the CLI’sls/tree, which list a resolved node’s children). TheCatalogis part of the result (not just theVersion) because building a provider needs to know which catalog’s ownDedupRepoto 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
segmentsas far as they go, through the four levels catalog -> workload -> version -> item tree, raisingNotFoundErrorthe moment a segment doesn’t match anything at its level.object_db_idis forwarded toprovideroncesegmentsreaches 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) andcli/browse.py’sls/treebreadcrumbs (which must also stop at an intermediate depth that doesn’t yet name a completeNode/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) areCatalog.walk_human_ref’s own job, since they no longer need anything fromRepositoryonce the catalog is chosen.
- async verify(level=VerifyLevel.QUICK, *, progress=None)¶
Integrity check over every catalog this repository holds — each distinct
DedupRepoverified exactly once (not once perCatalog): a vault’s ownconnection_configrows all share one physical pool, so naively loopingCatalog.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 ascatalogs(), 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 runsunits.verify_reachable.verify_reachable()once per opened instance and concatenates everyFinding— the top-down, reachability-scoped walk (Catalog -> Workload -> Version -> that version’s own composition records).progressis optional and forwarded to each call as-is.Gated on
_require_key_verified()the same asCatalog. workloads()/versions()—units.catalog.versions()’s own browsable-status filter silently drops every row it can’t decryptversion_specfor (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 identicalPoolDescriptor(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 eachverify_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 onediscover()/discover_remote()call’s repositories can share one store, so onlySession(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 callSession.close_repo()instead of a barerepo.close().