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
23from sqlalchemy .orm import Session
34from database .database import get_db
45from auth .auth import get_current_user
56from schemas import schemas
67from services import crud
78from typing import Optional
89from ..utils import route_guard
10+ import os
11+ import uuid
12+ from pathlib import Path
13+ import json
14+ from datetime import datetime
915
1016router = 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