Skip to main content

jax_daemon/http_server/api/v0/bucket/
rename.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::io::Cursor;
7use std::path::PathBuf;
8use uuid::Uuid;
9
10use crate::http_server::api::client::ApiRequest;
11use crate::ServiceState;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct RenameRequest {
15    /// Bucket ID containing the file to rename
16    pub bucket_id: Uuid,
17    /// Current absolute path of the file
18    pub old_path: String,
19    /// New absolute path for the file
20    pub new_path: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct RenameResponse {
25    pub old_path: String,
26    pub new_path: String,
27    pub link: Link,
28}
29
30pub async fn handler(
31    State(state): State<ServiceState>,
32    Json(req): Json<RenameRequest>,
33) -> Result<impl IntoResponse, RenameError> {
34    tracing::info!(
35        "RENAME API: Renaming {} to {} in bucket {}",
36        req.old_path,
37        req.new_path,
38        req.bucket_id
39    );
40
41    // Validate paths are absolute
42    let old_path = PathBuf::from(&req.old_path);
43    let new_path = PathBuf::from(&req.new_path);
44
45    if !old_path.is_absolute() {
46        return Err(RenameError::InvalidPath(format!(
47            "Old path must be absolute: {}",
48            req.old_path
49        )));
50    }
51
52    if !new_path.is_absolute() {
53        return Err(RenameError::InvalidPath(format!(
54            "New path must be absolute: {}",
55            req.new_path
56        )));
57    }
58
59    // Load mount at current head
60    let mut mount = state.peer().mount(req.bucket_id).await?;
61    tracing::info!("RENAME API: Loaded mount for bucket {}", req.bucket_id);
62
63    // Check if source exists
64    let _source_node = mount.get(&old_path).await.map_err(|_| {
65        RenameError::SourceNotFound(format!("Source path not found: {}", req.old_path))
66    })?;
67
68    // Check if destination already exists
69    if mount.get(&new_path).await.is_ok() {
70        return Err(RenameError::DestinationExists(format!(
71            "Destination path already exists: {}",
72            req.new_path
73        )));
74    }
75
76    // Read the file content
77    let file_data = mount.cat(&old_path).await.map_err(|e| {
78        tracing::error!("RENAME API: Failed to read file content: {}", e);
79        RenameError::Mount(e)
80    })?;
81
82    tracing::info!(
83        "RENAME API: Read {} bytes from {}",
84        file_data.len(),
85        req.old_path
86    );
87
88    // Remove from old path
89    mount.rm(&old_path).await.map_err(|e| {
90        tracing::error!("RENAME API: Failed to remove old path: {}", e);
91        RenameError::Mount(e)
92    })?;
93
94    tracing::info!("RENAME API: Removed file from {}", req.old_path);
95
96    // Add to new path
97    let reader = Cursor::new(file_data);
98    mount.add(&new_path, reader).await.map_err(|e| {
99        tracing::error!("RENAME API: Failed to add to new path: {}", e);
100        RenameError::Mount(e)
101    })?;
102
103    tracing::info!("RENAME API: Added file to {}", req.new_path);
104
105    // Save mount and update log
106    let new_bucket_link = state.peer().save_mount(&mount, None).await?;
107
108    tracing::info!(
109        "RENAME API: Renamed {} to {} in bucket {}, new link: {}",
110        req.old_path,
111        req.new_path,
112        req.bucket_id,
113        new_bucket_link.hash()
114    );
115
116    Ok((
117        http::StatusCode::OK,
118        Json(RenameResponse {
119            old_path: req.old_path,
120            new_path: req.new_path,
121            link: new_bucket_link,
122        }),
123    )
124        .into_response())
125}
126
127#[derive(Debug, thiserror::Error)]
128pub enum RenameError {
129    #[error("Invalid path: {0}")]
130    InvalidPath(String),
131    #[error("Source not found: {0}")]
132    SourceNotFound(String),
133    #[error("Destination exists: {0}")]
134    DestinationExists(String),
135    #[error("Mount error: {0}")]
136    Mount(#[from] MountError),
137}
138
139impl IntoResponse for RenameError {
140    fn into_response(self) -> Response {
141        match self {
142            RenameError::InvalidPath(msg) => (
143                http::StatusCode::BAD_REQUEST,
144                format!("Invalid path: {}", msg),
145            )
146                .into_response(),
147            RenameError::SourceNotFound(msg) => (
148                http::StatusCode::NOT_FOUND,
149                format!("Source not found: {}", msg),
150            )
151                .into_response(),
152            RenameError::DestinationExists(msg) => (
153                http::StatusCode::CONFLICT,
154                format!("Destination exists: {}", msg),
155            )
156                .into_response(),
157            RenameError::Mount(_) => (
158                http::StatusCode::INTERNAL_SERVER_ERROR,
159                "Unexpected error".to_string(),
160            )
161                .into_response(),
162        }
163    }
164}
165
166impl ApiRequest for RenameRequest {
167    type Response = RenameResponse;
168
169    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
170        let full_url = base_url.join("/api/v0/bucket/rename").unwrap();
171        client.post(full_url).json(&self)
172    }
173}