feat: 实现视频生成完成后自动下载并上传到云端服务

新增功能:
1. 视频生成完成后自动下载到本地临时文件
2. 自动上传到云端S3服务
3. 将S3 URL转换为CDN HTTPS地址
4. 支持防盗链绕过的视频下载
5. 完善的错误处理和fallback机制

技术实现:
- download_and_upload_video(): 主要流程控制
- download_video_to_file(): 下载视频到本地临时文件
- 使用CloudUploadService上传到S3
- convert_s3_to_cdn_url(): S3到CDN URL转换
- 临时文件自动清理机制

用户体验:
- 视频生成完成后自动获得可访问的CDN链接
- 无需手动下载和上传操作
- 支持原始URL作为fallback保证可用性
- 详细的日志记录便于问题排查

安全特性:
- 防盗链绕过HTTP头设置
- 临时文件安全清理
- 错误情况下的资源释放
- 超时控制防止长时间阻塞
This commit is contained in:
imeepos 2025-07-31 14:03:51 +08:00
parent d4b9e77020
commit 6c43e6f08b
1 changed files with 110 additions and 4 deletions

View File

@ -325,10 +325,22 @@ impl VolcanoVideoService {
match data.status.as_str() {
"done" => {
if let Some(video_url) = data.video_url {
record.mark_as_completed(video_url, None); // 火山云API没有缩略图
self.repository.update(&record).await?;
info!("视频生成任务完成: {}", record_id);
return Ok(());
// 下载视频并上传到云端
match self.download_and_upload_video(&video_url, &record_id).await {
Ok(cdn_url) => {
record.mark_as_completed(cdn_url, None); // 使用CDN URL
self.repository.update(&record).await?;
info!("视频生成任务完成已上传到CDN: {}", record_id);
return Ok(());
}
Err(e) => {
warn!("视频上传到CDN失败使用原始URL: {} - {}", record_id, e);
record.mark_as_completed(video_url, None); // 使用原始URL作为fallback
self.repository.update(&record).await?;
info!("视频生成任务完成使用原始URL: {}", record_id);
return Ok(());
}
}
} else {
record.mark_as_failed("视频URL为空".to_string(), Some("EMPTY_VIDEO_URL".to_string()));
self.repository.update(&record).await?;
@ -594,6 +606,100 @@ impl VolcanoVideoService {
Ok(hex::encode(signature))
}
/// 下载视频并上传到云端服务
async fn download_and_upload_video(&self, video_url: &str, record_id: &str) -> Result<String> {
info!("开始下载并上传视频: {} -> {}", video_url, record_id);
// 创建临时文件路径
let temp_dir = std::env::temp_dir();
let temp_filename = format!("volcano_video_{}_{}.mp4", record_id, chrono::Utc::now().timestamp());
let temp_file_path = temp_dir.join(&temp_filename);
let temp_file_str = temp_file_path.to_string_lossy().to_string();
// 下载视频到本地临时文件
info!("正在下载视频到临时文件: {}", temp_file_str);
match self.download_video_to_file(video_url, &temp_file_str).await {
Ok(_) => {
info!("视频下载成功: {}", temp_file_str);
}
Err(e) => {
error!("视频下载失败: {} - {}", video_url, e);
return Err(anyhow!("视频下载失败: {}", e));
}
}
// 上传到云端服务
info!("正在上传视频到云端: {}", temp_file_str);
let upload_result = match self.cloud_upload_service.upload_file(&temp_file_str, None, None).await {
Ok(result) => result,
Err(e) => {
// 清理临时文件
let _ = tokio::fs::remove_file(&temp_file_path).await;
error!("视频上传失败: {} - {}", temp_file_str, e);
return Err(anyhow!("视频上传失败: {}", e));
}
};
// 清理临时文件
if let Err(e) = tokio::fs::remove_file(&temp_file_path).await {
warn!("清理临时文件失败: {} - {}", temp_file_str, e);
}
if !upload_result.success {
let error_msg = upload_result.error_message.unwrap_or_default();
error!("视频上传失败: {}", error_msg);
return Err(anyhow!("视频上传失败: {}", error_msg));
}
let s3_url = upload_result.remote_url
.ok_or_else(|| anyhow!("上传成功但未返回S3 URL"))?;
// 转换S3 URL为CDN URL
let cdn_url = Self::convert_s3_to_cdn_url(&s3_url);
info!("视频上传成功: {} -> {} -> {}", video_url, s3_url, cdn_url);
Ok(cdn_url)
}
/// 下载视频文件到本地
async fn download_video_to_file(&self, video_url: &str, file_path: &str) -> Result<()> {
// 创建带有防盗链绕过的HTTP客户端
let client = reqwest::Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.timeout(std::time::Duration::from_secs(300)) // 5分钟超时
.build()
.map_err(|e| anyhow!("创建HTTP客户端失败: {}", e))?;
// 构建请求,添加必要的头部信息绕过防盗链
let request = client
.get(video_url)
.header("Referer", "https://www.volcengine.com/")
.header("Origin", "https://www.volcengine.com")
.header("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5")
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
.header("Cache-Control", "no-cache")
.header("Pragma", "no-cache")
.header("Sec-Fetch-Dest", "video")
.header("Sec-Fetch-Mode", "no-cors")
.header("Sec-Fetch-Site", "cross-site");
let response = request.send().await
.map_err(|e| anyhow!("请求视频失败: {}", e))?;
if !response.status().is_success() {
return Err(anyhow!("下载视频失败HTTP状态码: {}", response.status()));
}
let bytes = response.bytes().await
.map_err(|e| anyhow!("读取视频数据失败: {}", e))?;
tokio::fs::write(file_path, &bytes).await
.map_err(|e| anyhow!("保存视频文件失败: {}", e))?;
info!("视频文件保存成功: {} ({} bytes)", file_path, bytes.len());
Ok(())
}
/// 将S3 URL转换为可访问的CDN地址
fn convert_s3_to_cdn_url(s3_url: &str) -> String {
if s3_url.starts_with("s3://ap-northeast-2/modal-media-cache/") {