synology_apm_repo.sdk.units.content.pcps_disk module¶
VirtualDiskContentSource reassembles a PC/PS physical disk’s
independently-registered per-region dedup objects (“fragments”) into one
ContentSource that reads/exports/streams like a single disk image,
matching VM’s own disk_image model.
Unlike VM (one disk = one object_table row = one composition),
PC/PS’s disk-analyzer segments one physical disk into several regions
at backup time, each its own independently-registered dedup object
(FORMAT-SPEC.md: pcps-fragments). Two facts this class depends on: a fragment’s
chunk-map offsets are disk-absolute, not relative to its own start, so
opening its DedupFile with the whole disk’s size already gives a
complete, correctly-sparse view of the entire disk on its own; and
db/file_meta.file_size is the whole disk’s capacity (identical across
every sibling fragment), never one fragment’s own real length — that has
to be asked of its own composition directly
(CompositionRecord.extent).
This class’s only real job is routing a read to whichever fragment
covers it, and driving a whole-disk export fragment-by-fragment through
export_scheduler.export_to’s dst_offset mechanism — never through
VirtualDiskContentSource.read, which stays a browsing/preview path
only.
- class synology_apm_repo.sdk.units.content.pcps_disk.DiskFragment(fid, start, end, dedup_file, src_file_path)¶
Bases:
objectOne PC/PS per-region object, already resolved down to a ready-to-read
DedupFileand the disk-absolute[start, end)range its own composition actually covers (CompositionRecord.extent).
- class synology_apm_repo.sdk.units.content.pcps_disk.VirtualDiskContentSource(size, fragments)¶
Bases:
objectReassembles a PC/PS physical disk’s independently-addressed fragments into one
ContentSourcespanning the whole disk ([0, size)), with the samesize/read/stream/export_toshape as a VM’s single-compositiondisk_image— so nothing above this layer needs a PC/PS-specific branch.Fragments genuinely overlap in real data — a fragment’s own composition covers whatever whole 4096-byte chunks its capture touched, and the chunk straddling two regions’ boundary can carry real data in the later region while the earlier region pads the same range with a
ZEROdeclaration (its capture stopped at the true, unaligned boundary). The rule this class applies: when more than one fragment covers a byte, the fragment with the higherstartwins — a later region’s real capture takes precedence over an earlier region’s boundary-padding zero declaration.read/export_toboth get this “for free” by writing/exporting fragments in ascending-startorder, so a later fragment’s write naturally overwrites an earlier one’s in any shared range.- async read(offset=0, length=None)¶
See
units.base.ContentSource.read’s EOF contract — a request extending past this disk’s ownsizeis clamped, never an error.
- stream(block=8388608)¶
- property supports_concurrent_export: bool¶
Always
True—export_to()accepts and forwardsmax_concurrent_reads/max_concurrent_opensto each of its own fragments.
- async export_to(dst, *, sparse=True, progress=None, max_concurrent_opens=None, max_concurrent_reads=1)¶
Export the whole disk by walking fragments in order and handing each to
export_scheduler.export_to’s own bucket-major writer, landing at its disk-absolutedst_offsetinside one pre-sizeddstfile — never throughread. Fragments export sequentially, one at a time;max_concurrent_opens/max_concurrent_readsonly add concurrency within each fragment’s own bucket-major walk (the same two knobs a VM’sdisk_imageexposes — seechunk_walk.py). Gaps between/around fragments (_gaps) are real holes: left unwritten in the pre-sized destination whensparse=True(presized_file.create_presized), explicitly zero-filled otherwise. Every fragment shares oneBucketReaderCachefor this call, bounded toDEFAULT_BUCKET_CACHE_SIZE(16, the same boundchunk_walk.py/export_scheduler.pyshare so the three don’t each drift with a separately hardcoded copy) rather than left unbounded — a multi-fragment disk touching many distinct buckets would otherwise grow this cache proportionally to the total number of buckets touched across every fragment, so bucket-locality reuse persists across fragments only up to that cap.ExportResult.holes/zeros/bytes_writtenare summed across fragments independently, so a byte range more than one fragment declares (adjacent regions’ZEROtails agreeing) is counted more than once — the written file is still byte-correct, only the reported totals can run slightly ahead of the disk’s true size.Shares one multiprocess executor across every fragment’s own
export_to()call instead of letting each spin one up independently — the same “one executor per logical export, not per inner call” contractRepository.verify()’s own multi-catalog fan-out follows, for the same reason (a fragment’s own pool-spawn cost would otherwise be paid once per fragment). Built only when every fragment actually resolves to the identicalPoolDescriptor(same store/pool_root/vault key) — true for every real disk today (every fragment belongs to the same repository/pool), checked rather than assumed.