service/http_server/api/v0/bucket/
add.rs1use axum::extract::{Multipart, State};
2use axum::response::{IntoResponse, Response};
3use serde::{Deserialize, Serialize};
4use std::io::Cursor;
5use std::path::PathBuf;
6use uuid::Uuid;
7
8use common::prelude::Link;
9
10use crate::mount_ops::{add_data_to_bucket, MountOpsError};
11use crate::ServiceState;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[cfg_attr(feature = "clap", derive(clap::Args))]
15pub struct AddRequest {
16 #[cfg_attr(feature = "clap", arg(long))]
18 pub bucket_id: Uuid,
19
20 #[cfg_attr(feature = "clap", arg(long))]
22 pub mount_path: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AddResponse {
27 pub mount_path: String,
28 pub link: Link,
29 pub mime_type: String,
30}
31
32#[axum::debug_handler]
33pub async fn handler(
34 State(state): State<ServiceState>,
35 mut multipart: Multipart,
36) -> Result<impl IntoResponse, AddError> {
37 let mut bucket_id: Option<Uuid> = None;
38 let mut mount_path: Option<String> = None;
39 let mut file_data: Option<Vec<u8>> = None;
40
41 while let Some(field) = multipart
43 .next_field()
44 .await
45 .map_err(|e| AddError::MultipartError(e.to_string()))?
46 {
47 let field_name = field.name().unwrap_or("").to_string();
48
49 match field_name.as_str() {
50 "bucket_id" => {
51 let text = field
52 .text()
53 .await
54 .map_err(|e| AddError::MultipartError(e.to_string()))?;
55 bucket_id = Some(
56 Uuid::parse_str(&text)
57 .map_err(|_| AddError::InvalidRequest("Invalid bucket_id".into()))?,
58 );
59 }
60 "mount_path" => {
61 mount_path = Some(
62 field
63 .text()
64 .await
65 .map_err(|e| AddError::MultipartError(e.to_string()))?,
66 );
67 }
68 "file" => {
69 file_data = Some(
70 field
71 .bytes()
72 .await
73 .map_err(|e| AddError::MultipartError(e.to_string()))?
74 .to_vec(),
75 );
76 }
77 _ => {}
78 }
79 }
80
81 let bucket_id =
82 bucket_id.ok_or_else(|| AddError::InvalidRequest("bucket_id is required".into()))?;
83 let mount_path =
84 mount_path.ok_or_else(|| AddError::InvalidRequest("mount_path is required".into()))?;
85 let file_data = file_data.ok_or_else(|| AddError::InvalidRequest("file is required".into()))?;
86
87 let mount_path_buf = PathBuf::from(&mount_path);
89 if !mount_path_buf.is_absolute() {
90 return Err(AddError::InvalidPath("Mount path must be absolute".into()));
91 }
92
93 let mime_type = mime_guess::from_path(&mount_path_buf)
95 .first_or_octet_stream()
96 .to_string();
97
98 tracing::info!(
99 "Adding file to bucket {} at {} ({})",
100 bucket_id,
101 mount_path,
102 mime_type
103 );
104
105 let mime_type = mime_guess::from_path(&mount_path_buf)
107 .first_or_octet_stream()
108 .to_string();
109 let mount_path_clone = mount_path_buf.clone();
111 let state_clone = state.clone();
112
113 let new_bucket_link = tokio::task::spawn_blocking(move || -> Result<Link, MountOpsError> {
115 let reader = Cursor::new(file_data);
117 tokio::runtime::Handle::current().block_on(async {
118 let bucket_link =
119 add_data_to_bucket(bucket_id, mount_path_clone, reader, &state_clone).await?;
120 Ok(bucket_link)
121 })
122 })
123 .await
124 .map_err(|e| AddError::Default(anyhow::anyhow!("Task join error: {}", e)))??;
125
126 Ok((
127 http::StatusCode::OK,
128 axum::Json(AddResponse {
129 mount_path,
130 link: new_bucket_link,
131 mime_type,
132 }),
133 )
134 .into_response())
135}
136
137#[derive(Debug, thiserror::Error)]
138pub enum AddError {
139 #[error("Default error: {0}")]
140 Default(anyhow::Error),
141 #[error("Bucket not found: {0}")]
142 BucketNotFound(Uuid),
143 #[error("Invalid path: {0}")]
144 InvalidPath(String),
145 #[error("Invalid request: {0}")]
146 InvalidRequest(String),
147 #[error("Multipart error: {0}")]
148 MultipartError(String),
149 #[error("Database error: {0}")]
150 Database(String),
151 #[error("Storage error: {0}")]
152 MountOps(#[from] MountOpsError),
153}
154
155impl IntoResponse for AddError {
156 fn into_response(self) -> Response {
157 match self {
158 AddError::BucketNotFound(id) => (
159 http::StatusCode::NOT_FOUND,
160 format!("Bucket not found: {}", id),
161 )
162 .into_response(),
163 AddError::InvalidPath(msg)
164 | AddError::InvalidRequest(msg)
165 | AddError::MultipartError(msg) => (
166 http::StatusCode::BAD_REQUEST,
167 format!("Bad request: {}", msg),
168 )
169 .into_response(),
170 AddError::Database(_) | AddError::Default(_) | AddError::MountOps(_) => (
171 http::StatusCode::INTERNAL_SERVER_ERROR,
172 "Unexpected error".to_string(),
173 )
174 .into_response(),
175 }
176 }
177}