synology_apm_repo.sdk.storage.smb module

SmbStore — a real, executable ObjectStore implementation for a repository reached over the MS-SMB protocol (SMB2/3) rather than a locally-mounted share.

smbprotocol is imported lazily, inside __init__ and each method, rather than at module scope — the same rationale storage/s3.py’s module docstring gives for aioboto3: it’s always installed (a required dependency), but this module is imported unconditionally by storage/__init__.py, so a module-level import would make every caller of storage pay for it even when it never touches SMB.

Unlike S3Store/AzureStore, smbprotocol has no native-async surface (no aiohttp foundation to sit on) — its smbclient submodule is a synchronous, os-shaped API (open_file/stat/listdir, file objects with seek/read). This store’s four methods are thin asyncio.to_thread() wrappers around that synchronous body, the same shape LocalFsStore uses for os.pread/os.fstat/iterdir — not the native-async pattern the other two network backends use.

smbclient’s session/connection bookkeeping is process-wide by default (keyed by server/port), which would let aclose() on one SmbStore tear down a connection a sibling instance sharing the same server is still using. Every call here passes its own connection_cache dict instead, scoping each underlying SMB session to this instance alone — the same per-instance isolation S3Store/AzureStore get for free from owning their own client object.

This store keeps a small pool of independent sessions (_DEFAULT_CONNECTION_POOL_SIZE), not one shared session — see _ConnectionSlot and _call_with_retry for why: an SMB2 connection’s own client-side flow-control window (smbprotocol’s “credits”) starts at exactly 1 and only grows as the server grants more back on replies, so any two requests genuinely in flight at once on one connection race for that single credit and one of them gets a raised smbprotocol.exceptions. SMBException. Rather than track that live, per-connection credit count (which would mean reaching into smbprotocol internals this module otherwise stays well clear of), every operation is instead routed through one of a small number of independent connections, each never handling more than one in-flight request at a time — real, safe parallelism up to the pool size, without ever needing to know how large any one connection’s window currently is.

One contract difference from LocalFsStore, in the other direction from S3Store/AzureStore: an SMB share has real directory entities (like a local filesystem, unlike an object store’s prefixes), so listdir on a missing path raises NotFoundError, matching LocalFsStore rather than the S3/Azure prefix-probing behavior.

Unlike S3Store/AzureStore, smbprotocol gives this module no timeout/retry knobs of its own to configure past the initial connect — a stalled share would otherwise block a call forever, and neither a connect nor a later operation failure carries any signal distinguishing “worth retrying” from “never will be” — so this module adds both itself, via _DEFAULT_OPERATION_TIMEOUT/_DEFAULT_MAX_ATTEMPTS below.

class synology_apm_repo.sdk.storage.smb.SmbStore(share, *, server, port=445, username=None, password=None)

Bases: object

One SMB share, addressed by "/"-separated paths relative to the share root — the SMB equivalent of S3Store’s bucket/AzureStore’s container.

username accepts the Windows-native DOMAIN\username (or user@domain UPN) form directly; smbprotocol’s own NTLM/SPNEGO layer splits the domain back out of that single string, so this class has no separate domain field to keep in sync with it. Leaving username/password unset attempts an anonymous/guest session, same as leaving S3/Azure’s credential fields unset falls back to their own ambient credential chains.

Backed by a small pool of _DEFAULT_CONNECTION_POOL_SIZE independent SMB sessions, not one shared session — an SMB2 connection’s own client-side flow-control window starts at exactly 1, so two requests in flight at once on one connection would race for that single credit — each created lazily on first checkout rather than in __init__ (which does no I/O). A call checks out whichever slot is currently free — preferring one already connected, via a LIFO free-stack, so sequential, non-overlapping calls keep reusing the same session instead of needlessly spreading across the whole pool — and holds it exclusively until that call (including any of its own retries) finishes.

async aclose()

Tear down every pooled slot’s own SMB session that was ever used — safe to call more than once, and scoped to this instance’s own slots alone (each with its own private connection_cache, rather than sharing smbclient’s process-wide, server-keyed session bookkeeping), so it never disturbs another SmbStore sharing the same server.

Acquires every slot’s own checkout permit first (the same semaphore _acquire_slot uses, drained down to zero free slots) before touching any slot’s ready/connection_cache — a slot still checked out to an in-flight _call_with_retry caller (mid _ensure_slot_session, or mid read/stat/listdir on an already -established one) can’t have its own permit acquired here until that caller releases it, so this never tears a slot down out from under one. Releases every permit back afterward, so this store is still usable (a later call simply re-establishes whichever sessions it needs) rather than left permanently unusable.

async read(path, offset=0, length=None)
async size(path)
async exists(path)
async listdir(path)