Skip to main content

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

1use 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    /// Name of the bucket to create
17    #[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    // Validate bucket name
39    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    // Get the genesis link from the mount
63    let genesis_link = mount.link().await;
64    tracing::info!(
65        "CREATE BUCKET: Genesis link for bucket {}: {:?}",
66        id,
67        genesis_link
68    );
69
70    // Append genesis entry to log (height 0, no previous link, unpublished)
71    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
142// Client implementation - builds request for this operation
143impl 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}