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"), # "temp" 或 "permanent" ): """上传文件接口 参数: - 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(), []))}", ) # 生成文件ID 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()