tdm_server_rust/service/
oss_service.rs1use crate::{
6 app::AppState,
7 cache::on_episode_mutated,
8 entity::oss::{OssCredential, OssDto},
9 error::{ApiResult, AppError},
10 repository::{episode_repo::EpisodeRepository, oss_repo::OssRepository},
11};
12use bytes::Bytes;
13
14pub struct OssService;
16
17impl OssService {
18 #[tracing::instrument(skip_all, level = "debug")]
25 pub async fn upsert_oss(state: &AppState, dto: OssDto, member_id: i32) -> ApiResult<()> {
26 let cfg = (*state.config).clone();
27 OssRepository::new(state.db.clone(), cfg)
28 .upsert_oss(&dto, member_id)
29 .await?;
30 let manga_id = EpisodeRepository::new(state.db.clone())
31 .get_manga_id_by_episode_id(dto.episode_id)
32 .await?
33 .ok_or_else(|| AppError::business("话数不存在喵"))?;
34 on_episode_mutated(state, manga_id, &[member_id]).await;
35 Ok(())
36 }
37
38 #[tracing::instrument(skip_all, level = "debug")]
49 pub async fn get_upload_credential(
50 state: &AppState,
51 episode_id: i32,
52 post_name: String,
53 filename: String,
54 ) -> ApiResult<OssCredential> {
55 let cfg = (*state.config).clone();
56 OssRepository::new(state.db.clone(), cfg)
57 .get_upload_credential(episode_id, &post_name, &filename)
58 .await
59 }
60
61 #[tracing::instrument(skip_all, level = "debug")]
63 pub async fn get_image_upload_credential(
64 state: &AppState,
65 image_type: String,
66 filename: String,
67 ) -> ApiResult<OssCredential> {
68 let cfg = (*state.config).clone();
69 OssRepository::new(state.db.clone(), cfg)
70 .get_image_upload_credential(&image_type, &filename)
71 .await
72 }
73
74 #[tracing::instrument(skip_all, level = "debug")]
84 pub async fn get_download_credential(
85 state: &AppState,
86 episode_id: i32,
87 post_name: String,
88 ) -> ApiResult<OssCredential> {
89 let cfg = (*state.config).clone();
90 OssRepository::new(state.db.clone(), cfg)
91 .get_download_credential(episode_id, &post_name)
92 .await
93 }
94
95 #[tracing::instrument(skip_all, level = "debug")]
105 pub async fn download_redirect_target(
106 state: &AppState,
107 episode_id: i32,
108 post_name: String,
109 ) -> ApiResult<(String, String)> {
110 let cred = Self::get_download_credential(state, episode_id, post_name).await?;
111 let filename = filename_from_credential(&cred);
112 Ok((cred.presigned_url, filename))
113 }
114
115 #[tracing::instrument(skip_all, level = "debug")]
129 pub async fn download_file_proxy(
130 state: &AppState,
131 episode_id: i32,
132 post_name: String,
133 ) -> ApiResult<(String, Bytes)> {
134 let cred = Self::get_download_credential(state, episode_id, post_name.clone()).await?;
135 let filename = filename_from_credential(&cred);
136
137 let resp = crate::telemetry::traced_http_execute(
138 &state.http_client,
139 state.http_client.get(&cred.presigned_url),
140 )
141 .await
142 .map_err(|e| AppError::Internal(format!("拉取 OSS 文件失败: {e}")))?;
143 if !resp.status().is_success() {
144 return Err(AppError::Oss {
145 code: None,
146 msg: format!("OSS 下载失败: HTTP {}", resp.status()),
147 });
148 }
149 let bytes = resp
150 .bytes()
151 .await
152 .map_err(|e| AppError::Internal(format!("读取 OSS 文件失败: {e}")))?;
153 crate::utils::agent_debug::log(
154 "H1",
155 "oss_service.rs:download_file_proxy",
156 "oss_proxy_ok",
157 serde_json::json!({
158 "episodeId": episode_id,
159 "postName": post_name,
160 "filename": filename,
161 "bytes": bytes.len()
162 }),
163 );
164 Ok((filename, bytes))
165 }
166}
167
168fn filename_from_credential(cred: &OssCredential) -> String {
170 cred.original_filename
171 .as_ref()
172 .filter(|s| !s.trim().is_empty())
173 .cloned()
174 .or_else(|| {
175 cred.object_key
176 .as_ref()
177 .and_then(|k| k.split('/').next_back())
178 .map(|s| s.to_string())
179 })
180 .unwrap_or_else(|| "download.bin".to_string())
181}