Skip to main content

jax_daemon/http_server/api/v0/bucket/
add.rs

1use 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    /// Bucket ID to add file to
15    #[arg(long)]
16    pub bucket_id: Uuid,
17
18    /// Path in bucket where file should be mounted
19    #[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    // Parse multipart form data
50    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                // Get filename from the field
76                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    // Load mount at current head
118    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    // Process each file
129    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        // Construct full path
134        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        // Validate mount path
144        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        // Detect MIME type from file extension
158        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        // Try to add file to mount
165        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}