""" AIoT 文件存储路由 - COS 对象存储接口 API 路径规范: - POST /admin-api/aiot/storage/upload - 后端中转上传 - GET /admin-api/aiot/storage/presign - 获取预签名下载 URL - GET /admin-api/aiot/storage/sts - 获取 STS 临时凭证(前端直传) - GET /admin-api/aiot/storage/upload-url - 获取预签名上传 URL(前端直传) """ import os from fastapi import APIRouter, Query, Depends, HTTPException, UploadFile, File from app.yudao_compat import YudaoResponse, get_current_user from app.services.oss_storage import get_oss_storage, COSStorage, _generate_object_key router = APIRouter(prefix="/admin-api/aiot/storage", tags=["AIoT-文件存储"]) @router.post("/upload") async def upload_file( file: UploadFile = File(..., description="上传文件"), prefix: str = Query("alerts", description="存储路径前缀"), current_user: dict = Depends(get_current_user), ): """ 后端中转上传 文件经过后端写入 COS / 本地,返回 object_key。 适用于服务端需要对文件做校验或处理的场景。 """ storage = get_oss_storage() # 文件大小检查(限制 20MB) content = await file.read() if len(content) > 20 * 1024 * 1024: raise HTTPException(status_code=400, detail="文件大小超过 20MB 限制") # 文件类型检查 allowed_types = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".mp4", ".mov", ".pdf"} _, ext = os.path.splitext(file.filename or "unknown.jpg") ext = ext.lower() if ext not in allowed_types: raise HTTPException(status_code=400, detail=f"不支持的文件类型: {ext}") # 确定 content_type content_type = file.content_type or "application/octet-stream" # 生成 object_key object_key = _generate_object_key(prefix=prefix, ext=ext) # 上传 result_key = storage.upload_file(content, object_key, content_type) return YudaoResponse.success({ "objectKey": result_key, "filename": file.filename, "size": len(content), "contentType": content_type, }) @router.get("/presign") async def get_presigned_download_url( objectKey: str = Query(..., description="对象 Key"), expire: int = Query(1800, description="有效期(秒)"), current_user: dict = Depends(get_current_user), ): """ 获取预签名下载 URL 前端查看告警截图时调用此接口,拿到临时 URL 后直接访问 COS。 URL 过期后失效,防止泄露。 """ storage = get_oss_storage() url = storage.get_presigned_url(objectKey, expire) return YudaoResponse.success({ "url": url, "expire": expire, }) @router.get("/upload-url") async def get_presigned_upload_url( prefix: str = Query("alerts", description="存储路径前缀"), ext: str = Query(".jpg", description="文件扩展名"), expire: int = Query(1800, description="有效期(秒)"), current_user: dict = Depends(get_current_user), ): """ 获取预签名上传 URL 前端拿到此 URL 后,直接 PUT 文件到 COS,无需经过后端中转。 适用于大文件或高频上传场景。 """ storage = get_oss_storage() if not storage.is_cos_mode: raise HTTPException(status_code=400, detail="COS 未启用,请使用 /upload 接口") object_key = _generate_object_key(prefix=prefix, ext=ext) url = storage.get_presigned_upload_url(object_key, expire) if not url: raise HTTPException(status_code=500, detail="生成上传 URL 失败") return YudaoResponse.success({ "uploadUrl": url, "objectKey": object_key, "expire": expire, }) @router.get("/sts") async def get_sts_credential( prefix: str = Query("alerts", description="允许上传的路径前缀"), current_user: dict = Depends(get_current_user), ): """ 获取 STS 临时凭证 前端使用 COS JS SDK 直传时,先调用此接口获取临时密钥。 返回的 tmpSecretId / tmpSecretKey / sessionToken 用于初始化 SDK。 凭证仅允许向指定前缀上传,不可读取或删除其他路径。 """ storage = get_oss_storage() if not storage.is_cos_mode: raise HTTPException(status_code=400, detail="COS 未启用,无法签发 STS 凭证") allow_prefix = f"{prefix}/*" credential = storage.get_sts_credential(allow_prefix) if not credential: raise HTTPException(status_code=500, detail="STS 凭证签发失败,请检查 COS 配置或安装 qcloud-python-sts") return YudaoResponse.success(credential)