Skip to main content

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

1use axum::extract::{Json, State};
2use axum::response::{IntoResponse, Response};
3use common::mount::PrincipalRole;
4use common::prelude::MountError;
5use reqwest::{Client, RequestBuilder, Url};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::http_server::api::client::ApiRequest;
10use crate::ServiceState;
11
12#[derive(Debug, Clone, Serialize, Deserialize, clap::Args)]
13pub struct PublishRequest {
14    /// Bucket ID to publish
15    #[arg(long)]
16    pub bucket_id: Uuid,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct PublishResponse {
21    pub bucket_id: Uuid,
22    pub published: bool,
23    pub new_bucket_link: String,
24}
25
26pub async fn handler(
27    State(state): State<ServiceState>,
28    Json(req): Json<PublishRequest>,
29) -> Result<impl IntoResponse, PublishError> {
30    tracing::info!("PUBLISH API: Publishing bucket {}", req.bucket_id);
31
32    // Load mount at current head
33    let mount = state.peer().mount(req.bucket_id).await?;
34
35    // Check that the caller is the bucket owner
36    let our_key = state.peer().secret().public();
37    {
38        let manifest = mount.inner().await;
39        let our_share = manifest
40            .manifest()
41            .get_share(&our_key)
42            .ok_or(PublishError::NotOwner)?;
43        if *our_share.role() != PrincipalRole::Owner {
44            return Err(PublishError::NotOwner);
45        }
46    }
47
48    // Check if already published
49    if mount.is_published().await {
50        tracing::info!("PUBLISH API: Bucket {} is already published", req.bucket_id);
51        // Still return success, just note it's already published
52    }
53
54    // Publish: save with public secret, append to log, notify peers
55    let new_bucket_link = state.peer().save_mount(&mount, Some(true)).await?;
56
57    tracing::info!(
58        "PUBLISH API: Bucket {} published, new link: {}",
59        req.bucket_id,
60        new_bucket_link.hash()
61    );
62
63    Ok((
64        http::StatusCode::OK,
65        Json(PublishResponse {
66            bucket_id: req.bucket_id,
67            published: true,
68            new_bucket_link: new_bucket_link.hash().to_string(),
69        }),
70    )
71        .into_response())
72}
73
74#[derive(Debug, thiserror::Error)]
75pub enum PublishError {
76    #[error("Mount error: {0}")]
77    Mount(#[from] MountError),
78    #[error("Only the bucket owner can publish")]
79    NotOwner,
80}
81
82impl IntoResponse for PublishError {
83    fn into_response(self) -> Response {
84        match self {
85            PublishError::Mount(_) => (
86                http::StatusCode::INTERNAL_SERVER_ERROR,
87                "Unexpected error".to_string(),
88            )
89                .into_response(),
90            PublishError::NotOwner => (
91                http::StatusCode::FORBIDDEN,
92                "Only the bucket owner can publish".to_string(),
93            )
94                .into_response(),
95        }
96    }
97}
98
99// Client implementation - builds request for this operation
100impl ApiRequest for PublishRequest {
101    type Response = PublishResponse;
102
103    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
104        let full_url = base_url.join("/api/v0/bucket/publish").unwrap();
105        client.post(full_url).json(&self)
106    }
107}