Skip to content

Commit 90a2d40

Browse files
authored
feat(attachments): 添加附件上传、下载、删除功能,更新相关接口和前端组件(#129)
* feat(attachments): 添加附件上传、下载、删除功能,更新相关接口和前端组件 * feat(attachment): 优化附件上传逻辑,处理文件名冲突并统一上传接口调用 * feat(workspace): 优化文件读取逻辑,支持多种文件类型处理并添加文件下载功能 * feat(attachment): 优化文件下载功能,添加错误处理,删除冗余代码 * feat(workspace): 添加文件下载功能,优化权限检查和错误处理 * feat(workspace): 修改文件下载接口路径,移除获取论文内容的相关代码,优化文件下载逻辑 * feat(workspace): 添加主要论文内容处理逻辑,修改文件读取接口以支持新格式
1 parent 700ee2a commit 90a2d40

File tree

10 files changed

+900
-263
lines changed

10 files changed

+900
-263
lines changed

.claude/settings.local.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
"permissions": {
33
"allow": [
44
"Bash(mkdir:*)",
5-
"Bash(rm:*)"
5+
"Bash(rm:*)",
6+
"Bash(find:*)",
7+
"Bash(python:*)",
8+
"mcp__context7__get-library-docs"
69
],
710
"deny": [],
811
"ask": []
912
}
10-
}
13+
}

backend/routers/work_routes/work.py

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
from fastapi import APIRouter, Depends, HTTPException, status, Query
1+
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File
2+
from fastapi.responses import StreamingResponse
23
from sqlalchemy.orm import Session
34
from database.database import get_db
45
from auth.auth import get_current_user
56
from schemas import schemas
67
from services import crud
78
from typing import Optional
89
from ..utils import route_guard
10+
import os
11+
import uuid
12+
from pathlib import Path
13+
import json
14+
from datetime import datetime
915

1016
router = APIRouter(prefix="/api/works", tags=["works"])
1117

@@ -128,3 +134,163 @@ async def get_work_chat_history(
128134
if isinstance(chat_history, dict):
129135
return chat_history
130136
return {"messages": [], "context": {}}
137+
138+
# 附件相关路由
139+
def get_attachment_dir(work_id: str) -> Path:
140+
"""获取附件目录路径"""
141+
base_dir = Path("../pa_data/workspaces") / work_id / "attachment"
142+
base_dir.mkdir(parents=True, exist_ok=True)
143+
return base_dir
144+
145+
def get_file_type(filename: str) -> str:
146+
"""根据文件扩展名获取文件类型"""
147+
ext = Path(filename).suffix.lower()
148+
type_map = {
149+
'.pdf': 'pdf',
150+
'.doc': 'word',
151+
'.docx': 'word',
152+
'.xls': 'excel',
153+
'.xlsx': 'excel',
154+
'.png': 'image',
155+
'.jpg': 'image',
156+
'.jpeg': 'image',
157+
'.gif': 'image',
158+
'.txt': 'text',
159+
'.md': 'markdown',
160+
'.zip': 'archive',
161+
'.rar': 'archive'
162+
}
163+
return type_map.get(ext, 'unknown')
164+
165+
@router.post("/{work_id}/attachment", response_model=schemas.AttachmentUploadResponse)
166+
@route_guard
167+
async def upload_attachment(
168+
work_id: str,
169+
file: UploadFile = File(...),
170+
db: Session = Depends(get_db),
171+
current_user: int = Depends(get_current_user)
172+
):
173+
"""上传附件到指定工作"""
174+
# 检查工作是否存在且属于当前用户
175+
work = crud.get_work(db, work_id)
176+
if not work:
177+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Work not found")
178+
if work.created_by != current_user:
179+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to access this work")
180+
181+
# 检查文件大小(50MB限制)
182+
max_size = 50 * 1024 * 1024 # 50MB
183+
file.file.seek(0, 2) # 移动到文件末尾
184+
file_size = file.file.tell()
185+
file.file.seek(0) # 重置到文件开头
186+
187+
if file_size > max_size:
188+
raise HTTPException(status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="File too large (max 50MB)")
189+
190+
# 使用原始文件名,不允许重名
191+
original_filename = file.filename
192+
attachment_dir = get_attachment_dir(work_id)
193+
194+
# 检查文件是否已存在
195+
final_filename = original_filename
196+
file_path = attachment_dir / final_filename
197+
198+
if file_path.exists():
199+
raise HTTPException(
200+
status_code=status.HTTP_409_CONFLICT,
201+
detail=f"文件名已存在: {original_filename}"
202+
)
203+
204+
# 保存文件
205+
try:
206+
with open(file_path, "wb") as buffer:
207+
content = await file.read()
208+
buffer.write(content)
209+
except Exception as e:
210+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save file: {str(e)}")
211+
212+
# 创建附件信息
213+
attachment_info = schemas.AttachmentInfo(
214+
filename=final_filename,
215+
original_filename=original_filename,
216+
file_type=get_file_type(original_filename),
217+
file_size=file_size,
218+
mime_type=file.content_type or "application/octet-stream",
219+
upload_time=datetime.now().isoformat()
220+
)
221+
222+
return schemas.AttachmentUploadResponse(
223+
message="Attachment uploaded successfully",
224+
attachment=attachment_info
225+
)
226+
227+
@router.get("/{work_id}/attachments", response_model=schemas.AttachmentListResponse)
228+
@route_guard
229+
async def get_attachments(
230+
work_id: str,
231+
db: Session = Depends(get_db),
232+
current_user: int = Depends(get_current_user)
233+
):
234+
"""获取工作的附件列表"""
235+
# 检查工作是否存在且属于当前用户
236+
work = crud.get_work(db, work_id)
237+
if not work:
238+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Work not found")
239+
if work.created_by != current_user:
240+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to access this work")
241+
242+
# 获取附件目录
243+
attachment_dir = get_attachment_dir(work_id)
244+
245+
# 列出所有附件
246+
attachments = []
247+
if attachment_dir.exists():
248+
for file_path in attachment_dir.iterdir():
249+
if file_path.is_file():
250+
# 从文件名中提取原始信息(这里简化处理)
251+
stat = file_path.stat()
252+
attachment_info = schemas.AttachmentInfo(
253+
filename=file_path.name,
254+
original_filename=file_path.name, # 实际中可以存储映射关系
255+
file_type=get_file_type(file_path.name),
256+
file_size=stat.st_size,
257+
mime_type="application/octet-stream", # 实际中可以存储
258+
upload_time=datetime.fromtimestamp(stat.st_mtime).isoformat()
259+
)
260+
attachments.append(attachment_info)
261+
262+
return schemas.AttachmentListResponse(
263+
attachments=attachments,
264+
total=len(attachments)
265+
)
266+
267+
@router.delete("/{work_id}/attachment/{filename}")
268+
@route_guard
269+
async def delete_attachment(
270+
work_id: str,
271+
filename: str,
272+
db: Session = Depends(get_db),
273+
current_user: int = Depends(get_current_user)
274+
):
275+
"""删除指定附件"""
276+
# 检查工作是否存在且属于当前用户
277+
work = crud.get_work(db, work_id)
278+
if not work:
279+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Work not found")
280+
if work.created_by != current_user:
281+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to access this work")
282+
283+
# 构建文件路径
284+
attachment_dir = get_attachment_dir(work_id)
285+
file_path = attachment_dir / filename
286+
287+
# 检查文件是否存在
288+
if not file_path.exists():
289+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Attachment not found")
290+
291+
# 删除文件
292+
try:
293+
file_path.unlink()
294+
return {"message": "Attachment deleted successfully"}
295+
except Exception as e:
296+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete file: {str(e)}")

0 commit comments

Comments
 (0)