synology_apm_repo.sdk.storage.layout module

Repository layout detection (FORMAT-SPEC.md: repo-root-layout).

Two shapes exist, both placed by fixed, product-enforced logic — never nested arbitrarily deep:

  • Vault (APV, or any local-filesystem Copy destination): a directory (conventionally @ActiveProtectVault, though the name itself isn’t load-bearing — detection is by content marker, not name) is the repository root directly, created exactly one level under the admin-chosen shared folder. Identified by the coexistence of repo_info, link.key and .fully_created at that exact path. Exactly one repository per such root, and exactly one vault per shared folder.

  • Object storage (S3/Azure landed copies): the root contains an @ActiveProtectData/<12-char-repo-id>/ subtree, possibly with several sibling <repo-id> directories, and a parallel @ActiveProtectKey/{userKey,link}/ tree one level up from @ActiveProtectData. Each <repo-id> is an independent repository sharing the same key tree.

    repo_info under an object-store repository root is not a reliable marker by itself — it may carry a .<N> generation suffix and have no bare-named file at all. db/ and @data are both always present as bare-named directories regardless of generation suffixing, so the pair of them is the marker used here — requiring both, not just db, reduces the chance of a false positive on some unrelated directory that merely happens to contain a db subdirectory of its own.

Since neither shape is ever nested more than one level under whatever an admin actually provisioned (a shared folder, a bucket), a connection pointed anywhere from that level down to two levels above it still finds every repository. _DEFAULT_MAX_DEPTH bounds the walk at exactly that — not the fully open-ended search a misdirected scan of an unrelated directory tree could run away into, but not artificially limited to a single level either.

class synology_apm_repo.sdk.storage.layout.RepoKind(value)

Bases: Enum

Which of the two physical repository backends a RepoLayout describes.

VAULT = 'vault'
OBJECT_STORE = 'object_store'
class synology_apm_repo.sdk.storage.layout.RepoLayout(kind, repo_root, key_root=None, repo_id=None)

Bases: object

Where one repository lives within an ObjectStore.

repo_root and key_root are store-relative paths ("" means “the store’s own root”), never absolute paths — an ObjectStore has no such notion.

kind: RepoKind
repo_root: str
key_root: str | None = None
repo_id: str | None = None
async synology_apm_repo.sdk.storage.layout.iter_layouts(store, root='', *, max_depth=2)

Walk down from root looking for repository roots, yielding each as it is found.

Cheap by construction: only exists()/listdir() calls at each level, never a scan into Pool/Composition/db. A vault root or object-store bucket root ends the walk along that branch (no repository nests inside another); max_depth bounds how far it goes otherwise, at the 2-level default derived in this module’s docstring above.

Total result count is unknowable in advance — callers doing interactive discovery should treat this as an indeterminate-progress operation and consume it lazily.

async synology_apm_repo.sdk.storage.layout.detect_layout(store, root='')

Detect the single repository layout directly at root — for the common case where the caller already knows root is exactly one repository (e.g. it was chosen from a prior iter_layouts scan).

Raises NotFoundError if root is not itself a repository root, or is an object-store bucket root containing more than one (or zero) repository ids — callers facing that ambiguity should use iter_layouts instead.

class synology_apm_repo.sdk.storage.layout.RepositoryLayout(kind, repo_root, key_root=None, catalog_ids=None)

Bases: object

Where one Repository (a bucket, or a vault’s own shared folder) lives within an ObjectStore.

repo_root/key_root are store-relative paths ("" means “the store’s own root”), never absolute — same convention as RepoLayout.

catalog_ids is populated for OBJECT_STORE by listing @ActiveProtectData/’s children (cheap — already known at detection time, the same listing _layouts_at’s own second branch already does); left None for VAULT, whose catalogs (db/connection_config rows) can only be found later, by querying after opening — and also left None for the one genuinely ambiguous OBJECT_STORE case: root is itself an individual repository directory with no derivable bucket-root ancestor (see _data_dir_ancestor), where the one implicit catalog living there can’t be independently identified either. An empty list (as opposed to None) means a real, listable @ActiveProtectData was found but currently has zero valid repo-id children.

kind: RepoKind
repo_root: str
key_root: str | None = None
catalog_ids: list[str] | None = None
async synology_apm_repo.sdk.storage.layout.iter_repository_layouts(store, root='', *, max_depth=2)

Walk down from root looking for Repository roots (a bucket, or a vault’s own shared folder), yielding each as it is found — the Repository-level counterpart to iter_layouts.

Unlike iter_layouts, a bucket holding several sibling catalogs yields one RepositoryLayout (with catalog_ids listing every sibling found), never several — there is no “which repository id” ambiguity at this level to resolve.

synology_apm_repo.sdk.storage.layout.catalog_repo_layouts(layout)

The individual, directly-openable RepoLayout(s) layout resolves to — exactly what DedupRepo.open() already consumes unchanged; this is the only place RepositoryLayout and RepoLayout meet.

A vault (or the one genuinely ambiguous OBJECT_STORE case — catalog_ids is None) opens as a single RepoLayout at layout.repo_root itself: a vault’s own catalogs come from querying db/connection_config after opening, not a separate directory per catalog. Object storage opens one RepoLayout per catalog_ids entry, each rooted at that catalog’s own @ActiveProtectData/<repo-id> subdirectory — every sibling shares layout.key_root, since the key tree lives one level above @ActiveProtectData, not inside any one repo-id’s own directory.

Returns an empty list only when layout.catalog_ids is itself a real, empty list (a listable @ActiveProtectData with zero valid repo-id children) — nothing to open at all in that case.

synology_apm_repo.sdk.storage.layout.key_probe_layout(layout)

A throwaway RepoLayout carrying only the fields dedup.keys’ free functions (probe_encrypted/resolve_vault_key/verify) actually read for key/encryption resolution, straight off a bucket-rooted RepositoryLayout — safe to build with no catalog opened at all: VAULT reads repo_root (a vault’s own repo_root is the same value either way), OBJECT_STORE reads only key_root (shared by every sibling catalog, never repo_root). Used both before a Repository exists yet (api.session’s own discovery-time key/encryption probe) and by an already-constructed one (Repository.set_key()) — the one shared place this derivation lives, rather than two copies drifting apart.

async synology_apm_repo.sdk.storage.layout.detect_repository_layout(store, root='')

Detect the single Repository layout directly at root — the Repository-level counterpart to detect_layout.

Raises NotFoundError if root is not itself a repository root. Unlike detect_layout, never raises for “several repository ids found” — that’s no longer ambiguous at the Repository level, it’s catalog_ids having length greater than one; a caller wanting a single, already-disambiguated catalog picks one from catalog_ids (or queries them after opening, for a vault) rather than re-scanning at a narrower root.