Skip to main content

jax_daemon/http_server/api/v0/bucket/
unpublish.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 UnpublishRequest {
14    /// Bucket ID to unpublish
15    #[arg(long)]
16    pub bucket_id: Uuid,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct UnpublishResponse {
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<UnpublishRequest>,
29) -> Result<impl IntoResponse, UnpublishError> {
30    tracing::info!("UNPUBLISH API: Unpublishing 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(UnpublishError::NotOwner)?;
43        if *our_share.role() != PrincipalRole::Owner {
44            return Err(UnpublishError::NotOwner);
45        }
46    }
47
48    // Check if already unpublished
49    if !mount.is_published().await {
50        tracing::info!(
51            "UNPUBLISH API: Bucket {} is already unpublished",
52            req.bucket_id
53        );
54        let link = mount.link().await;
55        return Ok((
56            http::StatusCode::OK,
57            Json(UnpublishResponse {
58                bucket_id: req.bucket_id,
59                published: false,
60                new_bucket_link: link.hash().to_string(),
61            }),
62        )
63            .into_response());
64    }
65
66    // Unpublish the bucket (clears publish state + saves + log + notify)
67    let new_bucket_link = state.peer().save_mount(&mount, Some(false)).await?;
68
69    tracing::info!(
70        "UNPUBLISH API: Bucket {} unpublished, new link: {}",
71        req.bucket_id,
72        new_bucket_link.hash()
73    );
74
75    Ok((
76        http::StatusCode::OK,
77        Json(UnpublishResponse {
78            bucket_id: req.bucket_id,
79            published: false,
80            new_bucket_link: new_bucket_link.hash().to_string(),
81        }),
82    )
83        .into_response())
84}
85
86#[derive(Debug, thiserror::Error)]
87pub enum UnpublishError {
88    #[error("Mount error: {0}")]
89    Mount(#[from] MountError),
90    #[error("Only the bucket owner can unpublish")]
91    NotOwner,
92}
93
94impl IntoResponse for UnpublishError {
95    fn into_response(self) -> Response {
96        match self {
97            UnpublishError::Mount(_) => (
98                http::StatusCode::INTERNAL_SERVER_ERROR,
99                "Unexpected error".to_string(),
100            )
101                .into_response(),
102            UnpublishError::NotOwner => (
103                http::StatusCode::FORBIDDEN,
104                "Only the bucket owner can unpublish".to_string(),
105            )
106                .into_response(),
107        }
108    }
109}
110
111impl ApiRequest for UnpublishRequest {
112    type Response = UnpublishResponse;
113
114    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
115        let full_url = base_url.join("/api/v0/bucket/unpublish").unwrap();
116        client.post(full_url).json(&self)
117    }
118}