"""Alibaba Cloud OSS SDK V2 adapter for the guarded ARR object-store port.""" from __future__ import annotations import os import re from dataclasses import dataclass from typing import Any, Mapping, Optional from urllib.parse import urlparse from arr_storage.contracts import BackendObject, valid_object_key from arr_storage.remote import CloudClientError BUCKET_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$") REGION_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,62}$") @dataclass(frozen=True) class AliyunOssConfig: """Non-secret OSS routing; credentials remain in the SDK provider chain.""" region: str bucket: str endpoint: Optional[str] = None def __post_init__(self) -> None: region = self.region.strip().lower() bucket = self.bucket.strip().lower() endpoint = self.endpoint.strip().rstrip("/") if self.endpoint else None if not REGION_RE.fullmatch(region) or not BUCKET_RE.fullmatch(bucket): raise ValueError("OSS region or bucket configuration is invalid") if endpoint is not None: parsed = urlparse(endpoint) if parsed.scheme != "https" or not parsed.netloc or parsed.path not in {"", "/"}: raise ValueError("ARR_OSS_ENDPOINT must be an HTTPS endpoint origin") object.__setattr__(self, "region", region) object.__setattr__(self, "bucket", bucket) object.__setattr__(self, "endpoint", endpoint) @classmethod def from_environment( cls, environment: Optional[Mapping[str, str]] = None, ) -> "AliyunOssConfig": values = environment if environment is not None else os.environ region = str(values.get("ARR_OSS_REGION", "")).strip() bucket = str(values.get("ARR_OSS_BUCKET", "")).strip() endpoint = str(values.get("ARR_OSS_ENDPOINT", "")).strip() or None if not region or not bucket: raise ValueError("ARR_OSS_REGION and ARR_OSS_BUCKET are required") return cls(region=region, bucket=bucket, endpoint=endpoint) class _OssReader: """Small file-like wrapper that never leaks provider exception text.""" def __init__(self, body: Any) -> None: self._body = body self._buffer = bytearray() self._eof = False iterator_factory = getattr(body, "iter_bytes", None) try: self._iterator = ( iter(iterator_factory(block_size=1024 * 1024)) if callable(iterator_factory) else None ) except Exception: raise OSError("OSS stream initialization failed") from None def read(self, size: int = -1) -> bytes: if size == 0: return b"" if self._iterator is not None: return self._read_iterator(size) try: value = self._body.read(size) except TypeError: try: value = self._body.read() except Exception: raise OSError("OSS stream read failed") from None except Exception: raise OSError("OSS stream read failed") from None if not isinstance(value, bytes): raise OSError("OSS stream returned invalid bytes") return value def _read_iterator(self, size: int) -> bytes: try: if size is None or size < 0: chunks = [bytes(self._buffer)] if self._buffer else [] self._buffer.clear() while not self._eof: chunk = self._next_chunk() if chunk: chunks.append(chunk) return b"".join(chunks) while len(self._buffer) < size and not self._eof: chunk = self._next_chunk() if chunk: self._buffer.extend(chunk) value = bytes(self._buffer[:size]) del self._buffer[:size] return value except OSError: raise except Exception: raise OSError("OSS stream read failed") from None def _next_chunk(self) -> bytes: if self._eof or self._iterator is None: return b"" try: chunk = next(self._iterator) except StopIteration: self._eof = True return b"" if not isinstance(chunk, bytes): raise OSError("OSS stream returned invalid bytes") return chunk def close(self) -> None: try: close = getattr(self._body, "close", None) if callable(close): close() except Exception: raise OSError("OSS stream close failed") from None def __enter__(self) -> "_OssReader": return self def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: self.close() class AliyunOssV2Client: """CloudClientPort implemented with ``alibabacloud_oss_v2``. The bucket is required to be unversioned because OSS ignores ``forbid_overwrite`` when versioning is enabled or suspended. This is a fail-closed invariant for the staged/committed object workflow. """ def __init__( self, config: AliyunOssConfig, *, sdk: Optional[Any] = None, client: Optional[Any] = None, ) -> None: if sdk is None: try: import alibabacloud_oss_v2 as sdk_module # type: ignore[import-not-found] except ImportError: raise CloudClientError("driver_unavailable") from None sdk = sdk_module self._sdk = sdk self._config = config self._http_session: Optional[Any] = None if client is None: http_session: Optional[Any] = None try: import requests credentials = sdk.credentials.EnvironmentVariableCredentialsProvider() sdk_config = sdk.config.load_default() sdk_config.credentials_provider = credentials sdk_config.region = config.region if config.endpoint is not None: sdk_config.endpoint = config.endpoint # Never send OSS credentials through an ambient desktop/system # proxy. Any deployment proxy must be configured explicitly. http_session = requests.Session() http_session.trust_env = False sdk_config.http_client = sdk.transport.RequestsHttpClient( session=http_session ) client = sdk.Client(sdk_config) except Exception as error: if http_session is not None: http_session.close() self._raise_mapped(error) self._http_session = http_session self._client = client @property def bucket(self) -> str: return self._config.bucket def assert_immutable_writes_supported(self) -> None: """Reject buckets that cannot safely hold private immutable ARR data.""" try: result = self._client.get_bucket_info( self._sdk.GetBucketInfoRequest(bucket=self.bucket) ) except Exception as error: self._raise_mapped(error) info = getattr(result, "bucket_info", None) if info is None: raise CloudClientError("unavailable") location = str(getattr(info, "location", "") or "").strip().lower() if location not in {self._config.region, f"oss-{self._config.region}"}: raise CloudClientError("region_mismatch") acl = str(getattr(info, "acl", "") or "").strip().lower() # A public-read bucket is an approved deployment choice for this # integration. Every ARR object is still written with an explicit # private object ACL, which overrides the bucket ACL. Never accept a # bucket that grants anonymous writes. if acl not in {"private", "public-read"}: raise CloudClientError("public_access_incompatible") if getattr(info, "sse_rule", None) is None: raise CloudClientError("encryption_incompatible") status = str(getattr(info, "versioning", "") or "").strip().lower() if status in {"enabled", "suspended"}: raise CloudClientError("versioning_incompatible") def upload_file_if_absent( self, object_key: str, source: str, mime_type: str, metadata: Mapping[str, str], ) -> BackendObject: self._validate_request(object_key, metadata) try: self._client.put_object_from_file( self._sdk.PutObjectRequest( bucket=self.bucket, key=object_key, acl="private", content_type=mime_type, metadata=dict(metadata), forbid_overwrite=True, ), source, ) except Exception as error: self._raise_mapped(error) return self.stat_object(object_key) def stat_object(self, object_key: str) -> BackendObject: self._validate_key(object_key) try: result = self._client.head_object( self._sdk.HeadObjectRequest(bucket=self.bucket, key=object_key) ) except Exception as error: self._raise_mapped(error) raw_metadata = getattr(result, "metadata", None) or {} if not isinstance(raw_metadata, Mapping) or any( not isinstance(key, str) or not isinstance(value, str) for key, value in raw_metadata.items() ): raise CloudClientError("unavailable") metadata = dict(raw_metadata) content_type = getattr(result, "content_type", None) if isinstance(content_type, str) and content_type: metadata.setdefault("arr-backend-content-type", content_type) content_length = getattr(result, "content_length", None) if not isinstance(content_length, int) or isinstance(content_length, bool) or content_length < 0: raise CloudClientError("unavailable") return BackendObject( object_key=object_key, byte_size=content_length, metadata=metadata, etag=self._optional_text(getattr(result, "etag", None)), version_id=self._optional_text(getattr(result, "version_id", None)), ) def stream_object(self, object_key: str) -> _OssReader: self._validate_key(object_key) try: result = self._client.get_object( self._sdk.GetObjectRequest(bucket=self.bucket, key=object_key) ) except Exception as error: self._raise_mapped(error) body = getattr(result, "body", None) if body is None or not callable(getattr(body, "read", None)): raise CloudClientError("unavailable") return _OssReader(body) def copy_object_if_absent( self, source_key: str, destination_key: str, metadata: Mapping[str, str], ) -> BackendObject: self._validate_key(source_key) self._validate_request(destination_key, metadata) try: self._client.copy_object( self._sdk.CopyObjectRequest( bucket=self.bucket, key=destination_key, source_bucket=self.bucket, source_key=source_key, acl="private", metadata=dict(metadata), metadata_directive="REPLACE", content_type=metadata.get("arr-mime-type"), forbid_overwrite=True, ) ) except Exception as error: self._raise_mapped(error) return self.stat_object(destination_key) def delete_exact_object(self, object_key: str) -> None: self._validate_key(object_key) try: self._client.delete_object( self._sdk.DeleteObjectRequest(bucket=self.bucket, key=object_key) ) except Exception as error: self._raise_mapped(error) def close(self) -> None: close = getattr(self._client, "close", None) if callable(close): try: close() except Exception: pass if self._http_session is not None: try: self._http_session.close() except Exception: pass self._http_session = None @staticmethod def _optional_text(value: Any) -> Optional[str]: return value if isinstance(value, str) and value else None @staticmethod def _validate_key(object_key: str) -> None: if not valid_object_key(object_key): raise CloudClientError("invalid_request") @classmethod def _validate_request(cls, object_key: str, metadata: Mapping[str, str]) -> None: cls._validate_key(object_key) if any( not isinstance(key, str) or not key or not isinstance(value, str) for key, value in metadata.items() ): raise CloudClientError("invalid_request") @staticmethod def _raise_mapped(error: BaseException) -> None: unwrapped: Any = error unwrap = getattr(error, "unwrap", None) if callable(unwrap): try: unwrapped = unwrap() except Exception: unwrapped = error status = getattr(unwrapped, "status_code", None) code = str(getattr(unwrapped, "code", "") or "") if status == 404 or code in {"NoSuchKey", "NoSuchBucket", "NotFound"}: kind = "not_found" elif status in {409, 412} or code in { "FileAlreadyExists", "FileImmutable", "PreconditionFailed", }: kind = "conflict" elif status in {400, 401, 403}: kind = "invalid_request" if status == 400 else "forbidden" else: kind = "unavailable" raise CloudClientError(kind) from None