Skip to main content

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

1use axum::extract::{Json, State};
2use axum::response::{IntoResponse, Response};
3use common::prelude::{Link, 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 MvRequest {
14    /// Bucket ID containing the file/directory to move
15    pub bucket_id: Uuid,
16    /// Current absolute path of the file/directory
17    pub source_path: String,
18    /// New absolute path for the file/directory
19    pub dest_path: String,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct MvResponse {
24    pub source_path: String,
25    pub dest_path: String,
26    pub link: Link,
27}
28
29pub async fn handler(
30    State(state): State<ServiceState>,
31    Json(req): Json<MvRequest>,
32) -> Result<impl IntoResponse, MvError> {
33    tracing::info!(
34        "MV API: Moving {} to {} in bucket {}",
35        req.source_path,
36        req.dest_path,
37        req.bucket_id
38    );
39
40    // Validate paths are absolute
41    let source_path = PathBuf::from(&req.source_path);
42    let dest_path = PathBuf::from(&req.dest_path);
43
44    if !source_path.is_absolute() {
45        return Err(MvError::InvalidPath(format!(
46            "Source path must be absolute: {}",
47            req.source_path
48        )));
49    }
50
51    if !dest_path.is_absolute() {
52        return Err(MvError::InvalidPath(format!(
53            "Destination path must be absolute: {}",
54            req.dest_path
55        )));
56    }
57
58    // Load mount at current head
59    let mut mount = state.peer().mount(req.bucket_id).await?;
60    tracing::info!("MV API: Loaded mount for bucket {}", req.bucket_id);
61
62    // Perform the move operation
63    mount.mv(&source_path, &dest_path).await.map_err(|e| {
64        tracing::error!("MV API: Failed to move: {}", e);
65        MvError::Mount(e)
66    })?;
67
68    tracing::info!("MV API: Moved {} to {}", req.source_path, req.dest_path);
69
70    // Save mount and update log
71    let new_bucket_link = state.peer().save_mount(&mount, false).await?;
72
73    tracing::info!(
74        "MV API: Moved {} to {} in bucket {}, new link: {}",
75        req.source_path,
76        req.dest_path,
77        req.bucket_id,
78        new_bucket_link.hash()
79    );
80
81    Ok((
82        http::StatusCode::OK,
83        Json(MvResponse {
84            source_path: req.source_path,
85            dest_path: req.dest_path,
86            link: new_bucket_link,
87        }),
88    )
89        .into_response())
90}
91
92#[derive(Debug, thiserror::Error)]
93pub enum MvError {
94    #[error("Invalid path: {0}")]
95    InvalidPath(String),
96    #[error("Mount error: {0}")]
97    Mount(#[from] MountError),
98}
99
100impl IntoResponse for MvError {
101    fn into_response(self) -> Response {
102        match self {
103            MvError::InvalidPath(msg) => (
104                http::StatusCode::BAD_REQUEST,
105                format!("Invalid path: {}", msg),
106            )
107                .into_response(),
108            MvError::Mount(MountError::PathNotFound(path)) => (
109                http::StatusCode::NOT_FOUND,
110                format!("Source not found: {}", path.display()),
111            )
112                .into_response(),
113            MvError::Mount(MountError::PathAlreadyExists(path)) => (
114                http::StatusCode::CONFLICT,
115                format!("Destination already exists: {}", path.display()),
116            )
117                .into_response(),
118            MvError::Mount(MountError::MoveIntoSelf { from, to }) => (
119                http::StatusCode::BAD_REQUEST,
120                format!(
121                    "Cannot move '{}' into itself: destination '{}' is inside source",
122                    from.display(),
123                    to.display()
124                ),
125            )
126                .into_response(),
127            MvError::Mount(_) => (
128                http::StatusCode::INTERNAL_SERVER_ERROR,
129                "Unexpected error".to_string(),
130            )
131                .into_response(),
132        }
133    }
134}
135
136impl ApiRequest for MvRequest {
137    type Response = MvResponse;
138
139    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
140        let full_url = base_url.join("/api/v0/bucket/mv").unwrap();
141        client.post(full_url).json(&self)
142    }
143}