feat: local storage logic & endpoints

This commit is contained in:
Senko-san
2026-06-07 15:34:06 +03:00
parent dfd512a13f
commit 81ea93c371
23 changed files with 945 additions and 18 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
"""Domain entities and value objects — pure, framework-free."""
from app.domain.entities.storage import ObjectStat
from app.domain.entities.track import Artist, Track
from app.domain.entities.user import Credentials, User
__all__ = ["Credentials", "User"]
__all__ = ["Artist", "Credentials", "ObjectStat", "Track", "User"]
+9
View File
@@ -0,0 +1,9 @@
"""Value objects for file storage."""
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class ObjectStat:
size: int
content_type: str | None
+29
View File
@@ -0,0 +1,29 @@
"""Track and Artist domain entities."""
import datetime as dt
import uuid
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Artist:
id: uuid.UUID
name: str
created_at: dt.datetime
updated_at: dt.datetime
@dataclass(frozen=True, slots=True)
class Track:
id: uuid.UUID
title: str
artist_id: uuid.UUID
file_path: str
file_format: str
file_size: int
source: str
source_id: str
duration_seconds: int | None
metadata_status: str
created_at: dt.datetime
updated_at: dt.datetime
+16
View File
@@ -61,3 +61,19 @@ class DependencyUnavailableError(DomainError):
"""
code = "dependency_unavailable"
class StorageError(DomainError):
"""File storage operation failed."""
code = "storage_error"
class RangeNotSatisfiableError(DomainError):
"""Requested byte range cannot be satisfied."""
code = "range_not_satisfiable"
def __init__(self, total_size: int) -> None:
super().__init__("Requested range is not satisfiable.")
self.total_size = total_size
+40 -1
View File
@@ -7,9 +7,13 @@ are bound to these ports at the composition root (``app.api.deps``).
import datetime as dt
import uuid
from collections.abc import AsyncIterator
from contextlib import AbstractAsyncContextManager
from pathlib import Path
from typing import Protocol
from app.domain.entities import Credentials, User
from app.domain.entities import Credentials, ObjectStat, User
from app.domain.entities.track import Artist, Track
from app.domain.tokens import IssuedToken, TokenClaims, TokenType
@@ -56,3 +60,38 @@ class TokenService(Protocol):
"""Verify signature + expiry and return claims. Raises
:class:`~app.domain.errors.AuthenticationError` on any failure."""
...
class FileStorage(Protocol):
async def save_file(self, key: str, src_path: Path) -> int: ...
async def open_range(
self, key: str, start: int, end: int | None
) -> tuple[AsyncIterator[bytes], int]: ...
async def stat(self, key: str) -> ObjectStat: ...
async def exists(self, key: str) -> bool: ...
async def delete(self, key: str) -> None: ...
def as_local_path(self, key: str) -> AbstractAsyncContextManager[Path]: ...
class ArtistRepository(Protocol):
async def get_or_create(self, name: str) -> Artist: ...
class TrackRepository(Protocol):
async def get_by_id(self, track_id: uuid.UUID) -> Track | None: ...
async def get_by_source(self, source: str, source_id: str) -> Track | None: ...
async def add(
self,
*,
id: uuid.UUID,
title: str,
artist_id: uuid.UUID,
file_path: str,
file_format: str,
file_size: int,
source: str,
source_id: str,
metadata_status: str,
added_by: uuid.UUID | None,
) -> Track: ...
async def delete(self, track_id: uuid.UUID) -> None: ...