Skip to main content

jax_daemon/http_server/api/v0/bucket/
update.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)]
13pub struct UpdateResponse {
14    pub mount_path: String,
15    pub link: Link,
16    pub mime_type: String,
17}
18
19pub async fn handler(
20    State(state): State<ServiceState>,
21    mut multipart: Multipart,
22) -> Result<impl IntoResponse, UpdateError> {
23    let mut bucket_id: Option<Uuid> = None;
24    let mut mount_path: Option<String> = None;
25    let mut file_data: Option<Vec<u8>> = None;
26
27    // Parse multipart form data
28    while let Some(field) = multipart
29        .next_field()
30        .await
31        .map_err(|e| UpdateError::MultipartError(e.to_string()))?
32    {
33        let field_name = field.name().unwrap_or("").to_string();
34
35        match field_name.as_str() {
36            "bucket_id" => {
37                let text = field
38                    .text()
39                    .await
40                    .map_err(|e| UpdateError::MultipartError(e.to_string()))?;
41                bucket_id = Some(
42                    Uuid::parse_str(&text)
43                        .map_err(|_| UpdateError::InvalidRequest("Invalid bucket_id".into()))?,
44                );
45            }
46            "mount_path" => {
47                mount_path = Some(
48                    field
49                        .text()
50                        .await
51                        .map_err(|e| UpdateError::MultipartError(e.to_string()))?,
52                );
53            }
54            "file" => {
55                file_data = Some(
56                    field
57                        .bytes()
58                        .await
59                        .map_err(|e| UpdateError::MultipartError(e.to_string()))?
60                        .to_vec(),
61                );
62            }
63            _ => {}
64        }
65    }
66
67    let bucket_id =
68        bucket_id.ok_or_else(|| UpdateError::InvalidRequest("bucket_id is required".into()))?;
69    let mount_path =
70        mount_path.ok_or_else(|| UpdateError::InvalidRequest("mount_path is required".into()))?;
71    let file_data =
72        file_data.ok_or_else(|| UpdateError::InvalidRequest("file is required".into()))?;
73
74    // Validate mount path
75    let mount_path_buf = PathBuf::from(&mount_path);
76    if !mount_path_buf.is_absolute() {
77        return Err(UpdateError::InvalidPath(
78            "Mount path must be absolute".into(),
79        ));
80    }
81
82    tracing::info!(
83        "UPDATE API: Updating file in bucket {} at {}",
84        bucket_id,
85        mount_path
86    );
87
88    // Detect MIME type from file extension
89    let mime_type = mime_guess::from_path(&mount_path_buf)
90        .first_or_octet_stream()
91        .to_string();
92
93    // Load mount at current head
94    let mut mount = state.peer().mount(bucket_id).await?;
95
96    // Check if file exists (optional, but good for logging)
97    let file_exists = mount.get(&mount_path_buf).await.is_ok();
98    if file_exists {
99        tracing::info!("UPDATE API: Removing existing file at {}", mount_path);
100        // Remove the existing file
101        mount.rm(&mount_path_buf).await.map_err(|e| {
102            tracing::error!("UPDATE API: Failed to remove existing file: {}", e);
103            UpdateError::Mount(e)
104        })?;
105    } else {
106        tracing::info!("UPDATE API: File doesn't exist, will create new");
107    }
108
109    // Add the new file content
110    let reader = Cursor::new(file_data);
111    mount.add(&mount_path_buf, reader).await?;
112
113    tracing::info!("UPDATE API: Added new content to {}", mount_path);
114
115    // Save mount and update log
116    let new_bucket_link = state.peer().save_mount(&mount, None).await?;
117
118    tracing::info!(
119        "UPDATE API: Updated {} in bucket {}, new link: {}",
120        mount_path,
121        bucket_id,
122        new_bucket_link.hash()
123    );
124
125    Ok((
126        http::StatusCode::OK,
127        axum::Json(UpdateResponse {
128            mount_path,
129            link: new_bucket_link,
130            mime_type,
131        }),
132    )
133        .into_response())
134}
135
136#[derive(Debug, thiserror::Error)]
137pub enum UpdateError {
138    #[error("Invalid path: {0}")]
139    InvalidPath(String),
140    #[error("Invalid request: {0}")]
141    InvalidRequest(String),
142    #[error("Multipart error: {0}")]
143    MultipartError(String),
144    #[error("Mount error: {0}")]
145    Mount(#[from] MountError),
146}
147
148impl IntoResponse for UpdateError {
149    fn into_response(self) -> Response {
150        match self {
151            UpdateError::InvalidPath(msg)
152            | UpdateError::InvalidRequest(msg)
153            | UpdateError::MultipartError(msg) => (
154                http::StatusCode::BAD_REQUEST,
155                format!("Bad request: {}", msg),
156            )
157                .into_response(),
158            UpdateError::Mount(_) => (
159                http::StatusCode::INTERNAL_SERVER_ERROR,
160                "Unexpected error".to_string(),
161            )
162                .into_response(),
163        }
164    }
165}