synology_apm_repo.sdk.dedup.repository module¶
DedupRepo: opens one repository root and ties
RepoInfo/key material/Pool/CompositionReader together behind
open_file()/open_composition().
Repository-root path constants (FORMAT-SPEC.md: repo-root-layout’s fixed
directory names) are uniform across both layout kinds —
<repo_root>/@data/Pool, <repo_root>/@data/Composition,
<repo_root>/db/; only layout.repo_root itself differs (the vault
root directly for RepoKind.VAULT, or
@ActiveProtectData/<repoId> for RepoKind.OBJECT_STORE).
These are the same relative paths pool and keys already use.
- synology_apm_repo.sdk.dedup.repository.FILE_MAP_STATUS_COMPLETE = 2¶
file_map-status) — see
locate_file’s ownRaises:section for how each is handled.FILE_MAP_STATUS_COMPLETEis public:units.saas.streamalso needs it, to pre-filter forward-resolution’s own generation candidates down to oneslocate_filewill actually accept.- Type:
db/file_map.statusvalues (FORMAT-SPEC.md
- class synology_apm_repo.sdk.dedup.repository.FileLocation(stream_id, session_id, comp_offset, block, file_size)¶
Bases:
objectOne
db/file_maprow, resolved to whatDedupRepo.open_compositionneeds plus (when available)file_meta.file_size.statusis not carried here —locate_filealready rejects every row except Complete (FORMAT-SPEC.md: file_map-status) before constructing one.- comp_offset: CompOffset¶
- class synology_apm_repo.sdk.dedup.repository.DedupRepo(store, layout, info, dir_cache, *, vault_key=None, bucket_cache_size=16, chunk_cache_size=4096, verify_fingerprint=False)¶
Bases:
objectRounds out the module-level
Pool/CompositionReadermachinery with a small cache of read-onlydb/<name>sqlite connections — together, whatopen_file/open_compositionneed to hand back aDedupFile.- property store: ObjectStore¶
The underlying
ObjectStorethis repository was opened against — exposed for Catalog-Layer-and-above callers that occasionally need a raw path read outside thePool/CompositionReader/db()machinery (e.g. catalog’s object-store link-key display-name lookup, which lists@ActiveProtectKey/link/directly).
- property vault_key: bytes | None¶
The resolved VaultKey, if any (
Nonefor an unencrypted repository or one opened without key material) — exposed for Unit-Layer-and-above callers that need to decrypt something outside thePool/CompositionReadermachinery (e.g.DeviceProviderpeeling anaHlT-envelopedtarget.db).
- property dir_cache: DirCache¶
This repository’s shared
DirCache— exposed for the same reason asstore/vault_key— a caller outsidePool/CompositionReader(verify_checks.check_repo_info,units/verify_reachable.py’s top-down walk building its ownCompositionReader) needs to resolve a.<seqId>-suffixed path itself, without duplicating a second, uncachedlistdir.
- property comp_root: str¶
<repo_root>/@data/Composition— exposed sounits/verify_reachable.py’s top-down walk can build its ownCompositionReaderper visited composition record withoutopen_composition()’sDedupFilewrapping (verify wants the raw record/header, not a readable byte range).
- property pool_root: str¶
<repo_root>/@data/Pool— exposed for the same reason ascomp_root:units/verify_reachable.py’s top-down walk constructs its own privatePool(forcingverify_fingerprint/verify_ciphertext_crcon for the run, rather than mutating this repository’s own shared one), which needs this repository’spool_rootto do so.
- async classmethod open(store, layout, keys=None, *, bucket_cache_size=16, chunk_cache_size=4096, verify_fingerprint=False)¶
Open
layout. Cheap by construction — reads onlyrepo_info(and, ifkeysis given and the repository is encrypted, whateverKeyMaterial.resolve_vault_keytouches: one sqlite row or one small key file); never scans Pool or Composition.A
keyswhose GCM tag doesn’t check out raisesKeyMismatchErrorimmediately — better to fail here than to hand back a repository that will silently decrypt every chunk into garbage later (AES-CTR has no integrity check of its own).verify_fingerprint=Truemakes per-chunk.fgpverification this session’s default for every read throughpoolinstead of it being off by default — or pass it per-call there instead.
- async db(name)¶
Open (and cache) a connection to
db/<name>.Read-only against the store either way: the fast path opens the real file immutable, and the slow path opens a private materialized copy read-write (see
storage/sqlite.py) so an index hint can take effect — writes there never reach the store.nameis resolved throughPHYSICAL_NAME_ALIASESfirst (e.g."copy_target_file"has no on-disk object of its own; its table lives inside whichever generation"copy_target_version"resolves to), so requesting either aliased name transparently shares one connection.On a
RepoKind.OBJECT_STORElayout, the real generation is selected by FORMAT-SPEC.md: generation-selection’s transaction-log algorithm (resolve_generation), not the naive “largest.<N>suffix” rule every other per-generation file uses — adb/<name>.<N>generation can exist on disk before the transaction referencing it commits.db/<name>is always raw (neveraHlT/zstd-enveloped), soSqliteSource.from_raw_storeskips straight to materializing it — no envelope detection needed here, unlike every other envelope→SQLite path in this project.
- async locate_file(path)¶
Look up
pathindb/file_mapand supplement it withfile_meta.file_sizewhen a matching row exists there too.file_meta.pathis only unique perconnection_config_id— this takes the first match, fine for the common single-workload case but a placeholder pending the Catalog Layer’s proper disambiguation.- Raises:
NotFoundError – no row for
pathat all, or itsstatusis Initialized/Written/Compacted (0/1/3) — not yet, or no longer, resolvable content (FORMAT-SPEC.md: file_map-status).DataCorruptError – the row’s
statusis Corrupted/Tainted (4/5) — the format itself flags this data as known-bad.
- async file_map_paths_with_prefix(prefix, *, status=None)¶
Every
db/file_mappath starting withprefix, via aLIKE-prefix scan —file_map.pathis the table’s own primary key, so SQLite resolves this as an indexed range scan, not a table scan.%/_/\inprefixare escaped so they match literally rather than as SQL wildcards.status, when given, restricts to rows at exactly thatfile_map.statusvalue (FORMAT-SPEC.md: file_map-status) — unlikelocate_file, this method applies no status filtering on its own.
- async open_file(path)¶
Resolve
pathviadb/file_map(+file_meta.file_sizewhen available) and return a ready-to-readDedupFile.- Raises:
NotFoundError – see
locate_file.DataCorruptError – see
locate_file.
- open_composition(stream_id, session_id, comp_offset, size=None)¶
Construct a
DedupFiledirectly from a(stream_id, session_id, comp_offset)triple — the path any workload-specific Unit Layer provider ultimately takes once it has resolved its own metadata down to this triple.
- async probe_encrypted()¶
Cheaply determine whether this repository is actually vault-encrypted — no key needed at all, and never raises
KeyRequiredError. Reads the repository’s own encryption-key record directly (probe_encrypted) — never opens a bucket file, never touches the Pool. Not the compile-timeRepoInfo.encrypt_algorithmfield (seeBucketFileHeader.is_vault_encrypted’s docstring for why that’s unreliable); this reads the same live, per-repository recordKeyMaterial.resolve_vault_keyalready reads to find one specific candidate key’s wrapped VaultKey, just its latest entry rather than one looked up by id.Nonemeans “couldn’t tell” (the encryption-key record itself is entirely absent, which should not happen for a properly initialized repository) — a genuinely different answer from “confirmed not encrypted” (False), the same distinctionKeyStatus’s ownNO_KEY_PROVIDEDdraws for the same reason.Result is cached for the lifetime of this (read-only, never-changes-under-us)
DedupRepo— repeated calls cost nothing after the first.
- async close()¶
Release every cached sqlite connection (and any temp file/ directory a slow-path materialization created), then drop this repository’s in-memory Pool caches.
Settles every in-flight
db()fetch first, not just what’s already landed in_db_sources– a fetch cancelled mid-flight (a TUI worker torn down while a catalog load was still running, say) can already have opened a real, connectedSqliteSourcethat nothing else references; skipping straight to.values()would abandon exactly that connection’s aiosqlite background thread forever — the same reasoningapi.repository.Repository.close()applies to its own dedup-catalog cache one layer up.Every source gets a close attempt regardless of whether an earlier one raised or hung – same “attempt all, then report” posture as
Repository.close(), for the same reason.