"""Local directory photo source provider.""" from __future__ import annotations from pathlib import Path from PIL import Image from providers import PhotoProvider, PhotoRef, AuthType, register_provider SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".bmp", ".tiff"} @register_provider class LocalDirectoryProvider(PhotoProvider): provider_type = "local" display_name = "Local Directory" auth_type = AuthType.NONE config_schema = { "path": {"type": "string", "description": "Absolute path to photos directory", "required": True}, "recursive": {"type": "boolean", "description": "Scan subdirectories", "default": True}, } def __init__(self, instance_id: str, config: dict): super().__init__(instance_id, config) self._path = Path(config.get("path", ".")) self._recursive = config.get("recursive", True) def list_cache_ttl(self) -> int: return 10 # local filesystem is fast, short TTL def list_photos(self) -> list[PhotoRef]: photos = [] iterator = self._path.rglob("*") if self._recursive else self._path.iterdir() for f in iterator: if f.suffix.lower() in SUPPORTED_EXTENSIONS and f.is_file(): photos.append(PhotoRef( provider_id=self.instance_id, photo_id=f.name, display_name=f.name, sort_key=f.name.lower(), )) return sorted(photos, key=lambda p: p.sort_key) def get_photo(self, photo_id: str) -> Image.Image: path = self._find_file(photo_id) if not path: raise FileNotFoundError(f"Photo not found: {photo_id}") return Image.open(path).convert("RGB") def get_photo_size(self, photo_id: str) -> int | None: path = self._find_file(photo_id) return path.stat().st_size if path else None def supports_upload(self) -> bool: return True def upload_photo(self, filename: str, data: bytes) -> PhotoRef: dest = self._path / filename dest.write_bytes(data) return PhotoRef( provider_id=self.instance_id, photo_id=filename, display_name=filename, sort_key=filename.lower(), ) def supports_delete(self) -> bool: return True def delete_photo(self, photo_id: str) -> None: path = self._find_file(photo_id) if path: path.unlink() def _find_file(self, photo_id: str) -> Path | None: # Direct lookup first direct = self._path / photo_id if direct.is_file(): return direct # Recursive search if needed if self._recursive: for f in self._path.rglob(photo_id): if f.is_file(): return f return None