"""스토리지 백엔드 추상 인터페이스 — plan ds-s1-backend-1 D-1. ABC 는 첫날부터 Range(offset/length) stream 계약을 포함한다 — D-2 의 원격 streaming Range pass-through 가 afterthought 가 아니라 인터페이스 의무가 되도록. is_local=True 백엔드는 로컬 파일시스템 경로를 노출 → 호출부가 Starlette FileResponse (Range 자동 처리)를 그대로 쓴다. 원격 백엔드는 stream()/stat() 로 Range 를 구현한다. """ from __future__ import annotations import os from abc import ABC, abstractmethod from collections.abc import AsyncIterator from dataclasses import dataclass class StorageNotConfigured(RuntimeError): """활성화되지 않은(미프로비전) 백엔드 호출 — 503 으로 표면화. silent fallback 금지.""" @dataclass class StatResult: exists: bool size: int class StorageBackend(ABC): """원본 파일 접근 추상 인터페이스.""" # 로컬 파일시스템 경로를 노출하는가 (FileResponse 직결 가능 여부). is_local: bool = False @abstractmethod def local_path(self, rel_path: str) -> os.PathLike[str] | None: """is_local=True 면 물리 경로 반환(FileResponse 용). 원격 백엔드는 None.""" @abstractmethod async def stat(self, rel_path: str) -> StatResult: """크기/존재 여부. 미구성 백엔드는 StorageNotConfigured raise.""" @abstractmethod def stream( self, rel_path: str, *, start: int | None = None, end: int | None = None ) -> AsyncIterator[bytes]: """[start, end] 바이트 범위(inclusive)를 async 청크로 yield (Range pass-through). start/end 가 None 이면 전체. 미구성 백엔드는 StorageNotConfigured raise. """ raise NotImplementedError