tdm_server_rust/web/
oss_controller.rs1use crate::utils::query_deserialize::de_i32;
7use crate::{
8 app::AppState,
9 common::result::ResultBody,
10 common::AppJson,
11 entity::oss::{OssCredential, OssDto},
12 error::{ApiResult, AppError},
13 middleware::{member_can_upload_for_post, AuthMember},
14 service::oss_service::OssService,
15};
16use axum::{
17 body::Body,
18 extract::{Extension, Query, State},
19 http::{header, HeaderValue, StatusCode},
20 response::{IntoResponse, Response},
21 routing::{get, post},
22 Router,
23};
24use serde::Deserialize;
25
26#[derive(Debug, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct UploadCredentialQuery {
30 #[serde(deserialize_with = "de_i32")]
32 pub episode_id: i32,
33 pub post_name: String,
35 pub filename: String,
37}
38
39#[derive(Debug, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct ImageUploadCredentialQuery {
43 pub image_type: String,
45 pub filename: String,
47}
48
49#[derive(Debug, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct DownloadCredentialQuery {
53 #[serde(deserialize_with = "de_i32")]
55 pub episode_id: i32,
56 pub post_name: String,
58}
59
60pub fn routes() -> Router<AppState> {
62 Router::new()
63 .route("/", post(upsert))
64 .route("/uploadCredential", get(get_upload_credential))
65 .route("/imageUploadCredential", get(get_image_upload_credential))
66 .route("/downloadCredential", get(get_download_credential))
67 .route("/downloadFile", get(download_file))
68 .route("/downloadFile/proxy", get(download_file_proxy))
69}
70
71#[tracing::instrument(skip_all, level = "info")]
73pub async fn upsert(
74 State(state): State<AppState>,
75 Extension(AuthMember(member)): Extension<AuthMember>,
76 AppJson(body): AppJson<OssDto>,
77) -> ApiResult<ResultBody<()>> {
78 let member = member.ok_or_else(|| AppError::login_expired("请登录一下哦喵……"))?;
79 if !member_can_upload_for_post(&member, &body.post_name) {
80 return Err(AppError::download_unauth("没有该岗位的上传权限喵"));
81 }
82 let member_id = member.id;
83 OssService::upsert_oss(&state, body, member_id).await?;
84 Ok(ResultBody::success())
85}
86
87#[tracing::instrument(skip_all, level = "info")]
89pub async fn get_upload_credential(
90 State(state): State<AppState>,
91 Extension(AuthMember(member)): Extension<AuthMember>,
92 Query(q): Query<UploadCredentialQuery>,
93) -> ApiResult<ResultBody<OssCredential>> {
94 let member = member.ok_or_else(|| AppError::login_expired("请登录一下哦喵……"))?;
95 if !member_can_upload_for_post(&member, &q.post_name) {
96 return Err(AppError::download_unauth("没有该岗位的上传权限喵"));
97 }
98 let data =
99 OssService::get_upload_credential(&state, q.episode_id, q.post_name, q.filename).await?;
100 Ok(ResultBody::success_data(data))
101}
102
103#[tracing::instrument(skip_all, level = "info")]
105pub async fn get_image_upload_credential(
106 State(state): State<AppState>,
107 Query(q): Query<ImageUploadCredentialQuery>,
108) -> ApiResult<ResultBody<OssCredential>> {
109 let data = OssService::get_image_upload_credential(&state, q.image_type, q.filename).await?;
110 Ok(ResultBody::success_data(data))
111}
112
113#[tracing::instrument(skip_all, level = "info")]
115pub async fn get_download_credential(
116 State(state): State<AppState>,
117 Query(q): Query<DownloadCredentialQuery>,
118) -> ApiResult<ResultBody<OssCredential>> {
119 let data = OssService::get_download_credential(&state, q.episode_id, q.post_name).await?;
120 Ok(ResultBody::success_data(data))
121}
122
123#[tracing::instrument(skip_all, level = "info")]
125pub async fn download_file(
126 State(state): State<AppState>,
127 Query(q): Query<DownloadCredentialQuery>,
128) -> ApiResult<Response> {
129 let (presigned_url, filename) =
130 OssService::download_redirect_target(&state, q.episode_id, q.post_name).await?;
131 let location = HeaderValue::from_str(&presigned_url)
132 .map_err(|e| AppError::Internal(format!("生成跳转地址失败: {e}")))?;
133 let encoded = urlencoding::encode(&filename);
134 let download_filename = HeaderValue::from_str(&encoded)
135 .map_err(|e| AppError::Internal(format!("生成下载文件名响应头失败: {e}")))?;
136
137 Ok(Response::builder()
138 .status(StatusCode::FOUND)
139 .header(header::LOCATION, location)
140 .header("X-Download-Filename", download_filename)
141 .header(
142 header::ACCESS_CONTROL_EXPOSE_HEADERS,
143 "Location, X-Download-Filename",
144 )
145 .body(Body::empty())
146 .map_err(|e| AppError::Internal(format!("构建 302 响应失败: {e}")))?
147 .into_response())
148}
149
150#[tracing::instrument(skip_all, level = "info")]
152pub async fn download_file_proxy(
153 State(state): State<AppState>,
154 Query(q): Query<DownloadCredentialQuery>,
155) -> ApiResult<Response> {
156 let (presigned_url, filename) =
157 OssService::download_redirect_target(&state, q.episode_id, q.post_name).await?;
158 let location = HeaderValue::from_str(&presigned_url)
159 .map_err(|e| AppError::Internal(format!("生成跳转地址失败: {e}")))?;
160 let encoded = urlencoding::encode(&filename);
161 let download_filename = HeaderValue::from_str(&encoded)
162 .map_err(|e| AppError::Internal(format!("生成下载文件名响应头失败: {e}")))?;
163
164 Ok(Response::builder()
165 .status(StatusCode::TEMPORARY_REDIRECT)
166 .header(header::LOCATION, location)
167 .header("X-Download-Filename", download_filename)
168 .header(
169 header::ACCESS_CONTROL_EXPOSE_HEADERS,
170 "Location, X-Download-Filename",
171 )
172 .body(Body::empty())
173 .map_err(|e| AppError::Internal(format!("构建 307 响应失败: {e}")))?
174 .into_response())
175}