fix : 增加s3分片上传接口
This commit is contained in:
@@ -4,5 +4,3 @@ S3_mount_dir=/mntS3
|
||||
S3_bucket_name=modal-media-cache
|
||||
S3_region=ap-northeast-2
|
||||
S3_cdn_endpoint=https://d2nj71io21vkj2.cloudfront.net
|
||||
#CF_KV_namespace_id=f24d396e0daa418e89a1d7074b435c24
|
||||
CF_KV_namespace_id=527a61fea05543f2a49d62889ba868c5
|
||||
@@ -33,7 +33,6 @@ sentry_sdk.init(dsn="https://dab7b7ae652216282c89f029a76bb10a@sentry.bowongai.co
|
||||
]
|
||||
)
|
||||
modal_kv_cache = MediaSourceKVCache(kv_name=config.modal_kv_name,
|
||||
cf_kv_id=config.CF_KV_namespace_id,
|
||||
environment=config.modal_environment, )
|
||||
|
||||
sentry_header_schema = {
|
||||
|
||||
@@ -14,10 +14,6 @@ class WorkerConfig(BaseSettings):
|
||||
S3_mount_dir: str = Field(default='/mntS3', description="集群S3存储桶挂载在本地的根目录")
|
||||
S3_cdn_endpoint: str = Field(default="https://d2nj71io21vkj2.cloudfront.net",
|
||||
description="集群挂载S3存储桶的对应AWS Cloudfront CDN")
|
||||
|
||||
CF_KV_namespace_id: str = Field(default="527a61fea05543f2a49d62889ba868c5",
|
||||
description="Cloudflare KV namespace ID")
|
||||
|
||||
modal_kv_name: str = Field(default='media-cache', description="Modal视频缓存KV库")
|
||||
modal_environment: str = Field(default="dev", description="Modal worker运行环境")
|
||||
modal_app_name: str = Field(default='bowong-ai-video', description="Modal App集群名称")
|
||||
|
||||
@@ -7,7 +7,7 @@ from functools import cached_property
|
||||
from typing import List, Union, Optional, Any, Dict
|
||||
from urllib.parse import urlparse
|
||||
from pydantic import (BaseModel, Field, field_validator, ValidationError,
|
||||
field_serializer, SerializationInfo, computed_field, FileUrl, Base64Str, Base64Bytes)
|
||||
field_serializer, SerializationInfo, computed_field, FileUrl, Base64Str, Base64Bytes, ConfigDict)
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
from ..config import WorkerConfig
|
||||
from ..utils.TimeUtils import TimeDelta
|
||||
@@ -270,7 +270,42 @@ class UploadPresignRequest(BaseModel):
|
||||
key: str = Field(description="上传文件的key", examples=['123/456/abc.mp4'])
|
||||
content_type: str = Field(description="上传对象的文件类型", examples=['video/mp4'])
|
||||
|
||||
|
||||
class UploadPresignResponse(BaseModel):
|
||||
url: str = Field(description="就近加速的PUT上传地址")
|
||||
urn: str = Field(description="上传成功后获得的对应资源URN")
|
||||
expired_at: datetime = Field(description="上传地址签名过期时间戳")
|
||||
|
||||
|
||||
class UploadMultipartPresignRequest(UploadPresignRequest):
|
||||
parts_count: int = Field(description="分片数量")
|
||||
|
||||
|
||||
class UploadMultipartPresignResponse(BaseModel):
|
||||
urls: List[str] = Field(description="就近加速的PUT分片上传地址")
|
||||
list_url: str = Field(description="用于确认分片上传状态的请求地址")
|
||||
complete_url: str = Field(description="用于确认完成分片上传的请求地址")
|
||||
urn: str = Field(description="上传成功后获得的对应资源URN")
|
||||
expired_at: datetime = Field(description="上传地址签名过期时间戳")
|
||||
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"description": """
|
||||
1. 本地按文件总大小分Chunk大小按urls链接内的顺序通过HTTP PUT请求上传文件分片; 并将上传完成后获得的返回头ETag值记录与PartNumber对应, PartNumber对应使用url在urls内的顺位, 以1开始
|
||||
2. 所有分片上传完成后使用XML格式拼装出用于确认上传的body; 并通过HTTP POST complete_url确认上传, ContentType 需要确保为application/xml
|
||||
<CompleteMultipartUpload>
|
||||
<Part>
|
||||
<PartNumber>1</PartNumber>
|
||||
<ETag>"60575364b098a1a48765a28c3a48e0ef"</ETag>
|
||||
</Part>
|
||||
<Part>
|
||||
<PartNumber>2</PartNumber>
|
||||
<ETag>"a38691c31fd242faee5533c65b4501d7"</ETag>
|
||||
</Part>
|
||||
<Part>
|
||||
<PartNumber>3</PartNumber>
|
||||
<ETag>"7a7510cc83f98feea28f319567e4cf66"</ETag>
|
||||
</Part>
|
||||
</CompleteMultipartUpload>
|
||||
3. 如无法确认上传,可使用HTTP GET list_url debug当前分片上传状态,确认成功后list_url无法返回有效数据
|
||||
"""
|
||||
})
|
||||
|
||||
@@ -10,6 +10,9 @@ import sentry_sdk
|
||||
from fastapi import APIRouter, Depends, UploadFile, HTTPException, File, Form
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from starlette import status
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
|
||||
from ..config import WorkerConfig
|
||||
from ..middleware.authorization import verify_token
|
||||
from ..models.media_model import (MediaSources,
|
||||
@@ -18,7 +21,8 @@ from ..models.media_model import (MediaSources,
|
||||
MediaCacheStatus,
|
||||
DownloadResult,
|
||||
UploadResultResponse,
|
||||
UploadBase64Request, UploadPresignRequest, UploadPresignResponse
|
||||
UploadBase64Request, UploadPresignRequest, UploadPresignResponse,
|
||||
UploadMultipartPresignRequest, UploadMultipartPresignResponse
|
||||
)
|
||||
from ..models.web_model import SentryTransactionInfo
|
||||
from ..utils.KVCache import MediaSourceKVCache
|
||||
@@ -26,12 +30,19 @@ from ..utils.SentryUtils import SentryUtils
|
||||
|
||||
config = WorkerConfig()
|
||||
|
||||
client = boto3.client("s3",
|
||||
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
|
||||
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
||||
region_name=config.S3_region,
|
||||
endpoint_url="https://s3-accelerate.amazonaws.com",
|
||||
config=Config(
|
||||
s3={'addressing_style': 'virtual'},
|
||||
signature_version='s3v4', )
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/cache", tags=['缓存'], )
|
||||
if not config.CF_KV_namespace_id:
|
||||
raise ValueError("未配置Cloudflare KV namespace ID")
|
||||
|
||||
modal_kv_cache = MediaSourceKVCache(kv_name=config.modal_kv_name,
|
||||
cf_kv_id=config.CF_KV_namespace_id,
|
||||
environment=config.modal_environment)
|
||||
|
||||
|
||||
@@ -257,24 +268,11 @@ async def s3_upload_base64(body: UploadBase64Request) -> UploadResultResponse:
|
||||
return UploadResultResponse(media=media_source)
|
||||
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
|
||||
|
||||
@router.post('/upload-s3/simple/presign',
|
||||
summary="S3简单上传预签名",
|
||||
description="利用S3就近接入点上传",
|
||||
dependencies=[Depends(verify_token)])
|
||||
async def s3_presign_upload(body: UploadPresignRequest) -> UploadPresignResponse:
|
||||
client = boto3.client("s3",
|
||||
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
|
||||
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
||||
region_name=config.S3_region,
|
||||
endpoint_url="https://s3-accelerate.amazonaws.com",
|
||||
config=Config(
|
||||
s3={'addressing_style': 'virtual'},
|
||||
signature_version='s3v4', )
|
||||
)
|
||||
expires_in = 3600
|
||||
expired_at = datetime.datetime.now() + datetime.timedelta(seconds=expires_in)
|
||||
signed_url = client.generate_presigned_url("put_object",
|
||||
@@ -286,3 +284,43 @@ async def s3_presign_upload(body: UploadPresignRequest) -> UploadPresignResponse
|
||||
return UploadPresignResponse(url=signed_url,
|
||||
urn=f"s3://{config.S3_region}/{config.S3_bucket_name}/upload/{body.key}",
|
||||
expired_at=expired_at)
|
||||
|
||||
|
||||
@router.post("/upload-s3/multipart/presign",
|
||||
summary="S3分片上传预签名",
|
||||
description="", dependencies=[Depends(verify_token)])
|
||||
async def s3_presign_upload_multipart(body: UploadMultipartPresignRequest) -> UploadMultipartPresignResponse:
|
||||
chunk_count = body.parts_count
|
||||
multipart_upload_response = client.create_multipart_upload(Bucket=config.S3_bucket_name, Key=body.key,
|
||||
ContentType=body.content_type, )
|
||||
upload_id = multipart_upload_response.get("UploadId")
|
||||
signed_urls = []
|
||||
expires_in = 3600
|
||||
expired_at = datetime.datetime.now() + datetime.timedelta(seconds=expires_in)
|
||||
for i in range(chunk_count):
|
||||
signed_url = client.generate_presigned_url("upload_part",
|
||||
Params={
|
||||
'Bucket': config.S3_bucket_name,
|
||||
'Key': body.key,
|
||||
'PartNumber': i + 1,
|
||||
'UploadId': upload_id,
|
||||
}, ExpiresIn=expires_in)
|
||||
signed_urls.append(signed_url)
|
||||
|
||||
signed_completed_url = client.generate_presigned_url("complete_multipart_upload",
|
||||
Params={
|
||||
'Bucket': config.S3_bucket_name,
|
||||
'Key': body.key,
|
||||
'UploadId': upload_id,
|
||||
}, ExpiresIn=expires_in)
|
||||
signed_list_url = client.generate_presigned_url("list_parts",
|
||||
Params={
|
||||
'Bucket': config.S3_bucket_name,
|
||||
'Key': body.key,
|
||||
'UploadId': upload_id,
|
||||
}, ExpiresIn=expires_in)
|
||||
return UploadMultipartPresignResponse(urls=signed_urls,
|
||||
urn=f"s3://{config.S3_region}/{config.S3_bucket_name}/upload/{body.key}",
|
||||
expired_at=expired_at,
|
||||
complete_url=signed_completed_url,
|
||||
list_url=signed_list_url)
|
||||
|
||||
@@ -11,16 +11,18 @@ from ..models.media_model import MediaSource
|
||||
# cf_kv_api_token = os.environ.get("CF_KV_API_TOKEN")
|
||||
# cf_kv_namespace_id = os.environ.get("CF_KV_NAMESPACE_ID")
|
||||
|
||||
# secrets = modal.Secret.from_name("cf-kv-secret")
|
||||
|
||||
class KVCache:
|
||||
kv: modal.Dict
|
||||
cf_kv_id: str
|
||||
cf_kv_id: str = os.environ.get("CF_KV_NAMESPACE_ID")
|
||||
cf_account_id: str = os.environ.get("CF_ACCOUNT_ID")
|
||||
cf_kv_api_token: str = os.environ.get("CF_KV_API_TOKEN")
|
||||
|
||||
def __init__(self, kv_name: str, cf_kv_id: str, environment: str):
|
||||
self.cf_kv_id = cf_kv_id
|
||||
def __init__(self, kv_name: str, environment: str):
|
||||
# self.cf_kv_id = cf_kv_id
|
||||
self.kv = modal.Dict.from_name(kv_name, environment_name=environment, create_if_missing=True)
|
||||
logger.info(f"Using KV space : {self.cf_kv_id}")
|
||||
|
||||
def batch_update_cloudflare_kv(self, caches: Dict[str, str]):
|
||||
with httpx.Client() as client:
|
||||
@@ -69,7 +71,6 @@ class KVCache:
|
||||
|
||||
|
||||
class MediaSourceKVCache(KVCache):
|
||||
|
||||
def get_cache(self, urn: str) -> Optional[MediaSource]:
|
||||
cache_json = self.kv.get(urn)
|
||||
if not cache_json:
|
||||
|
||||
@@ -64,11 +64,10 @@ class SentryUtils:
|
||||
return decorator
|
||||
|
||||
@staticmethod
|
||||
def webhook_handler(webhook: WebhookNotify, func_id: str):
|
||||
def webhook_handler(webhook: WebhookNotify, func_id: str, raise_on_giveup: bool = False):
|
||||
def decorator(func):
|
||||
|
||||
@backoff.on_exception(exception=Exception, wait_gen=backoff.constant,
|
||||
max_time=15, max_tries=5, raise_on_giveup=True)
|
||||
max_time=15, max_tries=5, raise_on_giveup=raise_on_giveup)
|
||||
def webhook_with_retry(webhook: WebhookNotify, body: BaseFFMPEGTaskStatusResponse):
|
||||
if webhook.method == WebhookMethodEnum.POST:
|
||||
response = httpx.post(url=webhook.endpoint.__str__(),
|
||||
|
||||
@@ -137,7 +137,7 @@ class VideoMetadata(BaseModel):
|
||||
audio = AudioStream.model_validate(stream)
|
||||
streams.append(audio)
|
||||
elif stream.get("codec_type") == 'video':
|
||||
if stream.get("codec_name") in ("gif", "png", "mjpg", "jpeg", "webp"):
|
||||
if stream.get("codec_name") in ("gif", "png", "mjpg", "mjpeg", "webp"):
|
||||
logger.info("Parsing image stream")
|
||||
image = ImageStream.model_validate(stream)
|
||||
streams.append(image)
|
||||
|
||||
@@ -3,15 +3,14 @@ from BowongModalFunctions.config import WorkerConfig
|
||||
from .video import app as media_app
|
||||
from .web import app as web_app
|
||||
from .ffmpeg_app import app as ffmpeg_app
|
||||
from .comfyui_v1 import app as comfyui_v1_app
|
||||
from .comfyui_v2 import app as comfyui_v2_app
|
||||
# from .comfyui_v1 import app as comfyui_v1_app
|
||||
# from .comfyui_v2 import app as comfyui_v2_app
|
||||
|
||||
config = WorkerConfig()
|
||||
|
||||
app = modal.App(config.modal_app_name,
|
||||
include_source=False,
|
||||
secrets=[modal.Secret.from_name("cf-kv-secret",
|
||||
environment_name=config.modal_environment)])
|
||||
secrets=[modal.Secret.from_name("cf-kv-secret")])
|
||||
|
||||
app.include(media_app)
|
||||
app.include(ffmpeg_app)
|
||||
|
||||
@@ -16,7 +16,7 @@ app = modal.App(
|
||||
image=downloader_image,
|
||||
include_source=False,
|
||||
secrets=[
|
||||
modal.Secret.from_name("cf-kv-secret", environment_name='dev'),
|
||||
modal.Secret.from_name("cf-kv-secret"),
|
||||
])
|
||||
|
||||
with downloader_image.imports():
|
||||
@@ -55,7 +55,6 @@ with downloader_image.imports():
|
||||
cf_kv_namespace_id = os.environ.get("CF_KV_NAMESPACE_ID")
|
||||
|
||||
modal_kv_cache = MediaSourceKVCache(kv_name=config.modal_kv_name,
|
||||
cf_kv_id=config.CF_KV_namespace_id,
|
||||
environment=config.modal_environment)
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ app = modal.App(
|
||||
secrets=[
|
||||
modal.Secret.from_name('google-secret'),
|
||||
modal.Secret.from_name('aws-s3-secret'),
|
||||
modal.Secret.from_name('cf-kv-secret'),
|
||||
],
|
||||
include_source=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user