fix(workspace): 修改文件上传逻辑,新增上传工具函数,支持文件大小限制和错误处理

This commit is contained in:
Wenjie Zhang 2026-05-04 08:24:13 +08:00
parent 02b371851d
commit a450be57e3
4 changed files with 96 additions and 19 deletions

View File

@ -2,7 +2,6 @@ import uuid
from dataclasses import dataclass
from pathlib import Path
import aiofiles
from fastapi import HTTPException, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.agents.backends.sandbox import (
@ -13,6 +12,7 @@ from yuxi.agents.buildin import agent_manager
from yuxi.config import config as app_config
from yuxi.plugins.parser import Parser
from yuxi.repositories.conversation_repository import ConversationRepository
from yuxi.services.upload_utils import write_upload_to_path
from yuxi.utils.datetime_utils import utc_isoformat
from yuxi.utils.logging_config import logger
from yuxi.utils.paths import VIRTUAL_PATH_UPLOADS
@ -41,21 +41,12 @@ def _ensure_workdir() -> Path:
async def _write_upload_to_disk(upload: UploadFile, dest: Path) -> int:
await upload.seek(0)
written = 0
chunk_size = 1024 * 1024
async with aiofiles.open(dest, "wb") as buffer:
while True:
chunk = await upload.read(chunk_size)
if not chunk:
break
written += len(chunk)
if written > MAX_ATTACHMENT_SIZE_BYTES:
raise ValueError("附件过大,当前仅支持 5 MB 以内的文件")
await buffer.write(chunk)
return written
return await write_upload_to_path(
upload,
dest,
max_size_bytes=MAX_ATTACHMENT_SIZE_BYTES,
too_large_message="附件过大,当前仅支持 5 MB 以内的文件",
)
def _truncate_markdown(markdown: str) -> tuple[str, bool]:

View File

@ -0,0 +1,43 @@
from pathlib import Path
import aiofiles
from fastapi import UploadFile
async def write_upload_to_buffer(
upload: UploadFile,
buffer,
*,
max_size_bytes: int,
too_large_message: str,
chunk_size: int = 1024 * 1024,
) -> int:
await upload.seek(0)
written = 0
while chunk := await upload.read(chunk_size):
written += len(chunk)
if written > max_size_bytes:
raise ValueError(too_large_message)
await buffer.write(chunk)
return written
async def write_upload_to_path(
upload: UploadFile,
dest: Path,
*,
max_size_bytes: int,
too_large_message: str,
mode: str = "wb",
chunk_size: int = 1024 * 1024,
) -> int:
async with aiofiles.open(dest, mode) as buffer:
return await write_upload_to_buffer(
upload,
buffer,
max_size_bytes=max_size_bytes,
too_large_message=too_large_message,
chunk_size=chunk_size,
)

View File

@ -12,12 +12,14 @@ import aiofiles
from fastapi import HTTPException, UploadFile
from fastapi.responses import FileResponse, StreamingResponse
from yuxi.agents.backends.sandbox.paths import _global_user_data_dir, ensure_workspace_default_files
from yuxi.services.upload_utils import write_upload_to_buffer
from yuxi.services.viewer_filesystem_service import _detect_preview_type
from yuxi.storage.postgres.models_business import User
from yuxi.utils.datetime_utils import utc_isoformat_from_timestamp
from yuxi.utils.paths import WORKSPACE_DIR_NAME
EDITABLE_WORKSPACE_SUFFIXES = {".md", ".markdown", ".mdx", ".txt"}
MAX_WORKSPACE_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024
def _workspace_root(user: User) -> Path:
@ -237,11 +239,17 @@ async def upload_workspace_file(*, parent_path: str, file: UploadFile, current_u
try:
async with aiofiles.open(target, "xb") as buffer:
created_file = True
while chunk := await file.read(1024 * 1024):
await buffer.write(chunk)
await write_upload_to_buffer(
file,
buffer,
max_size_bytes=MAX_WORKSPACE_UPLOAD_SIZE_BYTES,
too_large_message="文件过大,当前仅支持 100 MB 以内的文件",
)
upload_completed = True
except FileExistsError as exc:
raise HTTPException(status_code=400, detail="同名文件或文件夹已存在") from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
finally:

View File

@ -1,10 +1,11 @@
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from types import SimpleNamespace
import pytest
from fastapi import HTTPException
from fastapi import HTTPException, UploadFile
from yuxi.agents.backends.sandbox import paths as workspace_paths
from yuxi.services import workspace_service as svc
@ -146,3 +147,37 @@ async def test_write_workspace_file_content_blocks_path_traversal(tmp_path: Path
)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_upload_workspace_file_writes_file(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr(workspace_paths.conf, "save_dir", str(tmp_path))
user = SimpleNamespace(id="user-1")
root = svc._workspace_root(user)
upload = UploadFile(filename="demo.txt", file=BytesIO(b"hello"))
result = await svc.upload_workspace_file(parent_path="/", file=upload, current_user=user)
assert result["success"] is True
assert result["entry"]["path"] == "/demo.txt"
assert result["entry"]["size"] == 5
assert (root / "demo.txt").read_bytes() == b"hello"
@pytest.mark.asyncio
async def test_upload_workspace_file_rejects_oversized_file_and_cleans_partial_file(
tmp_path: Path,
monkeypatch,
) -> None:
monkeypatch.setattr(workspace_paths.conf, "save_dir", str(tmp_path))
monkeypatch.setattr(svc, "MAX_WORKSPACE_UPLOAD_SIZE_BYTES", 5)
user = SimpleNamespace(id="user-1")
root = svc._workspace_root(user)
upload = UploadFile(filename="large.txt", file=BytesIO(b"123456"))
with pytest.raises(HTTPException) as exc_info:
await svc.upload_workspace_file(parent_path="/", file=upload, current_user=user)
assert exc_info.value.status_code == 400
assert "100 MB" in exc_info.value.detail
assert not (root / "large.txt").exists()