from __future__ import annotations from typing import Any, Literal from pydantic import BaseModel, Field, ValidationError as PydanticValidationError, field_validator from app.core.exceptions import ValidationError BackendType = Literal["local_directory", "s3", "webdav"] class BackendConfigError(ValidationError): code = "backend_config_invalid" message = "Backend configuration is invalid." class LocalDirectoryConfig(BaseModel): base_path: str = Field(min_length=1) @field_validator("base_path") @classmethod def clean_base_path(cls, value: str) -> str: cleaned = value.strip() if not cleaned: raise ValueError("base_path must not be empty") return cleaned class S3CompatibleConfig(BaseModel): bucket: str = Field(min_length=1) region: str | None = None endpoint_url: str | None = None access_key_id: str | None = None secret_access_key: str | None = None prefix: str = "" force_path_style: bool = False verify_ssl: bool = True storage_class: str | None = None server_side_encryption: str | None = None @field_validator("bucket") @classmethod def clean_bucket(cls, value: str) -> str: cleaned = value.strip() if not cleaned: raise ValueError("bucket must not be empty") return cleaned @field_validator("prefix") @classmethod def clean_prefix(cls, value: str) -> str: return value.strip().strip("/") class WebDAVConfig(BaseModel): endpoint_url: str = Field(min_length=1) username: str = Field(min_length=1) password: str | None = None root_path: str = Field(default="iron", min_length=1) verify_ssl: bool = True @field_validator("endpoint_url", "username") @classmethod def clean_required_text(cls, value: str) -> str: cleaned = value.strip() if not cleaned: raise ValueError("value must not be empty") return cleaned @field_validator("endpoint_url") @classmethod def clean_endpoint_url(cls, value: str) -> str: return value.strip().rstrip("/") @field_validator("root_path") @classmethod def clean_root_path(cls, value: str) -> str: cleaned = "/".join(part for part in value.strip().strip("/").split("/") if part) if not cleaned: raise ValueError("root_path must not be empty") return cleaned CONFIG_MODELS: dict[str, type[BaseModel]] = { "local_directory": LocalDirectoryConfig, "s3": S3CompatibleConfig, "webdav": WebDAVConfig, } SECRET_CONFIG_KEYS = { "access_key_id", "secret_access_key", "password", } def supported_backend_types() -> set[str]: return set(CONFIG_MODELS) def validate_backend_config(backend_type: str, config: dict[str, Any]) -> dict[str, Any]: model = CONFIG_MODELS.get(backend_type) if model is None: raise BackendConfigError(f"Unsupported backend type: {backend_type}.") try: return model.model_validate(config).model_dump() except PydanticValidationError as exc: first_error = exc.errors()[0] if exc.errors() else {} location = ".".join(str(part) for part in first_error.get("loc", [])) message = first_error.get("msg", "Invalid backend configuration.") prefix = f"{location}: " if location else "" raise BackendConfigError(prefix + message) from exc def split_backend_config(backend_type: str, config: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: normalized = validate_backend_config(backend_type, config) public_config: dict[str, Any] = {} secret_config: dict[str, Any] = {} for key, value in normalized.items(): if key in SECRET_CONFIG_KEYS: if value: secret_config[key] = value else: public_config[key] = value return public_config, secret_config def merge_backend_config(public_config: dict[str, Any], secret_config: dict[str, Any]) -> dict[str, Any]: return {**public_config, **secret_config}