service/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::prelude::{Mount, MountError};
9
10use crate::http_server::api::client::ApiRequest;
11use crate::ServiceState;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[cfg_attr(feature = "clap", derive(clap::Args))]
15pub struct CreateRequest {
16    /// Name of the bucket to create
17    #[cfg_attr(feature = "clap", 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    use crate::database::models::Bucket as BucketModel;
34
35    // Validate bucket name
36    if req.name.is_empty() {
37        return Err(CreateError::InvalidName("Name cannot be empty".into()));
38    }
39
40    let id = Uuid::new_v4();
41    let owner = state.node().secret();
42    let blobs = state.node().blobs();
43    let mount = Mount::init(id, req.name.clone(), owner, blobs).await?;
44    let link = mount.link();
45
46    // Create bucket in database
47    let _bucket = BucketModel::create(id, req.name.clone(), link.clone(), state.database())
48        .await
49        .map_err(|e| match e {
50            crate::database::models::bucket::BucketError::AlreadyExists(name) => {
51                CreateError::AlreadyExists(name)
52            }
53            crate::database::models::bucket::BucketError::Database(e) => {
54                CreateError::Database(e.to_string())
55            }
56        })?;
57
58    Ok((
59        http::StatusCode::CREATED,
60        Json(CreateResponse {
61            bucket_id: _bucket.id,
62            name: _bucket.name,
63            created_at: _bucket.created_at,
64        }),
65    )
66        .into_response())
67}
68
69#[derive(Debug, thiserror::Error)]
70pub enum CreateError {
71    #[error("Bucket already exists: {0}")]
72    AlreadyExists(String),
73    #[error("Invalid bucket name: {0}")]
74    InvalidName(String),
75    #[error("Database error: {0}")]
76    Database(String),
77    #[error("Mount error: {0}")]
78    Mount(#[from] MountError),
79}
80
81impl IntoResponse for CreateError {
82    fn into_response(self) -> Response {
83        match self {
84            CreateError::AlreadyExists(name) => (
85                http::StatusCode::CONFLICT,
86                format!("Bucket already exists: {}", name),
87            )
88                .into_response(),
89            CreateError::InvalidName(msg) => (
90                http::StatusCode::BAD_REQUEST,
91                format!("Invalid name: {}", msg),
92            )
93                .into_response(),
94            CreateError::Database(_) | CreateError::Mount(_) => (
95                http::StatusCode::INTERNAL_SERVER_ERROR,
96                "Unexpected error".to_string(),
97            )
98                .into_response(),
99        }
100    }
101}
102
103// Client implementation - builds request for this operation
104impl ApiRequest for CreateRequest {
105    type Response = CreateResponse;
106
107    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
108        let full_url = base_url.join("/api/v0/bucket").unwrap();
109        client.post(full_url).json(&self)
110    }
111}