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: object

One PC/PS per-region object, already resolved down to a ready-to-read DedupFile and the disk-absolute [start, end) range its own composition actually covers (CompositionRecord.extent).

fid: int
start: int
end: int
dedup_file: DedupFile
src_file_path: str

Kept for diagnostics/attrs display only — never consulted for addressing (that’s what start/end are for).

class synology_apm_repo.sdk.units.content.pcps_disk.VirtualDiskContentSource(size, fragments)

Bases: object

Reassembles a PC/PS physical disk’s independently-addressed fragments into one ContentSource spanning the whole disk ([0, size)), with the same size/read/stream/ export_to shape as a VM’s single-composition disk_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 ZERO declaration (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 higher start wins — a later region’s real capture takes precedence over an earlier region’s boundary-padding zero declaration. read/export_to both get this “for free” by writing/exporting fragments in ascending-start order, 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 own size is clamped, never an error.

stream(block=8388608)
property supports_concurrent_export: bool

Always True — export_to() accepts and forwards max_concurrent_reads/max_concurrent_opens to 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-absolute dst_offset inside one pre-sized dst file — never through read. Fragments export sequentially, one at a time; max_concurrent_opens/ max_concurrent_reads only add concurrency within each fragment’s own bucket-major walk (the same two knobs a VM’s disk_image exposes — see chunk_walk.py). Gaps between/around fragments (_gaps) are real holes: left unwritten in the pre-sized destination when sparse=True (presized_file.create_presized), explicitly zero-filled otherwise. Every fragment shares one BucketReaderCache for this call, bounded to DEFAULT_BUCKET_CACHE_SIZE (16, the same bound chunk_walk.py/export_scheduler.py share 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_written are summed across fragments independently, so a byte range more than one fragment declares (adjacent regions’ ZERO tails 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” contract Repository.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 identical PoolDescriptor (same store/pool_root/vault key) — true for every real disk today (every fragment belongs to the same repository/pool), checked rather than assumed.