""" 管理员OSS文件管理路由 提供管理员上传文件到OSS的接口 """ from fastapi import APIRouter, UploadFile, File, Depends, HTTPException, Query from typing import List import oss2 from app.dependencies.admin_auth import get_current_admin from app.models.admin import AdminUser from app.services.oss_service import get_oss_service, OSSService router = APIRouter(prefix="/api/admin/oss", tags=["管理员OSS文件管理"]) # 文件大小限制配置 MAX_FILE_SIZE_DEFAULT = 20 * 1024 * 1024 # 20MB MAX_FILE_SIZE_COURSE = 2 * 1024 * 1024 * 1024 # 2GB def _get_file_size(upload_file: UploadFile) -> int: file_obj = upload_file.file try: current_pos = file_obj.tell() file_obj.seek(0, 2) size = file_obj.tell() file_obj.seek(current_pos) return size except Exception: return 0 @router.post("/upload") async def upload_file( file: UploadFile = File(...), prefix: str = Query(default="uploads", description="存储路径前缀"), current_admin: AdminUser = Depends(get_current_admin), oss_service: OSSService = Depends(get_oss_service) ): """上传文件到OSS(管理员)""" file_size = _get_file_size(file) max_size = MAX_FILE_SIZE_COURSE if prefix.startswith("courses") else MAX_FILE_SIZE_DEFAULT max_size_mb = max_size / (1024 * 1024) if file_size and file_size > max_size: raise HTTPException( status_code=400, detail=f"文件大小超过{max_size_mb:.0f}MB限制" ) try: file.file.seek(0) url = oss_service.upload_file_stream(file.file, prefix, file.filename) return { "url": url, "filename": file.filename, "size": file_size } except RuntimeError as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/presigned-url") async def get_presigned_url( prefix: str = Query(default="uploads", description="存储路径前缀"), filename: str = Query(default=None, description="原始文件名(用于保留扩展名)"), content_type: str = Query(default=None, description="文件 Content-Type"), current_admin: AdminUser = Depends(get_current_admin), oss_service: OSSService = Depends(get_oss_service) ): """生成预签名 PUT 上传 URL,供前端直传 OSS 前端先请求此接口获取 presigned_url,然后直接 PUT 到 OSS。 上传完成后,将返回的 object_key 和 public_url 用于业务记录。 """ max_size = MAX_FILE_SIZE_COURSE if prefix.startswith("courses") else MAX_FILE_SIZE_DEFAULT max_size_mb = max_size / (1024 * 1024) try: result = oss_service.generate_presigned_put_url( prefix=prefix, original_filename=filename, expires=3600, # 1小时有效期 content_type=content_type, ) result["max_size_mb"] = max_size_mb return result except RuntimeError as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/list-videos") async def list_oss_videos( prefix: str = Query(default="courses/videos", description="OSS路径前缀"), current_admin: AdminUser = Depends(get_current_admin), oss_service: OSSService = Depends(get_oss_service) ): """列出OSS中指定前缀下的视频文件 优化:先用 ObjectIterator(fetch_stats=True) 快速列出所有文件, 过滤出视频后,批量并发 head_object 获取元数据,避免逐个串行请求。 """ if not oss_service.bucket: raise HTTPException(status_code=500, detail="OSS服务未正确配置") VIDEO_EXTS = {'.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.wmv', '.m4v'} video_keys = [] try: for obj in oss2.ObjectIterator(oss_service.bucket, prefix=prefix): key = obj.key ext = '.' + key.rsplit('.', 1)[-1].lower() if '.' in key else '' if ext not in VIDEO_EXTS: continue video_keys.append({ "key": key, "size": obj.size, "last_modified": obj.last_modified, "default_filename": key.rsplit('/', 1)[-1], }) except Exception as e: raise HTTPException(status_code=500, detail=f"列举OSS文件失败: {str(e)}") if not video_keys: return {"items": [], "total": 0} # 批量并发获取元数据(避免逐个串行请求) import asyncio async def _fetch_meta(item: dict) -> dict: try: head_result = await asyncio.to_thread( oss_service.bucket.head_object, item["key"] ) filename = oss_service.get_original_filename_from_headers( getattr(head_result, 'headers', None), fallback=item["default_filename"], ) except Exception: filename = item["default_filename"] return { "key": item["key"], "filename": filename, "url": f"https://{oss_service.bucket_domain}/{item['key']}", "size": item["size"], "last_modified": item["last_modified"], } try: results = await asyncio.gather(*[_fetch_meta(item) for item in video_keys]) except Exception as e: raise HTTPException(status_code=500, detail=f"获取文件元数据失败: {str(e)}") results = sorted(results, key=lambda x: x["last_modified"], reverse=True) return {"items": results, "total": len(results)}