modify: aiofile 文件覆盖问题

This commit is contained in:
2026-07-28 16:19:27 +08:00
parent 48669eb2a9
commit e3265e40f0
+54 -5
View File
@@ -10,11 +10,14 @@ AI Box Sophon Adapter — 自定义 FastAPI 路由
import os import os
import json import json
import uuid import uuid
import hashlib
import logging import logging
import aiofiles
from fastapi import APIRouter, UploadFile, File, Form, Request, Body, HTTPException from fastapi import APIRouter, UploadFile, File, Form, Request, Body, HTTPException
from fastapi.responses import FileResponse
from pydantic import ValidationError from pydantic import ValidationError
from aiofile import AIOFile
from .models.video_models import VideoUploadResponse, VideoUploadResult from .models.video_models import VideoUploadResponse, VideoUploadResult
from .models.alarm_models import ( from .models.alarm_models import (
@@ -55,6 +58,8 @@ async def video_upload(
""" """
# 生成唯一 VideoId # 生成唯一 VideoId
video_id = uuid.uuid4().hex video_id = uuid.uuid4().hex
# 获取设备声明的文件大小(若 Starlette 不支持则记 -1)
declared_size = getattr(Video, 'size', -1) or -1
try: try:
# 复用网关存储路径约定: data/uploads/<worker_type>/<uuid>/<filename> # 复用网关存储路径约定: data/uploads/<worker_type>/<uuid>/<filename>
@@ -64,18 +69,24 @@ async def video_upload(
dest = os.path.join(upload_root, object_key) dest = os.path.join(upload_root, object_key)
os.makedirs(os.path.dirname(dest), exist_ok=True) os.makedirs(os.path.dirname(dest), exist_ok=True)
# 流式写入文件(与网关 /api/v1/storage/upload 一致) # 流式写入文件,同时计算 MD5 和总字节数
async with AIOFile(dest, "wb") as afp: total_bytes = 0
md5_hash = hashlib.md5()
async with aiofiles.open(dest, "wb") as afp:
while chunk := await Video.read(1024 * 1024): while chunk := await Video.read(1024 * 1024):
await afp.write(chunk) await afp.write(chunk)
total_bytes += len(chunk)
md5_hash.update(chunk)
file_size = os.path.getsize(dest) file_size = os.path.getsize(dest)
file_md5 = md5_hash.hexdigest()
logger.info( logger.info(
f"[VideoUpload] 视频已保存: VideoId={video_id}, " f"[VideoUpload] 视频已保存: VideoId={video_id}, "
f"BoardId={BoardId}, BoardIp={BoardIp}, " f"BoardId={BoardId}, BoardIp={BoardIp}, "
f"TaskSession={TaskSession}, File={Video.filename}, " f"TaskSession={TaskSession}, File={Video.filename}, "
f"Size={file_size} bytes, Path={object_key}" f"declared={declared_size}, received={total_bytes}, disk={file_size}, "
f"md5={file_md5}, Path={object_key}"
) )
return VideoUploadResponse( return VideoUploadResponse(
@@ -86,7 +97,7 @@ async def video_upload(
except Exception as e: except Exception as e:
logger.error( logger.error(
f"[VideoUpload] 上传失败: BoardId={BoardId}, " f"[VideoUpload] 上传失败: BoardId={BoardId}, "
f"BoardIp={BoardIp}, Error={e}" f"BoardIp={BoardIp}, Error={type(e).__name__}: {e}"
) )
return VideoUploadResponse( return VideoUploadResponse(
Result=VideoUploadResult(Code=-1, Desc=str(e)), Result=VideoUploadResult(Code=-1, Desc=str(e)),
@@ -94,6 +105,44 @@ async def video_upload(
) )
@router.get(
"/video/{video_id}",
summary="视频下载/播放",
description="根据 VideoId 下载或流式播放告警关联视频",
)
async def video_download(
request: Request,
video_id: str,
) -> FileResponse:
"""
视频播放端点
根据 video_upload 返回的 VideoId 查找并返回视频文件。
支持浏览器内嵌播放(Content-Disposition: inline)。
"""
upload_root = request.app.state.upload_root
worker_type = "aibox_sophon_adapter"
# 在 uploads 目录下查找匹配的视频文件
video_dir = os.path.join(upload_root, worker_type, video_id)
if not os.path.isdir(video_dir):
raise HTTPException(status_code=404, detail=f"视频不存在: {video_id}")
# 取目录下第一个文件作为视频(通常只有一个)
files = os.listdir(video_dir)
if not files:
raise HTTPException(status_code=404, detail=f"视频目录为空: {video_id}")
video_path = os.path.join(video_dir, files[0])
if not os.path.isfile(video_path):
raise HTTPException(status_code=404, detail=f"视频文件不存在: {files[0]}")
return FileResponse(
video_path,
media_type="video/mp4",
headers={"Content-Disposition": "inline"},
)
@router.post( @router.post(
"/alarm_report", "/alarm_report",
summary="告警上报接口", summary="告警上报接口",