jax_daemon/http_server/api/v0/bucket/
mv.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::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 pub bucket_id: Uuid,
16 pub source_path: String,
18 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 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 let mut mount = state.peer().mount(req.bucket_id).await?;
60 tracing::info!("MV API: Loaded mount for bucket {}", req.bucket_id);
61
62 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 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}