1use crate::{
7 common::AppJson,
8 app::AppState,
9 common::result::ResultBody,
10 entity::oss::{OssCredential, OssDto},
11 error::{ApiResult, AppError},
12 middleware::AuthMember,
13 service::oss_service::OssService,
14};
15use axum::{
16 body::Body,
17 extract::{Extension, Query, State},
18 http::{header, HeaderValue, StatusCode},
19 response::{IntoResponse, Response},
20 routing::{get, post},
21 Router,
22};
23use crate::utils::query_deserialize::de_i32;
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_id = member.map(|m| m.id).unwrap_or(0);
79 OssService::upsert_oss(&state, body, member_id).await?;
80 Ok(ResultBody::success())
81}
82
83#[tracing::instrument(skip_all, level = "info")]
85pub async fn get_upload_credential(
86 State(state): State<AppState>,
87 Query(q): Query<UploadCredentialQuery>,
88) -> ApiResult<ResultBody<OssCredential>> {
89 let data =
90 OssService::get_upload_credential(&state, q.episode_id, q.post_name, q.filename).await?;
91 Ok(ResultBody::success_data(data))
92}
93
94#[tracing::instrument(skip_all, level = "info")]
96pub async fn get_image_upload_credential(
97 State(state): State<AppState>,
98 Query(q): Query<ImageUploadCredentialQuery>,
99) -> ApiResult<ResultBody<OssCredential>> {
100 let data =
101 OssService::get_image_upload_credential(&state, q.image_type, q.filename).await?;
102 Ok(ResultBody::success_data(data))
103}
104
105#[tracing::instrument(skip_all, level = "info")]
107pub async fn get_download_credential(
108 State(state): State<AppState>,
109 Query(q): Query<DownloadCredentialQuery>,
110) -> ApiResult<ResultBody<OssCredential>> {
111 let data = OssService::get_download_credential(&state, q.episode_id, q.post_name).await?;
112 Ok(ResultBody::success_data(data))
113}
114
115#[tracing::instrument(skip_all, level = "info")]
117pub async fn download_file(
118 State(state): State<AppState>,
119 Query(q): Query<DownloadCredentialQuery>,
120) -> ApiResult<Response> {
121 let (presigned_url, filename) =
122 OssService::download_redirect_target(&state, q.episode_id, q.post_name).await?;
123 let location = HeaderValue::from_str(&presigned_url)
124 .map_err(|e| AppError::Internal(format!("生成跳转地址失败: {e}")))?;
125 let encoded = urlencoding::encode(&filename);
126 let download_filename = HeaderValue::from_str(&encoded)
127 .map_err(|e| AppError::Internal(format!("生成下载文件名响应头失败: {e}")))?;
128
129 Ok(Response::builder()
130 .status(StatusCode::FOUND)
131 .header(header::LOCATION, location)
132 .header("X-Download-Filename", download_filename)
133 .header(
134 header::ACCESS_CONTROL_EXPOSE_HEADERS,
135 "Location, X-Download-Filename",
136 )
137 .body(Body::empty())
138 .map_err(|e| AppError::Internal(format!("构建 302 响应失败: {e}")))?
139 .into_response())
140}
141
142#[tracing::instrument(skip_all, level = "info")]
144pub async fn download_file_proxy(
145 State(state): State<AppState>,
146 Query(q): Query<DownloadCredentialQuery>,
147) -> ApiResult<Response> {
148 let (filename, bytes) =
149 OssService::download_file_proxy(&state, q.episode_id, q.post_name).await?;
150 let encoded = urlencoding::encode(&filename);
151 let mut headers = axum::http::HeaderMap::new();
152 headers.insert(
153 header::CONTENT_TYPE,
154 HeaderValue::from_static("application/octet-stream"),
155 );
156 headers.insert(
157 header::CONTENT_DISPOSITION,
158 HeaderValue::from_str(&format!(
159 "attachment; filename=\"{encoded}\"; filename*=UTF-8''{encoded}"
160 ))
161 .map_err(|e| AppError::Internal(format!("生成下载文件名响应头失败: {e}")))?,
162 );
163 headers.insert(
164 header::ACCESS_CONTROL_EXPOSE_HEADERS,
165 HeaderValue::from_static("Content-Disposition,Content-Type,Content-Length"),
166 );
167 Ok((StatusCode::OK, headers, bytes).into_response())
168}