"""Configuration management for WebDrop Bridge application.""" import logging import os from dataclasses import dataclass from pathlib import Path from typing import List from dotenv import load_dotenv logger = logging.getLogger(__name__) class ConfigurationError(Exception): """Raised when configuration is invalid.""" pass @dataclass class Config: """Application configuration loaded from environment variables. Attributes: app_name: Application display name app_version: Application version (semantic versioning) log_level: Logging level (DEBUG, INFO, WARNING, ERROR) log_file: Optional log file path allowed_roots: List of whitelisted root directories for file access allowed_urls: List of whitelisted URL domains/patterns (empty = no restriction) webapp_url: URL to load in embedded web application window_width: Initial window width in pixels window_height: Initial window height in pixels enable_logging: Whether to write logs to file Raises: ConfigurationError: If configuration values are invalid """ app_name: str app_version: str log_level: str log_file: Path | None allowed_roots: List[Path] allowed_urls: List[str] webapp_url: str window_width: int window_height: int enable_logging: bool @classmethod def from_env(cls, env_file: str | None = None) -> "Config": """Load configuration from environment variables. Looks for a .env file in the current working directory and loads environment variables from it. All unset variables use sensible defaults. Args: env_file: Optional path to .env file to load. If None, loads default .env Returns: Config: Configured instance with environment values or defaults Raises: ConfigurationError: If any configured value is invalid """ # Load .env file if it exists load_dotenv(env_file) # Extract and validate configuration values app_name = os.getenv("APP_NAME", "WebDrop Bridge") app_version = os.getenv("APP_VERSION", "1.0.0") log_level = os.getenv("LOG_LEVEL", "INFO").upper() log_file_str = os.getenv("LOG_FILE", "logs/webdrop_bridge.log") allowed_roots_str = os.getenv("ALLOWED_ROOTS", "Z:/,C:/Users/Public") allowed_urls_str = os.getenv("ALLOWED_URLS", "") webapp_url = os.getenv("WEBAPP_URL", "file:///./webapp/index.html") window_width = int(os.getenv("WINDOW_WIDTH", "1024")) window_height = int(os.getenv("WINDOW_HEIGHT", "768")) enable_logging = os.getenv("ENABLE_LOGGING", "true").lower() == "true" # Validate log level valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} if log_level not in valid_levels: raise ConfigurationError( f"Invalid LOG_LEVEL: {log_level}. " f"Must be one of: {', '.join(valid_levels)}" ) # Validate and parse allowed roots try: allowed_roots = [] for p in allowed_roots_str.split(","): root_path = Path(p.strip()).resolve() if not root_path.exists(): raise ConfigurationError( f"Allowed root '{p.strip()}' does not exist" ) if not root_path.is_dir(): raise ConfigurationError( f"Allowed root '{p.strip()}' is not a directory" ) allowed_roots.append(root_path) except ConfigurationError: raise except (ValueError, OSError) as e: raise ConfigurationError( f"Invalid ALLOWED_ROOTS: {allowed_roots_str}. Error: {e}" ) from e # Validate window dimensions if window_width <= 0 or window_height <= 0: raise ConfigurationError( f"Window dimensions must be positive: " f"{window_width}x{window_height}" ) # Create log file path if logging enabled log_file = None if enable_logging: log_file = Path(log_file_str).resolve() # Validate webapp URL is not empty if not webapp_url: raise ConfigurationError("WEBAPP_URL cannot be empty") # Parse allowed URLs (empty string = no restriction) allowed_urls = [ url.strip() for url in allowed_urls_str.split(",") if url.strip() ] if allowed_urls_str else [] return cls( app_name=app_name, app_version=app_version, log_level=log_level, log_file=log_file, allowed_roots=allowed_roots, allowed_urls=allowed_urls, webapp_url=webapp_url, window_width=window_width, window_height=window_height, enable_logging=enable_logging, ) def __repr__(self) -> str: """Return developer-friendly representation.""" return ( f"Config(app={self.app_name} v{self.app_version}, " f"log_level={self.log_level}, " f"allowed_roots={len(self.allowed_roots)} dirs, " f"window={self.window_width}x{self.window_height})" )