service/http_server/api/v0/bucket/
share.rs

1use axum::extract::{Json, State};
2use axum::response::{IntoResponse, Response};
3use common::prelude::Link;
4use reqwest::{Client, RequestBuilder, Url};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use common::crypto::PublicKey;
9
10use crate::http_server::api::client::ApiRequest;
11use crate::mount_ops::MountOpsError;
12use crate::ServiceState;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[cfg_attr(feature = "clap", derive(clap::Args))]
16pub struct ShareRequest {
17    /// Bucket ID to share
18    #[cfg_attr(feature = "clap", arg(long))]
19    pub bucket_id: Uuid,
20
21    /// Public key of the peer to share with (hex-encoded)
22    #[cfg_attr(feature = "clap", arg(long))]
23    pub peer_public_key: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ShareResponse {
28    pub bucket_id: Uuid,
29    pub peer_public_key: String,
30    pub new_bucket_link: String,
31}
32
33#[axum::debug_handler]
34pub async fn handler(
35    State(state): State<ServiceState>,
36    Json(req): Json<ShareRequest>,
37) -> Result<impl IntoResponse, ShareError> {
38    // Parse the peer's public key from hex
39    let peer_public_key = PublicKey::from_hex(&req.peer_public_key)
40        .map_err(|e| ShareError::InvalidPublicKey(e.to_string()))?;
41
42    // Run file operations in blocking task
43    let new_bucket_link = tokio::task::spawn_blocking(move || -> Result<Link, MountOpsError> {
44        tokio::runtime::Handle::current().block_on(async {
45            tracing::info!("Adding file to mount");
46            let bucket_link =
47                crate::mount_ops::share_bucket(req.bucket_id, peer_public_key, &state).await?;
48            Ok(bucket_link)
49        })
50    })
51    .await
52    .map_err(|e| ShareError::Mount(format!("Task join error: {}", e)))??;
53
54    tracing::info!(
55        "Bucket {} shared with peer {}",
56        req.bucket_id,
57        req.peer_public_key
58    );
59
60    Ok((
61        http::StatusCode::OK,
62        Json(ShareResponse {
63            bucket_id: req.bucket_id,
64            peer_public_key: req.peer_public_key,
65            new_bucket_link: new_bucket_link.hash().to_string(),
66        }),
67    )
68        .into_response())
69}
70
71#[derive(Debug, thiserror::Error)]
72pub enum ShareError {
73    #[error("Bucket not found: {0}")]
74    BucketNotFound(Uuid),
75    #[error("Invalid public key: {0}")]
76    InvalidPublicKey(String),
77    #[error("Share not found")]
78    ShareNotFound,
79    #[error("Database error: {0}")]
80    Database(String),
81    #[error("Mount error: {0}")]
82    Mount(String),
83    #[error("Crypto error: {0}")]
84    Crypto(String),
85}
86
87impl From<MountOpsError> for ShareError {
88    fn from(err: MountOpsError) -> Self {
89        match err {
90            MountOpsError::BucketNotFound(id) => ShareError::BucketNotFound(id),
91            MountOpsError::ShareNotFound => ShareError::ShareNotFound,
92            MountOpsError::Database(msg) => ShareError::Database(msg),
93            MountOpsError::Mount(e) => ShareError::Mount(e.to_string()),
94            MountOpsError::CryptoError(msg) => ShareError::Crypto(msg),
95            MountOpsError::ShareError(msg) => ShareError::Crypto(msg),
96            MountOpsError::InvalidPath(msg) => ShareError::Mount(msg),
97        }
98    }
99}
100
101impl IntoResponse for ShareError {
102    fn into_response(self) -> Response {
103        match self {
104            ShareError::BucketNotFound(id) => (
105                http::StatusCode::NOT_FOUND,
106                format!("Bucket not found: {}", id),
107            )
108                .into_response(),
109            ShareError::InvalidPublicKey(msg) => (
110                http::StatusCode::BAD_REQUEST,
111                format!("Invalid public key: {}", msg),
112            )
113                .into_response(),
114            ShareError::ShareNotFound => (
115                http::StatusCode::NOT_FOUND,
116                "Share not found for this bucket".to_string(),
117            )
118                .into_response(),
119            ShareError::Database(_) | ShareError::Mount(_) | ShareError::Crypto(_) => (
120                http::StatusCode::INTERNAL_SERVER_ERROR,
121                "Unexpected error".to_string(),
122            )
123                .into_response(),
124        }
125    }
126}
127
128// Client implementation - builds request for this operation
129impl ApiRequest for ShareRequest {
130    type Response = ShareResponse;
131
132    fn build_request(self, base_url: &Url, client: &Client) -> RequestBuilder {
133        let full_url = base_url.join("/api/v0/bucket/share").unwrap();
134        client.post(full_url).json(&self)
135    }
136}