import os from collections.abc import Awaitable, Callable from .blobs import BlobCache from .metadata import FileMetadataCache def _env_int(name: str, default: int) -> int: try: return int(os.getenv(name, str(default))) except (TypeError, ValueError): return default class FileCache: """Facade over the metadata cache (shared) and the blob cache (process-local).""" def __init__(self, metadata: FileMetadataCache, blobs: BlobCache, blob_max_age: int, edge_max_age: int): self.metadata = metadata self.blobs = blobs self.blob_max_age = blob_max_age self.edge_max_age = edge_max_age async def get_metadata(self, file_id: str, loader: Callable[[], Awaitable[dict | None]]): return await self.metadata.get(file_id, loader) async def get_blob(self, storage_key: str, loader: Callable[[], Awaitable[bytes]]) -> bytes: cached = self.blobs.get(storage_key) if cached is not None: return cached data = await loader() self.blobs.set(storage_key, data) return data async def invalidate(self, file_id: str, storage_key: str | None = None) -> None: await self.metadata.invalidate(file_id) if storage_key: self.blobs.invalidate(storage_key) def stats(self) -> dict: return { 'metadata_hits': self.metadata.hits, 'metadata_misses': self.metadata.misses, 'blob_hits': self.blobs.hits, 'blob_misses': self.blobs.misses, 'blob_bytes': self.blobs.current_bytes, } def build_file_cache(cache) -> FileCache: """Build the cache stack from FILE_CACHE_* environment settings.""" return FileCache( metadata=FileMetadataCache(cache, ttl=_env_int('FILE_CACHE_METADATA_TTL', 60)), blobs=BlobCache( max_total_bytes=_env_int('FILE_CACHE_BLOB_TOTAL_BYTES', 64 * 1024 * 1024), max_item_bytes=_env_int('FILE_CACHE_BLOB_ITEM_BYTES', 2 * 1024 * 1024), ttl=_env_int('FILE_CACHE_BLOB_TTL', 300), ), blob_max_age=_env_int('FILE_CACHE_BROWSER_MAX_AGE', 86400), # Cloudflare keeps serving deleted files, so the edge window stays short. edge_max_age=_env_int('FILE_CACHE_EDGE_MAX_AGE', 1800), )