feat: 初始化项目,并且接近完成 ozon 部分
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""本地文件存储:落 data/media/,由 FastAPI /media 静态托管。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import mimetypes
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from config import get_settings
|
||||
|
||||
|
||||
def media_root() -> Path:
|
||||
root = Path(get_settings().data_dir) / "media"
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
return root
|
||||
|
||||
|
||||
def public_url(key: str) -> str:
|
||||
"""media key → 可访问 URL。"""
|
||||
settings = get_settings()
|
||||
return f"{settings.app_base_url.rstrip('/')}/media/{key}"
|
||||
|
||||
|
||||
def _ext_from_url_or_type(hint: str, content_type: str = "") -> str:
|
||||
if content_type:
|
||||
ctype = content_type.split(";")[0].strip().lower()
|
||||
mapping = {
|
||||
"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp",
|
||||
"image/gif": ".gif", "image/bmp": ".bmp", "video/mp4": ".mp4",
|
||||
}
|
||||
if ctype in mapping:
|
||||
return mapping[ctype]
|
||||
ext = mimetypes.guess_extension(hint.split("?")[0].lower()) or ".jpg"
|
||||
return ".jpg" if ext == ".jpe" else ext
|
||||
|
||||
|
||||
def write_bytes(data: bytes, key_prefix: str = "", ext: str = ".jpg") -> str:
|
||||
"""写文件,返回 media key(相对 media 根的路径)。"""
|
||||
key = f"{key_prefix + '/' if key_prefix else ''}{uuid.uuid4().hex}{ext}"
|
||||
path = media_root() / key
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
return key
|
||||
|
||||
|
||||
async def download_bytes(url: str, referer: str | None = None, timeout: float = 60.0) -> tuple[bytes, str]:
|
||||
"""下载远程字节。返回 (bytes, content_type)。"""
|
||||
headers = {"Referer": referer} if referer else {}
|
||||
headers.setdefault("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")
|
||||
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True, verify=False) as client:
|
||||
resp = await client.get(url, headers=headers)
|
||||
resp.raise_for_status()
|
||||
ctype = (resp.headers.get("content-type") or "application/octet-stream").split(";")[0].strip()
|
||||
return resp.content, ctype
|
||||
|
||||
|
||||
async def save_from_url(url: str, key_prefix: str = "", referer: str | None = None) -> str:
|
||||
data, ctype = await download_bytes(url, referer)
|
||||
key = write_bytes(data, key_prefix, _ext_from_url_or_type(url, ctype))
|
||||
return public_url(key)
|
||||
|
||||
|
||||
def local_path(stored_url_or_key: str) -> Path | None:
|
||||
"""stored_url(http.../media/xxx)或 key → 本地文件路径。"""
|
||||
s = stored_url_or_key
|
||||
if "/media/" in s:
|
||||
s = s.split("/media/", 1)[1]
|
||||
p = media_root() / s
|
||||
return p if p.exists() else None
|
||||
Reference in New Issue
Block a user