jax_daemon/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, MountError};
9
10use crate::ServiceState;
11
12#[derive(Debug, Clone, Serialize, Deserialize, clap::Args)]
13pub struct AddRequest {
14 #[arg(long)]
16 pub bucket_id: Uuid,
17
18 #[arg(long)]
20 pub mount_path: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct FileUploadResult {
25 pub mount_path: String,
26 pub mime_type: String,
27 pub size: usize,
28 pub success: bool,
29 pub error: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AddResponse {
34 pub bucket_link: Link,
35 pub files: Vec<FileUploadResult>,
36 pub total_files: usize,
37 pub successful_files: usize,
38 pub failed_files: usize,
39}
40
41pub async fn handler(
42 State(state): State<ServiceState>,
43 mut multipart: Multipart,
44) -> Result<impl IntoResponse, AddError> {
45 let mut bucket_id: Option<Uuid> = None;
46 let mut base_path: Option<String> = None;
47 let mut files: Vec<(String, Vec<u8>)> = Vec::new();
48
49 while let Some(field) = multipart.next_field().await.map_err(|e| {
51 tracing::error!("Multipart parsing error: {}", e);
52 AddError::MultipartError(e.to_string())
53 })? {
54 let field_name = field.name().unwrap_or("").to_string();
55
56 match field_name.as_str() {
57 "bucket_id" => {
58 let text = field.text().await.map_err(|e| {
59 tracing::error!("Error reading bucket_id field: {}", e);
60 AddError::MultipartError(e.to_string())
61 })?;
62 bucket_id = Some(Uuid::parse_str(&text).map_err(|e| {
63 tracing::error!("Invalid bucket_id format: {}", e);
64 AddError::InvalidRequest("Invalid bucket_id".into())
65 })?);
66 tracing::info!("Parsed bucket_id: {}", bucket_id.unwrap());
67 }
68 "mount_path" => {
69 base_path = Some(field.text().await.map_err(|e| {
70 tracing::error!("Error reading mount_path field: {}", e);
71 AddError::MultipartError(e.to_string())
72 })?);
73 }
74 "file" | "files" => {
75 let filename = field
77 .file_name()
78 .map(|s| s.to_string())
79 .unwrap_or_else(|| "unnamed".to_string());
80
81 tracing::info!("Reading file: {}", filename);
82 let file_data = field
83 .bytes()
84 .await
85 .map_err(|e| {
86 tracing::error!("Error reading file data for {}: {}", filename, e);
87 AddError::MultipartError(e.to_string())
88 })?
89 .to_vec();
90
91 files.push((filename, file_data));
92 }
93 _ => {
94 tracing::warn!("Ignoring unknown field: {}", field_name);
95 }
96 }
97 }
98
99 let bucket_id =
100 bucket_id.ok_or_else(|| AddError::InvalidRequest("bucket_id is required".into()))?;
101 let base_path =
102 base_path.ok_or_else(|| AddError::InvalidRequest("mount_path is required".into()))?;
103
104 if files.is_empty() {
105 return Err(AddError::InvalidRequest(
106 "At least one file is required".into(),
107 ));
108 }
109
110 tracing::info!(
111 "Uploading {} file(s) to bucket {} at path {}",
112 files.len(),
113 bucket_id,
114 base_path
115 );
116
117 tracing::info!("Loading mount for bucket {}", bucket_id);
119 let mut mount = state.peer().mount(bucket_id).await.map_err(|e| {
120 tracing::error!("Failed to load mount for bucket {}: {}", bucket_id, e);
121 e
122 })?;
123
124 let mut results = Vec::new();
125 let mut successful = 0;
126 let mut failed = 0;
127
128 tracing::info!("Processing {} files", files.len());
130 for (idx, (filename, file_data)) in files.iter().enumerate() {
131 tracing::info!("Processing file {}/{}: {}", idx + 1, files.len(), filename);
132
133 let full_path = if base_path == "/" {
135 format!("/{}", filename)
136 } else {
137 format!("{}/{}", base_path.trim_end_matches('/'), filename)
138 };
139 tracing::info!("Full path: {}", full_path);
140
141 let mount_path_buf = PathBuf::from(&full_path);
142
143 if !mount_path_buf.is_absolute() {
145 tracing::warn!("Path is not absolute: {}", full_path);
146 results.push(FileUploadResult {
147 mount_path: full_path.clone(),
148 mime_type: String::new(),
149 size: file_data.len(),
150 success: false,
151 error: Some("Mount path must be absolute".to_string()),
152 });
153 failed += 1;
154 continue;
155 }
156
157 let mime_type = mime_guess::from_path(&mount_path_buf)
159 .first_or_octet_stream()
160 .to_string();
161
162 let file_size = file_data.len();
163
164 match mount
166 .add(&mount_path_buf, Cursor::new(file_data.clone()))
167 .await
168 {
169 Ok(_) => {
170 tracing::info!(
171 "✓ Added file {} ({} bytes, {})",
172 full_path,
173 file_size,
174 mime_type
175 );
176 results.push(FileUploadResult {
177 mount_path: full_path,
178 mime_type,
179 size: file_size,
180 success: true,
181 error: None,
182 });
183 successful += 1;
184 }
185 Err(e) => {
186 tracing::error!("✗ Failed to add file {}: {}", full_path, e);
187 results.push(FileUploadResult {
188 mount_path: full_path,
189 mime_type,
190 size: file_size,
191 success: false,
192 error: Some(e.to_string()),
193 });
194 failed += 1;
195 }
196 }
197 }
198
199 let bucket_link = if successful > 0 {
200 tracing::info!("Saving mount (at least one file succeeded)");
201 state.peer().save_mount(&mount, false).await.map_err(|e| {
202 tracing::error!("Failed to save mount: {}", e);
203 tracing::error!("Error details: {:?}", e);
204 e
205 })?
206 } else {
207 tracing::error!("All files failed to upload");
208 return Err(AddError::InvalidRequest(
209 "All files failed to upload".into(),
210 ));
211 };
212
213 tracing::info!("Bucket link: {}", bucket_link);
214
215 Ok((
216 http::StatusCode::OK,
217 axum::Json(AddResponse {
218 bucket_link,
219 files: results,
220 total_files: successful + failed,
221 successful_files: successful,
222 failed_files: failed,
223 }),
224 )
225 .into_response())
226}
227
228#[derive(Debug, thiserror::Error)]
229pub enum AddError {
230 #[error("Invalid request: {0}")]
231 InvalidRequest(String),
232 #[error("Multipart error: {0}")]
233 MultipartError(String),
234 #[error("Mount error: {0}")]
235 Mount(#[from] MountError),
236}
237
238impl IntoResponse for AddError {
239 fn into_response(self) -> Response {
240 match self {
241 AddError::InvalidRequest(msg) | AddError::MultipartError(msg) => (
242 http::StatusCode::BAD_REQUEST,
243 format!("Bad request: {}", msg),
244 )
245 .into_response(),
246 AddError::Mount(_) => (
247 http::StatusCode::INTERNAL_SERVER_ERROR,
248 "Unexpected error".to_string(),
249 )
250 .into_response(),
251 }
252 }
253}