Skip to main content

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

1use axum::extract::{Json, State};
2use axum::response::{IntoResponse, Response};
3use common::prelude::MountError;
4use reqwest::{Client, RequestBuilder, Url};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7use uuid::Uuid;
8
9use crate::http_server::api::client::ApiRequest;
10use crate::ServiceState;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct DeleteRequest {
14    /// Bucket ID containing the file to delete
15    pub bucket_id: Uuid,
16    /// Absolute path to the file or directory to delete
17    pub path: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DeleteResponse {
22    pub path: String,
23    pub new_bucket_link: String,
24}
25
26pub async fn handler(
27    State(state): State<ServiceState>,
28    Json(req): Json<DeleteRequest>,
29) -> Result<impl IntoResponse, DeleteError> {
30    tracing::info!(
31        "DELETE API: Received delete request for path {} in bucket {}",
32        req.path,
33        req.bucket_id
34    );
35
36    // Validate path is absolute
37    let path = PathBuf::from(&req.path);
38    if !path.is_absolute() {
39        return Err(DeleteError::InvalidPath(format!(
40            "Path must be absolute: {}",
41            req.path
42        )));
43    }
44
45    // Load mount at current head
46    let mut mount = state.peer().mount(req.bucket_id).await?;
47    tracing::info!("DELETE API: Loaded mount for bucket {}", req.bucket_id);
48
49    // Check if path exists before attempting delete
50    if mount.get(&path).await.is_err() {
51        return Err(DeleteError::PathNotFound(req.path.clone()));
52    }
53
54    // Remove the file/directory
55    mount.rm(&path).await.map_err(|e| {
56        tracing::error!("DELETE API: Failed to remove {}: {}", req.path, e);
57        DeleteError::Mount(e)
58    })?;
59
60    tracing::info!("DELETE API: Removed {} from mount", req.path);
61
62    // Save mount and update log
63    let new_bucket_link = state.peer().save_mount(&mount, false).await?;
64
65    tracing::info!(
66        "DELETE API: Deleted {} from bucket {}, new link: {}",
67        req.path,
68        req.bucket_id,
69        new_bucket_link.hash()
70    );
71
72    Ok((
73        http::StatusCode::OK,
74        Json(DeleteResponse {
75            path: req.path,
76            new_bucket_link: new_bucket_link.hash().to_string(),
77        }),
78    )
79        .into_response())
80}
81
82#[derive(Debug, thiserror::Error)]
83pub enum DeleteError {
84    #[error("Invalid path: {0}")]
85    InvalidPath(String),
86    #[error("Path not found: {0}")]
87    PathNotFound(String),
88    #[error("Mount error: {0}")]
89    Mount(#[from] MountError),
90}
91
92impl IntoResponse for DeleteError {
93    fn into_response(self) -> Response {
94        match self {
95            DeleteError::InvalidPath(msg) => (
96                http::StatusCode::BAD_REQUEST,
97                format!("Invalid path: {}", msg),
98            )
99                .into_response(),
100            DeleteError::PathNotFound(msg) => (
101                http::StatusCode::NOT_FOUND,
102                format!("Path not found: {}", msg),
103            )
104                .into_response(),
105            DeleteError::Mount(_) => (
106                http::StatusCode::INTERNAL_SERVER_ERROR,
107                "Unexpected error".to_string(),
108            )
109                .into_response(),
110        }
111    }
112}
113
114impl ApiRequest for DeleteRequest {
115    type Response = DeleteResponse;
116
117    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
118        let full_url = base_url.join("/api/v0/bucket/delete").unwrap();
119        client.post(full_url).json(&self)
120    }
121}