jax_daemon/http_server/api/v0/bucket/
rename.rs1use 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 pub bucket_id: Uuid,
17 pub old_path: String,
19 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 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 let mut mount = state.peer().mount(req.bucket_id).await?;
61 tracing::info!("RENAME API: Loaded mount for bucket {}", req.bucket_id);
62
63 let _source_node = mount.get(&old_path).await.map_err(|_| {
65 RenameError::SourceNotFound(format!("Source path not found: {}", req.old_path))
66 })?;
67
68 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 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 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 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 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}