from __future__ import annotations import hashlib import os from pathlib import Path from typing import Tuple from urllib.parse import urlparse import httpx from fastapi import FastAPI, UploadFile, File, HTTPException from fastapi.responses import FileResponse from pydantic import BaseModel # Хранилище картинок только для этого сервиса MEDIA_ROOT = Path(os.getenv("NYANIMEDB_MEDIA_ROOT", "media")).resolve() MEDIA_ROOT.mkdir(parents=True, exist_ok=True) app = FastAPI() def _guess_ext_from_url(url: str) -> str: path = urlparse(url).path _, ext = os.path.splitext(path) if ext and len(ext) <= 5: return ext return ".jpg" def _guess_ext_from_name(name: str) -> str: _, ext = os.path.splitext(name) if ext and len(ext) <= 5: return ext return ".jpg" def _build_rel_path_from_hash(h: str, ext: str, subdir: str = "posters") -> Tuple[str, Path]: """ Строим путь вида subdir/ab/cd/.ext по sha1-хешу содержимого. """ level1 = h[:2] level2 = h[2:4] rel = f"{subdir}/{level1}/{level2}/{h}{ext}" abs_path = MEDIA_ROOT / rel return rel, abs_path async def _save_bytes(data: bytes, *, filename_hint: str, subdir: str = "posters") -> str: # sha1 по содержимому h = hashlib.sha1(data).hexdigest() # расширение либо из имени, либо .jpg ext = _guess_ext_from_name(filename_hint) rel, abs_path = _build_rel_path_from_hash(h, ext, subdir=subdir) if abs_path.exists(): return rel abs_path.parent.mkdir(parents=True, exist_ok=True) tmp_path = abs_path.with_suffix(abs_path.suffix + ".tmp") with open(tmp_path, "wb") as f: f.write(data) os.replace(tmp_path, abs_path) return rel class DownloadByUrlRequest(BaseModel): url: str subdir: str = "posters" @app.post("/upload") async def upload_image( file: UploadFile = File(...), subdir: str = "posters", ): """ Загрузка файла от клиента (ETL/админка/бекенд). Возвращает относительный путь (subdir/ab/cd/hash.ext). """ data = await file.read() rel = await _save_bytes(data, filename_hint=file.filename or "", subdir=subdir) return {"path": rel} @app.post("/download-by-url") async def download_by_url(payload: DownloadByUrlRequest): """ Скачивает картинку по URL и сохраняет по тем же правилам, что downloader.py. """ async with httpx.AsyncClient(timeout=20.0) as client: r = await client.get(payload.url) r.raise_for_status() data = r.content # filename_hint берём из URL rel = await _save_bytes(data, filename_hint=payload.url, subdir=payload.subdir) return {"path": rel} @app.get("/media/{path:path}") async def get_image(path: str): """ Отдаёт файл по относительному пути (например, posters/ab/cd/hash.jpg). """ abs_path = MEDIA_ROOT / path if not abs_path.is_file(): raise HTTPException(status_code=404, detail="Image not found") return FileResponse(abs_path)