| # 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() |