service/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::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 #[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 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 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
103impl 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}