| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- """
- 管理员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)}
|