image downloader service added

This commit is contained in:
garaev kamil 2025-12-06 06:41:25 +03:00
parent 1012ac22b6
commit 74d6adf23a
6 changed files with 412 additions and 0 deletions

View file

@ -0,0 +1,110 @@
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/<hash>.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)