synology_apm_repo.sdk.storage.s3 module

S3Store — a real, executable ObjectStore implementation for S3-compatible object storage, one of the two backends this project’s own object-storage samples are laid out for.

aioboto3/botocore are imported lazily, inside __init__ and each method, rather than at module scope: aioboto3 is always installed (a required dependency), but it’s a substantial import graph (botocore, aiohttp, …), and this module is imported unconditionally by storage/__init__.py, so a module-level import would make every caller of storage pay that cost even if it never touches S3Store. Python caches the import after the first successful call, so the repeated import statements below cost a dict lookup, not a re-import.

This and AzureStore use aioboto3/async rather than a thread around plain boto3 because both sit on aiohttp, where many outstanding network round-trips genuinely overlap on one thread — a real gain a thread-pool wrapper around a synchronous client wouldn’t provide. One consequence: aioboto3’s client is an async context manager, so this class creates it lazily on first use and owns its teardown via S3Store.aclose (called by Session.close()) — forgetting that would leak an aiohttp connector. The constructor itself stays synchronous (it only builds an aioboto3.Session, which does no I/O).

Two contract differences from LocalFsStore, both because S3 genuinely has no directory entities (listdir’s and exists’s own docstrings cover the “directory” side of that): S3Store.read treats an out-of-range Range request as the ObjectStore contract’s own short-read-at-EOF case (b"") rather than S3’s own InvalidRange client error, to match the same contract every other backend already provides.

Every client this module builds goes through _with_default_timeouts, which caps botocore’s own long batch-job timeouts down to values an interactive caller — the TUI’s connect dialog, in particular — can actually wait through when an endpoint is unreachable.

class synology_apm_repo.sdk.storage.s3.S3Store(bucket, *, client=None, **client_kwargs)

Bases: object

An S3 (or S3-compatible — MinIO, Synology C2, …) bucket, addressed by "/"-separated paths relative to the bucket root.

client, if given, is used as-is (tests inject a fake or otherwise pre-configured async client this way, already entered); otherwise one is built lazily on first use from client_kwargs (region_name, endpoint_url — for S3-compatible but non-AWS backends, aws_access_key_id/aws_secret_access_key, …), passed straight through to aioboto3.Session().client("s3", **client_kwargs).

async aclose()

Release the underlying aiohttp connector.

Required, not optional, for a client this class created itself. An injected client is left alone: whoever created it owns closing it. Safe to call more than once.

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

True for either an object exactly at path, or a “directory” — a prefix with at least one object under it. ObjectStore.exists()’s own contract explicitly covers both (layout.py relies on it: db/@data are never objects in their own right on an object-storage backend, only prefixes with real objects underneath — a head_object check alone would wrongly report every such “directory” as absent).

async listdir(path)

An absent “directory” (a prefix with zero objects under it) and an empty one are indistinguishable — S3 has no real directory entities, only prefixes — so both correctly report [] rather than one of them raising NotFoundError.

async synology_apm_repo.sdk.storage.s3.list_buckets(**client_kwargs)

Every bucket visible to these credentials — a bucket-less operation S3Store has no method for, since all four of its methods are scoped to one chosen bucket. Builds its own transient client the same lazy-import way S3Store does and closes it before returning; there is no lifecycle for a caller to manage beyond this one call.

client_kwargs is exactly what a caller would otherwise pass to S3Store. Raises whatever the underlying aioboto3 call raises (e.g. ClientError for AccessDenied when the credentials aren’t authorized to list buckets at the account level) — unhandled, the same as every other backend-specific exception S3Store’s own methods let propagate.