from __future__ import annotations import json from pathlib import Path import anyio from sqlalchemy.ext.asyncio import AsyncSession from app.adapters.storage.local import LocalStorageAdapter from app.core.config import get_settings from app.core.ids import new_id from app.core.secret_codec import encrypt_secret_config from app.core.storage_layout import build_blob_storage_key from app.models.entities import Backend, Blob, BlobReplica from app.repositories.backend_repository import BackendRepository from app.repositories.blob_replica_repository import BlobReplicaRepository DEFAULT_LOCAL_BACKEND_ID = "bkd_local_default" DEFAULT_LOCAL_BACKEND_NAME = "local-default" class LocalStorageService: def __init__(self, session: AsyncSession) -> None: self.session = session self.settings = get_settings() self.backend_repository = BackendRepository(session) self.blob_replica_repository = BlobReplicaRepository(session) self.adapter = LocalStorageAdapter(self.settings.local_storage_dir) async def ensure_backend_ready(self) -> Backend: await self.adapter.ensure_base_path() backend = await self.backend_repository.get_by_name(DEFAULT_LOCAL_BACKEND_NAME) if backend is not None: return backend backend = Backend( id=DEFAULT_LOCAL_BACKEND_ID, name=DEFAULT_LOCAL_BACKEND_NAME, type="local_directory", stability_class="local", read_priority=100, write_priority=100, is_enabled=True, public_config_json=json.dumps({"base_path": self.settings.local_storage_dir}), secret_config_json=encrypt_secret_config({}), ) await self.backend_repository.create(backend) await self.session.commit() return backend async def persist_blob_from_temp(self, blob: Blob, temp_path: str) -> BlobReplica: backend = await self.ensure_backend_ready() existing = await self.blob_replica_repository.get_by_blob_and_backend(blob.id, backend.id) if existing is not None: return existing storage_key = self._build_storage_key(blob) await self.adapter.put_file(temp_path, storage_key) replica = BlobReplica( id=new_id("rep"), blob_id=blob.id, backend_id=backend.id, storage_key=storage_key, status="ready", checksum=blob.content_hash, size_bytes=blob.size_bytes, ) created = await self.blob_replica_repository.create(replica) await self.session.commit() stored_path = await self.adapter.resolve_path(storage_key) await anyio.to_thread.run_sync(stored_path.exists) return created async def resolve_replica_path(self, replica: BlobReplica) -> Path: await self.ensure_backend_ready() return await self.adapter.resolve_path(replica.storage_key) @staticmethod def _build_storage_key(blob: Blob) -> str: return build_blob_storage_key(blob)