modify: aiofile 文件覆盖问题
This commit is contained in:
@@ -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="告警上报接口",
|
||||||
|
|||||||
Reference in New Issue
Block a user