149 lines
4.2 KiB
Python
149 lines
4.2 KiB
Python
"""API v3 文件下载服务。
|
||
|
||
下载用户提供的图片/视频/音频到本地存储。
|
||
"""
|
||
|
||
import base64
|
||
import logging
|
||
import os
|
||
import re
|
||
import uuid
|
||
from datetime import datetime
|
||
from urllib.parse import urlparse
|
||
|
||
import httpx
|
||
|
||
from app.config import settings
|
||
|
||
logger = logging.getLogger("videogen")
|
||
|
||
|
||
def _get_date_str() -> str:
|
||
"""获取当前日期字符串。"""
|
||
return datetime.now().strftime("%Y%m%d")
|
||
|
||
|
||
def _get_uploads_dir() -> str:
|
||
"""获取上传文件存储目录。"""
|
||
upload_dir = os.path.join(os.path.dirname(settings.STORAGE_LOCAL_PATH), "uploads", "api")
|
||
os.makedirs(upload_dir, exist_ok=True)
|
||
return upload_dir
|
||
|
||
|
||
async def download_file_from_url(url: str, sub_dir: str = "") -> str:
|
||
"""从 URL 下载文件到本地。
|
||
|
||
Args:
|
||
url: 文件 URL
|
||
sub_dir: 子目录(如 images/videos/audios)
|
||
|
||
Returns:
|
||
相对路径: /uploads/api/{sub_dir}/{date}/{filename}
|
||
"""
|
||
upload_dir = _get_uploads_dir()
|
||
date_str = _get_date_str()
|
||
|
||
# 创建目标目录
|
||
dest_dir = os.path.join(upload_dir, sub_dir, date_str)
|
||
os.makedirs(dest_dir, exist_ok=True)
|
||
|
||
# 从 URL 提取扩展名
|
||
parsed = urlparse(url)
|
||
path = parsed.path
|
||
ext = os.path.splitext(path)[1].lower()
|
||
if not ext or len(ext) > 10:
|
||
ext = ".bin" # 默认扩展名
|
||
|
||
# 生成唯一文件名
|
||
filename = f"{uuid.uuid4().hex}{ext}"
|
||
dest_path = os.path.join(dest_dir, filename)
|
||
|
||
# 下载文件
|
||
async with httpx.AsyncClient(timeout=300, follow_redirects=True) as client:
|
||
async with client.stream("GET", url) as response:
|
||
response.raise_for_status()
|
||
with open(dest_path, "wb") as f:
|
||
async for chunk in response.aiter_bytes(chunk_size=8192):
|
||
f.write(chunk)
|
||
|
||
# 返回相对路径
|
||
rel_path = f"/uploads/api/{sub_dir}/{date_str}/{filename}"
|
||
logger.info("Downloaded file: %s -> %s", url[:80], rel_path)
|
||
return rel_path
|
||
|
||
|
||
def save_base64_file(data: str, sub_dir: str = "") -> str:
|
||
"""保存 Base64 编码的文件到本地。
|
||
|
||
Args:
|
||
data: Base64 编码的数据(可包含 data:...;base64, 前缀)
|
||
sub_dir: 子目录
|
||
|
||
Returns:
|
||
相对路径: /uploads/api/{sub_dir}/{date}/{filename}
|
||
"""
|
||
upload_dir = _get_uploads_dir()
|
||
date_str = _get_date_str()
|
||
|
||
# 创建目标目录
|
||
dest_dir = os.path.join(upload_dir, sub_dir, date_str)
|
||
os.makedirs(dest_dir, exist_ok=True)
|
||
|
||
# 解析 Base64 数据
|
||
if "," in data:
|
||
header, b64_data = data.split(",", 1)
|
||
# 从 header 提取 MIME 类型
|
||
mime_match = re.search(r"data:([^;]+)", header)
|
||
mime_type = mime_match.group(1) if mime_match else "application/octet-stream"
|
||
# 根据 MIME 类型确定扩展名
|
||
ext_map = {
|
||
"image/jpeg": ".jpg",
|
||
"image/png": ".png",
|
||
"image/webp": ".webp",
|
||
"image/gif": ".gif",
|
||
"video/mp4": ".mp4",
|
||
"video/webm": ".webm",
|
||
"audio/mpeg": ".mp3",
|
||
"audio/wav": ".wav",
|
||
"audio/ogg": ".ogg",
|
||
}
|
||
ext = ext_map.get(mime_type, ".bin")
|
||
else:
|
||
b64_data = data
|
||
ext = ".bin"
|
||
|
||
# 解码并保存
|
||
try:
|
||
file_data = base64.b64decode(b64_data)
|
||
except Exception as exc:
|
||
raise ValueError(f"Base64 解码失败: {exc}")
|
||
|
||
filename = f"{uuid.uuid4().hex}{ext}"
|
||
dest_path = os.path.join(dest_dir, filename)
|
||
|
||
with open(dest_path, "wb") as f:
|
||
f.write(file_data)
|
||
|
||
rel_path = f"/uploads/api/{sub_dir}/{date_str}/{filename}"
|
||
logger.info("Saved base64 file: %s (%d bytes)", rel_path, len(file_data))
|
||
return rel_path
|
||
|
||
|
||
async def process_media_url(url: str, media_type: str) -> str:
|
||
"""处理媒体 URL:下载到本地或保存 Base64。
|
||
|
||
Args:
|
||
url: URL 或 Base64 数据
|
||
media_type: image / video / audio
|
||
|
||
Returns:
|
||
相对路径: /uploads/api/{type}/{date}/{filename}
|
||
"""
|
||
sub_dir = {"image": "images", "video": "videos", "audio": "audios"}.get(media_type, "files")
|
||
|
||
# 判断是 Base64 还是 URL
|
||
if url.startswith("data:"):
|
||
return save_base64_file(url, sub_dir)
|
||
else:
|
||
return await download_file_from_url(url, sub_dir)
|