Files
aiagent/backend/app/api/uploads.py

113 lines
3.3 KiB
Python
Raw Normal View History

"""
预览 / 聊天附件上传写入 LOCAL_FILE_TOOLS_ROOT file_read 等工具使用
"""
from __future__ import annotations
import re
import uuid
import logging
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from pydantic import BaseModel
from app.api.auth import get_current_user
from app.core.config import settings
from app.models.user import User
from app.services.builtin_tools import _local_file_workspace_root
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/v1/uploads",
tags=["uploads"],
)
class PreviewUploadResponse(BaseModel):
"""相对工作区根的路径,与 file_read 约定一致(可用相对路径)。"""
relative_path: str
filename: str
size: int
content_type: str | None = None
def _safe_filename(name: str) -> str:
base = Path(name).name.strip()
if not base:
return "attachment.bin"
base = re.sub(r"[^\w.\-\u4e00-\u9fff]+", "_", base, flags=re.UNICODE)
return base[:180] if len(base) > 180 else base
@router.post(
"/preview",
response_model=PreviewUploadResponse,
status_code=status.HTTP_201_CREATED,
)
async def upload_preview_file(
file: UploadFile = File(...),
current_user: User = Depends(get_current_user),
):
"""
上传单个文件到工作区 `uploads/preview/<user_id>/`返回相对路径
大小上限与 LOCAL_FILE_WRITE_MAX_BYTES 一致
"""
max_bytes = max(1024, int(getattr(settings, "LOCAL_FILE_WRITE_MAX_BYTES", 2_097_152) or 2_097_152))
root = _local_file_workspace_root()
uid = str(current_user.id)
dest_dir = root / "uploads" / "preview" / uid
try:
dest_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
logger.warning("创建上传目录失败: %s", e)
raise HTTPException(status_code=500, detail="无法创建上传目录") from e
raw_name = file.filename or "attachment"
safe = _safe_filename(raw_name)
short = uuid.uuid4().hex[:12]
dest = dest_dir / f"{short}_{safe}"
total = 0
chunk_size = 1024 * 256
try:
with dest.open("wb") as out:
while True:
buf = await file.read(chunk_size)
if not buf:
break
total += len(buf)
if total > max_bytes:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"文件超过允许大小({max_bytes} 字节)",
)
out.write(buf)
except HTTPException:
try:
dest.unlink(missing_ok=True)
except OSError:
pass
raise
except OSError as e:
try:
dest.unlink(missing_ok=True)
except OSError:
pass
logger.warning("写入上传文件失败: %s", e)
raise HTTPException(status_code=500, detail="保存文件失败") from e
try:
rel = dest.relative_to(root).as_posix()
except ValueError:
rel = str(dest).replace("\\", "/")
logger.info("预览上传 user=%s path=%s size=%s", uid, rel, total)
return PreviewUploadResponse(
relative_path=rel,
filename=raw_name,
size=total,
content_type=file.content_type,
)