You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

122 lines
4.5 KiB

from __future__ import annotations
import json
from datetime import UTC, datetime
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import ConflictError, NotFoundError, ValidationError
from app.core.ids import new_id
from app.models.entities import Backend
from app.repositories.backend_repository import BackendRepository
from app.services.storage_service import StorageService
class BackendNotFoundError(NotFoundError):
code = "backend_not_found"
message = "Backend does not exist."
class BackendConfigError(ValidationError):
code = "backend_config_invalid"
message = "Backend configuration is invalid."
SUPPORTED_BACKEND_TYPES = {"local", "s3"}
SUPPORTED_STABILITY_CLASSES = {"local", "stable", "opportunistic"}
class BackendService:
def __init__(self, session: AsyncSession) -> None:
self.session = session
self.repository = BackendRepository(session)
self.storage_service = StorageService(session)
async def list_backends(self) -> list[Backend]:
return await self.repository.list_all()
async def create_backend(
self,
*,
name: str,
backend_type: str,
stability_class: str,
read_priority: int,
write_priority: int,
config: dict,
) -> Backend:
cleaned_name = name.strip()
if not cleaned_name:
raise BackendConfigError("Backend name must not be empty.")
if backend_type not in SUPPORTED_BACKEND_TYPES:
raise BackendConfigError(f"Unsupported backend type: {backend_type}.")
if stability_class not in SUPPORTED_STABILITY_CLASSES:
raise BackendConfigError(f"Unsupported stability class: {stability_class}.")
if await self.repository.get_by_name(cleaned_name) is not None:
raise ConflictError("A backend with this name already exists.")
normalized_config = self._validate_backend_config(backend_type, config)
backend = Backend(
id=new_id("bkd"),
name=cleaned_name,
type=backend_type,
stability_class=stability_class,
read_priority=read_priority,
write_priority=write_priority,
is_enabled=True,
config_json=json.dumps(normalized_config),
)
created = await self.repository.create(backend)
await self.session.commit()
return created
async def check_backend(self, backend_id: str) -> tuple[Backend, str, str | None]:
backend = await self._get_backend(backend_id)
try:
status, detail = await self.storage_service.check_backend(backend)
except Exception as exc:
backend.last_health_status = "unhealthy"
backend.last_health_checked_at = datetime.now(UTC)
await self.repository.save(backend)
await self.session.commit()
return backend, "unhealthy", str(exc)
backend.last_health_status = status
backend.last_health_checked_at = datetime.now(UTC)
await self.repository.save(backend)
await self.session.commit()
return backend, status, detail
async def disable_backend(self, backend_id: str) -> Backend:
backend = await self._get_backend(backend_id)
backend.is_enabled = False
saved = await self.repository.save(backend)
await self.session.commit()
return saved
async def _get_backend(self, backend_id: str) -> Backend:
backend = await self.repository.get_by_id(backend_id)
if backend is None:
raise BackendNotFoundError()
return backend
@staticmethod
def _validate_backend_config(backend_type: str, config: dict) -> dict:
if backend_type == "local":
base_path = config.get("base_path")
if not isinstance(base_path, str) or not base_path.strip():
raise BackendConfigError("Local backend config requires a non-empty base_path.")
return {"base_path": base_path}
if backend_type == "s3":
bucket = config.get("bucket")
if not isinstance(bucket, str) or not bucket.strip():
raise BackendConfigError("S3 backend config requires a non-empty bucket.")
normalized = {"bucket": bucket}
for key in ("region", "endpoint_url", "access_key_id", "secret_access_key"):
value = config.get(key)
if value is not None:
normalized[key] = value
return normalized
raise BackendConfigError(f"Unsupported backend type: {backend_type}.")