fix(workspace): 修改文件上传逻辑,新增上传工具函数,支持文件大小限制和错误处理
This commit is contained in:
parent
02b371851d
commit
a450be57e3
@ -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]:
|
||||
|
||||
43
backend/package/yuxi/services/upload_utils.py
Normal file
43
backend/package/yuxi/services/upload_utils.py
Normal 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,
|
||||
)
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user