# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. from abc import ABC, abstractmethod from typing import Any, Dict, List class BackendSession(ABC): """ A session that holds an open connection/file handle for the duration of an operation (e.g. a full GET streaming read). Use as a context manager. """ @abstractmethod def size(self) -> int: """Return the image size in bytes.""" ... @abstractmethod def read(self, offset: int, length: int) -> bytes: """ Read *length* bytes starting at *offset*. For NBD backends, raises RuntimeError if the server returns empty data. For file backends, returns empty bytes at EOF. """ ... @abstractmethod def close(self) -> None: """Release the underlying connection or file handle.""" ... def __enter__(self) -> "BackendSession": return self def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None: self.close() class ImageBackend(ABC): """ Abstract base class for image storage backends. Each backend (NBD, file, etc.) implements this interface so the HTTP handler can operate uniformly without backend-specific branching. """ @property @abstractmethod def supports_extents(self) -> bool: """Whether this backend supports querying allocation/dirty extents.""" ... @property @abstractmethod def supports_range_write(self) -> bool: """Whether this backend supports writing at arbitrary byte offsets.""" ... @abstractmethod def size(self) -> int: """Return the image size in bytes.""" ... @abstractmethod def read(self, offset: int, length: int) -> bytes: """Read *length* bytes starting at *offset*.""" ... @abstractmethod def write(self, data: bytes, offset: int) -> None: """Write *data* at *offset*.""" ... @abstractmethod def write_full(self, stream, content_length: int, flush: bool) -> int: """ Consume *content_length* bytes from *stream* and write the full image. Returns bytes written. Raises on short read. """ ... @abstractmethod def flush(self) -> None: """Flush pending data to stable storage.""" ... @abstractmethod def zero(self, offset: int, length: int) -> None: """Zero *length* bytes starting at *offset*.""" ... @abstractmethod def get_capabilities(self) -> Dict[str, bool]: """ Return backend capabilities dict with keys: read_only, can_flush, can_zero. """ ... @abstractmethod def get_allocation_extents(self) -> List[Dict[str, Any]]: """ Return allocation extents as [{"start": int, "length": int, "zero": bool}, ...]. """ ... @abstractmethod def get_dirty_extents(self, dirty_bitmap_context: str) -> List[Dict[str, Any]]: """ Return merged dirty+zero extents as [{"start": int, "length": int, "dirty": bool, "zero": bool}, ...]. """ ... @abstractmethod def open_session(self) -> BackendSession: """ Open a session that holds a single connection/file handle for the duration of a streaming operation (e.g. GET). """ ... @abstractmethod def close(self) -> None: """Release any resources held by this backend.""" ... def __enter__(self) -> "ImageBackend": return self def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None: self.close()