|
| 1 | +""" |
| 2 | +nbdkit Python backend integration for block-level device caching. |
| 3 | +
|
| 4 | +This module serves as the bridge between nbdkit and our file abstraction layer. |
| 5 | +It handles the "outside" config (nbdkit parameters) while delegating file |
| 6 | +operations to the composed file chain. |
| 7 | +""" |
| 8 | + |
| 9 | +import logging |
| 10 | +from pathlib import Path |
| 11 | +from typing import Dict, Any |
| 12 | + |
| 13 | +# Import file detection |
| 14 | +from blkcache.file import detect |
| 15 | +from blkcache.file.device import DEFAULT_SECTOR_SIZE |
| 16 | + |
| 17 | +log = logging.getLogger(__name__) |
| 18 | + |
| 19 | + |
| 20 | +class HandleManager: |
| 21 | + """Manages file handles and their lifecycle for nbdkit backend.""" |
| 22 | + |
| 23 | + def __init__(self): |
| 24 | + self._handles: Dict[int, Any] = {} # handle_id -> File instance |
| 25 | + self._next_handle = 1 |
| 26 | + self._files = [] # Keep references to context managers |
| 27 | + |
| 28 | + def open_file(self, path: Path, mode: str) -> int: |
| 29 | + """Open a file and return a handle ID.""" |
| 30 | + file_cls = detect(path) |
| 31 | + file_instance = file_cls(path, mode) |
| 32 | + |
| 33 | + # Enter the context manager and keep reference |
| 34 | + opened_file = file_instance.__enter__() |
| 35 | + self._files.append((file_instance, opened_file)) |
| 36 | + |
| 37 | + # Assign handle and store |
| 38 | + handle = self._next_handle |
| 39 | + self._handles[handle] = opened_file |
| 40 | + self._next_handle += 1 |
| 41 | + |
| 42 | + log.debug("Opened file %s as handle %d", path, handle) |
| 43 | + return handle |
| 44 | + |
| 45 | + def get_file(self, handle: int): |
| 46 | + """Get the file instance for a handle.""" |
| 47 | + if handle not in self._handles: |
| 48 | + raise ValueError(f"Invalid handle: {handle}") |
| 49 | + return self._handles[handle] |
| 50 | + |
| 51 | + def close_file(self, handle: int) -> None: |
| 52 | + """Close a specific file handle.""" |
| 53 | + if handle in self._handles: |
| 54 | + file_instance = self._handles[handle] |
| 55 | + # Find and close the corresponding context manager |
| 56 | + for i, (ctx_mgr, opened_file) in enumerate(self._files): |
| 57 | + if opened_file is file_instance: |
| 58 | + try: |
| 59 | + ctx_mgr.__exit__(None, None, None) |
| 60 | + except Exception as e: |
| 61 | + log.warning("Error closing file handle %d: %s", handle, e) |
| 62 | + self._files.pop(i) |
| 63 | + break |
| 64 | + |
| 65 | + del self._handles[handle] |
| 66 | + log.debug("Closed handle %d", handle) |
| 67 | + |
| 68 | + def close_all(self) -> None: |
| 69 | + """Close all open files.""" |
| 70 | + for ctx_mgr, _ in self._files: |
| 71 | + try: |
| 72 | + ctx_mgr.__exit__(None, None, None) |
| 73 | + except Exception as e: |
| 74 | + log.warning("Error during cleanup: %s", e) |
| 75 | + |
| 76 | + self._handles.clear() |
| 77 | + self._files.clear() |
| 78 | + log.debug("Closed all handles") |
| 79 | + |
| 80 | + |
| 81 | +# Global state |
| 82 | +DEV: Path | None = None |
| 83 | +CACHE: Path | None = None |
| 84 | +SECTOR_SIZE = DEFAULT_SECTOR_SIZE |
| 85 | +METADATA = {} |
| 86 | + |
| 87 | +# Handle manager instance |
| 88 | +HANDLE_MANAGER = HandleManager() |
| 89 | + |
| 90 | + |
| 91 | +# These functions are now handled by the BlockCache class |
| 92 | + |
| 93 | + |
| 94 | +def config(key: str, val: str) -> None: |
| 95 | + """Stores device, cache paths and parses metadata key-value pairs.""" |
| 96 | + global DEV, CACHE, SECTOR_SIZE, METADATA |
| 97 | + |
| 98 | + if key == "device": |
| 99 | + DEV = Path(val) |
| 100 | + elif key == "cache": |
| 101 | + CACHE = Path(val) |
| 102 | + elif key == "sector" or key == "block": # Accept both for compatibility |
| 103 | + SECTOR_SIZE = int(val) |
| 104 | + elif key == "metadata": |
| 105 | + # Parse metadata string in format "key1=value1,key2=value2" |
| 106 | + for pair in val.split(","): |
| 107 | + if "=" in pair: |
| 108 | + k, v = pair.split("=", 1) |
| 109 | + METADATA[k.strip()] = v.strip() |
| 110 | + else: |
| 111 | + # Store unknown keys in metadata |
| 112 | + METADATA[key] = val |
| 113 | + |
| 114 | + |
| 115 | +def config_complete() -> None: |
| 116 | + """Validates required parameters.""" |
| 117 | + global DEV, CACHE, SECTOR_SIZE, METADATA |
| 118 | + |
| 119 | + if DEV is None: |
| 120 | + raise RuntimeError("device= is required") |
| 121 | + |
| 122 | + # For now, just log the config - we'll build file composition later |
| 123 | + log.debug("Config: device=%s, cache=%s, sector_size=%d", DEV, CACHE, SECTOR_SIZE) |
| 124 | + |
| 125 | + |
| 126 | +def open(_readonly: bool) -> int: |
| 127 | + """Opens device and returns handle ID.""" |
| 128 | + mode = "rb" if _readonly else "r+b" |
| 129 | + return HANDLE_MANAGER.open_file(DEV, mode) |
| 130 | + |
| 131 | + |
| 132 | +def get_size(h: int) -> int: |
| 133 | + """Get file size.""" |
| 134 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 135 | + return file_instance.size() |
| 136 | + |
| 137 | + |
| 138 | +def pread(h: int, count: int, offset: int) -> bytes: |
| 139 | + """Read data at offset.""" |
| 140 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 141 | + return file_instance.pread(count, offset) |
| 142 | + |
| 143 | + |
| 144 | +def close(h: int) -> None: |
| 145 | + """Close file handle.""" |
| 146 | + log.debug("Backend close() called for handle %d", h) |
| 147 | + HANDLE_MANAGER.close_file(h) |
| 148 | + log.debug("Backend close() completed") |
| 149 | + |
| 150 | + |
| 151 | +# Optional capability functions - use duck typing |
| 152 | +def can_write(h: int) -> bool: |
| 153 | + """Check if file supports writing.""" |
| 154 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 155 | + return "w" in file_instance.mode or "a" in file_instance.mode or "+" in file_instance.mode |
| 156 | + |
| 157 | + |
| 158 | +def can_flush(h: int) -> bool: |
| 159 | + """Check if file supports flushing.""" |
| 160 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 161 | + return hasattr(file_instance, "flush") |
| 162 | + |
| 163 | + |
| 164 | +def can_trim(h: int) -> bool: |
| 165 | + """Check if file supports trim operations.""" |
| 166 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 167 | + return hasattr(file_instance, "trim") |
| 168 | + |
| 169 | + |
| 170 | +def can_zero(h: int) -> bool: |
| 171 | + """Check if file supports zero operations.""" |
| 172 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 173 | + return hasattr(file_instance, "zero") |
| 174 | + |
| 175 | + |
| 176 | +def can_fast_zero(h: int) -> bool: |
| 177 | + """Check if file supports fast zero operations.""" |
| 178 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 179 | + return hasattr(file_instance, "fast_zero") |
| 180 | + |
| 181 | + |
| 182 | +def can_extents(h: int) -> bool: |
| 183 | + """Check if file supports extent operations.""" |
| 184 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 185 | + return hasattr(file_instance, "extents") |
| 186 | + |
| 187 | + |
| 188 | +def is_rotational(h: int) -> bool: |
| 189 | + """Check if underlying storage is rotational.""" |
| 190 | + file_instance = HANDLE_MANAGER.get_file(h) |
| 191 | + return hasattr(file_instance, "is_rotational") and file_instance.is_rotational() |
| 192 | + |
| 193 | + |
| 194 | +def can_multi_conn(h: int) -> bool: |
| 195 | + """Check if file supports multiple connections.""" |
| 196 | + # For now, return False for safety |
| 197 | + return False |
0 commit comments