jax_daemon/http_server/api/v0/bucket/
create.rs1use axum::extract::{Json, State};
2use axum::response::{IntoResponse, Response};
3use reqwest::{Client, RequestBuilder, Url};
4use serde::{Deserialize, Serialize};
5use time::OffsetDateTime;
6use uuid::Uuid;
7
8use common::bucket_log::BucketLogProvider;
9use common::prelude::{Mount, MountError};
10
11use crate::http_server::api::client::ApiRequest;
12use crate::ServiceState;
13
14#[derive(Debug, Clone, Serialize, Deserialize, clap::Args)]
15pub struct CreateRequest {
16 #[arg(long)]
18 pub name: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CreateResponse {
23 pub bucket_id: Uuid,
24 pub name: String,
25 #[serde(with = "time::serde::rfc3339")]
26 pub created_at: OffsetDateTime,
27}
28
29pub async fn handler(
30 State(state): State<ServiceState>,
31 Json(req): Json<CreateRequest>,
32) -> Result<impl IntoResponse, CreateError> {
33 tracing::info!(
34 "CREATE BUCKET: Received request to create bucket '{}'",
35 req.name
36 );
37
38 if req.name.is_empty() {
40 tracing::warn!("CREATE BUCKET: Invalid empty bucket name");
41 return Err(CreateError::InvalidName("Name cannot be empty".into()));
42 }
43
44 let id = Uuid::new_v4();
45 tracing::info!("CREATE BUCKET: Generated bucket ID: {}", id);
46
47 let owner = state.node().secret();
48 let blobs = state.node().blobs();
49
50 tracing::info!("CREATE BUCKET: Initializing mount for bucket {}", id);
51 let mount = Mount::init(id, req.name.clone(), owner, blobs)
52 .await
53 .map_err(|e| {
54 tracing::error!("CREATE BUCKET: Failed to initialize mount: {}", e);
55 e
56 })?;
57 tracing::info!(
58 "CREATE BUCKET: Mount initialized successfully for bucket {}",
59 id
60 );
61
62 let genesis_link = mount.link().await;
64 tracing::info!(
65 "CREATE BUCKET: Genesis link for bucket {}: {:?}",
66 id,
67 genesis_link
68 );
69
70 tracing::info!(
72 "CREATE BUCKET: Appending genesis entry to log for bucket {}",
73 id
74 );
75 state
76 .peer()
77 .logs()
78 .append(id, req.name.clone(), genesis_link.clone(), None, 0, false)
79 .await
80 .map_err(|e| {
81 tracing::error!(
82 "CREATE BUCKET: Failed to append genesis to log for bucket {}: {}",
83 id,
84 e
85 );
86 CreateError::SaveMount(format!("Failed to append genesis: {}", e))
87 })?;
88 tracing::info!(
89 "CREATE BUCKET: Genesis entry appended successfully for bucket {}",
90 id
91 );
92
93 tracing::info!(
94 "CREATE BUCKET: Bucket '{}' created successfully with ID {}",
95 req.name,
96 id
97 );
98 Ok((
99 http::StatusCode::CREATED,
100 Json(CreateResponse {
101 bucket_id: id,
102 name: req.name,
103 created_at: OffsetDateTime::now_utc(),
104 }),
105 )
106 .into_response())
107}
108
109#[derive(Debug, thiserror::Error)]
110pub enum CreateError {
111 #[error("Invalid bucket name: {0}")]
112 InvalidName(String),
113 #[error("Failed to save mount: {0}")]
114 SaveMount(String),
115 #[error("Mount error: {0}")]
116 Mount(#[from] MountError),
117}
118
119impl IntoResponse for CreateError {
120 fn into_response(self) -> Response {
121 tracing::error!("CREATE BUCKET ERROR: {:?}", self);
122 match self {
123 CreateError::InvalidName(msg) => (
124 http::StatusCode::BAD_REQUEST,
125 format!("Invalid name: {}", msg),
126 )
127 .into_response(),
128 CreateError::SaveMount(msg) => (
129 http::StatusCode::INTERNAL_SERVER_ERROR,
130 format!("Failed to save mount: {}", msg),
131 )
132 .into_response(),
133 CreateError::Mount(e) => (
134 http::StatusCode::INTERNAL_SERVER_ERROR,
135 format!("Mount error: {}", e),
136 )
137 .into_response(),
138 }
139 }
140}
141
142impl ApiRequest for CreateRequest {
144 type Response = CreateResponse;
145
146 fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
147 let full_url = base_url.join("/api/v0/bucket").unwrap();
148 client.post(full_url).json(&self)
149 }
150}