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
+116
View File
@@ -0,0 +1,116 @@
"""UploadService — handles user file uploads."""
import contextlib
import hashlib
import os
import tempfile
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol
import anyio
from app.domain.entities.user import User
from app.domain.ports import ArtistRepository, FileStorage, TrackRepository
class UploadFileProtocol(Protocol):
filename: str | None
async def read(self, size: int = -1) -> bytes: ...
@dataclass(frozen=True)
class UploadResult:
track_id: uuid.UUID
title: str
already_exists: bool
async def _stream_to_temp(upload: UploadFileProtocol, dest: Path) -> tuple[str, int]:
h = hashlib.sha256()
size = 0
async with await anyio.open_file(dest, "wb") as out:
while True:
chunk = await upload.read(65536)
if not chunk:
break
h.update(chunk)
await out.write(chunk)
size += len(chunk)
return h.hexdigest(), size
class UploadService:
def __init__(
self,
tracks: TrackRepository,
artists: ArtistRepository,
storage: FileStorage,
tmp_dir: Path | None = None,
) -> None:
self._tracks = tracks
self._artists = artists
self._storage = storage
self._tmp_dir = tmp_dir
async def handle_upload(
self,
*,
upload: UploadFileProtocol,
user: User,
) -> UploadResult:
filename = upload.filename or "unknown"
ext = Path(filename).suffix.lower().lstrip(".") or "bin"
title = Path(filename).stem or "Unknown"
fd, tmp_str = tempfile.mkstemp(
suffix=f".{ext}",
dir=str(self._tmp_dir) if self._tmp_dir else None,
)
tmp_path = Path(tmp_str)
try:
os.close(fd)
sha256_hex, file_size = await _stream_to_temp(upload, tmp_path)
existing = await self._tracks.get_by_source("upload", sha256_hex)
if existing is not None:
return UploadResult(
track_id=existing.id,
title=existing.title,
already_exists=True,
)
track_id = uuid.uuid4()
key = f"tracks/{str(track_id)[:2]}/{track_id}.{ext}"
await self._storage.save_file(key, tmp_path)
try:
artist = await self._artists.get_or_create("Unknown Artist")
track = await self._tracks.add(
id=track_id,
title=title,
artist_id=artist.id,
file_path=key,
file_format=ext,
file_size=file_size,
source="upload",
source_id=sha256_hex,
metadata_status="pending",
added_by=user.id,
)
except Exception:
with contextlib.suppress(Exception):
await self._storage.delete(key)
raise
# TODO(1D): enqueue metadata enrichment task
return UploadResult(
track_id=track.id,
title=track.title,
already_exists=False,
)
finally:
await anyio.Path(tmp_path).unlink(missing_ok=True)