| import os |
| import uuid |
| import asyncio |
| import logging |
| from datetime import datetime |
| from pathlib import Path |
| from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks |
| from fastapi.responses import FileResponse, JSONResponse |
|
|
| router = APIRouter() |
|
|
| |
| plugin = None |
| cleanup_task_handle = None |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def stop_cleanup_task(): |
| """停止清理任务""" |
| global cleanup_task_handle |
| if cleanup_task_handle and not cleanup_task_handle.done(): |
| cleanup_task_handle.cancel() |
| cleanup_task_handle = None |
| logger.info("文件清理定时任务已停止") |
|
|
|
|
| def start_cleanup_task(): |
| """启动清理任务""" |
| global cleanup_task_handle |
| if plugin and not cleanup_task_handle: |
| try: |
| loop = asyncio.get_event_loop() |
| if loop.is_running(): |
| cleanup_task_handle = loop.create_task(periodic_cleanup()) |
| logger.info("文件清理定时任务已启动") |
| else: |
| logger.warning("事件循环未运行,无法启动定时任务") |
| except RuntimeError as e: |
| logger.warning(f"启动定时任务失败: {e}") |
|
|
|
|
| def set_plugin_instance(plugin_instance): |
| """由系统调用,注入插件实例""" |
| global plugin |
| plugin = plugin_instance |
| start_cleanup_task() |
|
|
|
|
| async def periodic_cleanup(): |
| """定期清理任务""" |
| while True: |
| try: |
| await asyncio.sleep(3600) |
| if plugin: |
| await plugin.cleanup_expired_files() |
| except asyncio.CancelledError: |
| break |
| except Exception as e: |
| logger.error(f"清理任务出错: {e}") |
|
|
|
|
| @router.post("/upload") |
| async def upload_file( |
| file: UploadFile = File(...), |
| file_type: str = Form("temp"), |
| ): |
| """上传文件接口 |
| |
| 参数: |
| - file: 上传的文件 |
| - file_type: 文件类型,"temp" 为临时文件(8小时后自动删除),"permanent" 为永久文件 |
| |
| 返回: |
| { |
| "file_id": "生成的文件ID", |
| "filename": "文件名", |
| "url": "临时访问URL", |
| "type": "文件类型", |
| "created": "创建时间" |
| } |
| """ |
| if not plugin: |
| raise HTTPException(status_code=500, detail="插件未加载") |
|
|
| try: |
| |
| if file_type not in ["temp", "permanent"]: |
| raise HTTPException( |
| status_code=400, detail="无效的文件类型,应为 'temp' 或 'permanent'" |
| ) |
|
|
| |
| allowed_extensions = { |
| "image": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"], |
| "video": [".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".webm"], |
| "audio": [".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a"], |
| "document": [".md", ".txt", ".pdf", ".doc", ".docx"], |
| } |
|
|
| file_ext = Path(file.filename).suffix.lower() |
| file_category = None |
|
|
| for category, extensions in allowed_extensions.items(): |
| if file_ext in extensions: |
| file_category = category |
| break |
|
|
| if not file_category: |
| raise HTTPException( |
| status_code=400, |
| detail=f"不支持的文件格式: {file_ext}。支持的格式: {', '.join(sum(allowed_extensions.values(), []))}", |
| ) |
|
|
| |
| file_id = str(uuid.uuid4()) |
|
|
| |
| content = await file.read() |
|
|
| |
| is_temp = file_type == "temp" |
| success = await plugin.save_file( |
| file_id, content, file.filename, is_temp=is_temp |
| ) |
|
|
| if not success: |
| raise HTTPException(status_code=500, detail="保存文件失败") |
|
|
| |
| metadata = plugin.file_metadata[file_id] |
|
|
| return { |
| "file_id": file_id, |
| "filename": file.filename, |
| "url": f"/plugins/cache/api/file/{file_id}", |
| "type": file_type, |
| "created": datetime.fromtimestamp(metadata["created"]).isoformat(), |
| "message": "文件上传成功", |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"上传文件时出错: {str(e)}") |
|
|
|
|
| @router.get("/list") |
| async def list_files(): |
| """获取文件列表接口 |
| |
| 返回: |
| { |
| "files": [ |
| { |
| "file_id": "文件ID", |
| "name": "文件名", |
| "type": "temp/permanent", |
| "created": "创建时间", |
| "url": "临时访问URL" |
| } |
| ], |
| "total": 文件总数 |
| } |
| """ |
| if not plugin: |
| raise HTTPException(status_code=500, detail="插件未加载") |
|
|
| try: |
| file_list = plugin.get_file_list() |
| return {"files": file_list, "total": len(file_list)} |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"获取文件列表失败: {str(e)}") |
|
|
|
|
| @router.get("/file/{file_id}") |
| async def download_file(file_id: str): |
| """下载文件接口 |
| |
| 参数: |
| - file_id: 文件ID |
| |
| 返回:文件内容 |
| """ |
| if not plugin: |
| raise HTTPException(status_code=500, detail="插件未加载") |
|
|
| try: |
| file_path = plugin.get_file_path(file_id) |
|
|
| if not file_path: |
| raise HTTPException(status_code=404, detail="文件不存在") |
|
|
| file_path_obj = Path(file_path) |
| if not file_path_obj.exists(): |
| raise HTTPException(status_code=404, detail="文件不存在") |
|
|
| |
| return FileResponse( |
| path=file_path_obj, |
| filename=Path(file_path).name, |
| media_type="application/octet-stream", |
| ) |
| except HTTPException: |
| raise |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"下载文件失败: {str(e)}") |
|
|
|
|
| @router.delete("/file/{file_id}") |
| async def delete_file(file_id: str): |
| """删除文件接口 |
| |
| 参数: |
| - file_id: 文件ID |
| |
| 返回: |
| { |
| "success": true/false, |
| "message": "删除结果消息" |
| } |
| """ |
| if not plugin: |
| raise HTTPException(status_code=500, detail="插件未加载") |
|
|
| try: |
| success = await plugin.delete_file(file_id) |
|
|
| if not success: |
| raise HTTPException(status_code=404, detail="文件不存在或已删除") |
|
|
| return {"success": True, "message": "文件删除成功"} |
| except HTTPException: |
| raise |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"删除文件失败: {str(e)}") |
|
|
|
|
| @router.get("/status") |
| async def get_status(): |
| """获取插件状态接口 |
| |
| 返回: |
| { |
| "name": "插件名称", |
| "enabled": true/false, |
| "file_count": 文件总数, |
| "cache_dir": "缓存目录路径" |
| } |
| """ |
| if not plugin: |
| return {"error": "插件未加载"} |
|
|
| return plugin.get_status() |
|
|