Skip to main content

boatramp_node/
blobs.rs

1//! Blob (object-store) backend construction: build the configured object store
2//! (fs/S3/GCS/Azure, with optional blob-change notification provisioning) from a
3//! resolved [`BlobArgs`]. Each cloud backend is feature-gated; a disabled one
4//! returns an explanatory error rather than a misleading no-op. Moved out of the
5//! binary (node-library N2b.2c); the binary populates `BlobArgs` from its CLI
6//! `ServeArgs`.
7
8use std::path::Path;
9use std::sync::Arc;
10
11use boatramp_core::Storage;
12
13use crate::backends::BlobBackend;
14use crate::error::{Error, Result};
15
16#[cfg(feature = "fs")]
17use boatramp_storage::FsStorage;
18
19/// The resolved blob-backend selection — the binary populates this from its CLI
20/// `ServeArgs` (the credential/endpoint flags), keeping clap out of the library.
21#[derive(Debug, Clone)]
22pub struct BlobArgs {
23    pub blobs: BlobBackend,
24    pub s3_bucket: Option<String>,
25    pub s3_endpoint: Option<String>,
26    pub s3_region: Option<String>,
27    pub s3_path_style: bool,
28    pub gcs_bucket: Option<String>,
29    pub gcs_endpoint: Option<String>,
30    pub gcs_anonymous: bool,
31    pub azure_account: Option<String>,
32    pub azure_container: Option<String>,
33    pub azure_access_key: Option<String>,
34    pub azure_emulator: bool,
35}
36
37/// The blob backend plus, on a cloud object store with notification provisioning
38/// configured, its blob-change [`WatchProvider`](boatramp_core::blob_provision::WatchProvider)
39/// and operator tier (FA-5b2). The provider/tier are consumed only by the handler
40/// runtime, so they are dead code in a `--no-default-features` (no `handlers`) build.
41pub struct BuiltBlobs {
42    pub storage: Arc<dyn Storage>,
43    #[cfg_attr(not(feature = "handlers"), allow(dead_code))]
44    pub watch_provider: Option<Arc<dyn boatramp_core::blob_provision::WatchProvider>>,
45    #[cfg_attr(not(feature = "handlers"), allow(dead_code))]
46    pub provision_tier: boatramp_core::blob_notify::ProvisionTier,
47}
48
49/// Build the object store for the selected [`BlobBackend`]. `data_dir` is used
50/// only by the `fs` backend (unused when `fs` is off).
51#[cfg_attr(not(feature = "fs"), allow(unused_variables))]
52pub async fn build_blobs(
53    args: &BlobArgs,
54    data_dir: &Path,
55    notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
56    notify_account: Option<String>,
57) -> Result<BuiltBlobs> {
58    match args.blobs {
59        #[cfg(feature = "fs")]
60        BlobBackend::Fs => Ok(BuiltBlobs {
61            storage: Arc::new(FsStorage::new(data_dir.join("blobs"))),
62            watch_provider: None,
63            provision_tier: boatramp_core::blob_notify::ProvisionTier::default(),
64        }),
65        #[cfg(not(feature = "fs"))]
66        BlobBackend::Fs => Err(Error::NoFsSupport),
67        BlobBackend::S3 => build_s3(args, notify_tier, notify_account).await,
68        BlobBackend::Gcs => build_gcs(args, notify_tier, notify_account).await,
69        BlobBackend::Azure => build_azure(args, notify_tier, notify_account).await,
70    }
71}
72
73// Azure storage + optional blob-change notification (Event Grid → Storage Queue,
74// FA-5b2). When a notify tier is configured the backend is consumer-wired and
75// paired with the AzureWatchProvider (the Event Grid subscription is an operator
76// step — see the provider recipe).
77#[cfg(feature = "azure")]
78async fn build_azure(
79    args: &BlobArgs,
80    notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
81    _notify_account: Option<String>,
82) -> Result<BuiltBlobs> {
83    let (Some(account), Some(container)) =
84        (args.azure_account.clone(), args.azure_container.clone())
85    else {
86        return Err(Error::AzureConfigRequired);
87    };
88    let opts = boatramp_storage::AzureOptions {
89        account,
90        container,
91        access_key: args.azure_access_key.clone(),
92        emulator: args.azure_emulator,
93    };
94    match notify_tier {
95        Some(tier) => {
96            let (storage, provider) = boatramp_storage::AzureStorage::connect_with_notify(opts)
97                .map_err(|err| Error::AzureConnect(err.to_string()))?;
98            Ok(BuiltBlobs {
99                storage: Arc::new(storage),
100                watch_provider: Some(Arc::new(provider)),
101                provision_tier: tier,
102            })
103        }
104        None => {
105            let storage = boatramp_storage::AzureStorage::connect(opts)
106                .map_err(|err| Error::AzureConnect(err.to_string()))?;
107            Ok(BuiltBlobs {
108                storage: Arc::new(storage),
109                watch_provider: None,
110                provision_tier: boatramp_core::blob_notify::ProvisionTier::default(),
111            })
112        }
113    }
114}
115
116#[cfg(not(feature = "azure"))]
117async fn build_azure(
118    _args: &BlobArgs,
119    _notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
120    _notify_account: Option<String>,
121) -> Result<BuiltBlobs> {
122    Err(Error::NoAzureSupport)
123}
124
125// GCS storage + optional blob-change notification (GCS→Pub/Sub, FA-5b2). When a
126// notify tier is configured the backend is consumer-wired and paired with the
127// GcsWatchProvider; `blob_notify_account_id` is read as the GCP project id.
128#[cfg(feature = "gcs")]
129async fn build_gcs(
130    args: &BlobArgs,
131    notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
132    notify_account: Option<String>,
133) -> Result<BuiltBlobs> {
134    let bucket = args.gcs_bucket.clone().ok_or(Error::GcsBucketRequired)?;
135    let opts = boatramp_storage::GcsOptions {
136        bucket,
137        endpoint: args.gcs_endpoint.clone(),
138        anonymous: args.gcs_anonymous,
139    };
140    match notify_tier {
141        Some(tier) => {
142            let project = notify_account.unwrap_or_default();
143            let (storage, provider) =
144                boatramp_storage::GcsStorage::connect_with_notify(opts, project)
145                    .await
146                    .map_err(|err| Error::GcsConnect(err.to_string()))?;
147            Ok(BuiltBlobs {
148                storage: Arc::new(storage),
149                watch_provider: Some(Arc::new(provider)),
150                provision_tier: tier,
151            })
152        }
153        None => {
154            let storage = boatramp_storage::GcsStorage::connect(opts)
155                .await
156                .map_err(|err| Error::GcsConnect(err.to_string()))?;
157            Ok(BuiltBlobs {
158                storage: Arc::new(storage),
159                watch_provider: None,
160                provision_tier: boatramp_core::blob_notify::ProvisionTier::default(),
161            })
162        }
163    }
164}
165
166#[cfg(not(feature = "gcs"))]
167async fn build_gcs(
168    _args: &BlobArgs,
169    _notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
170    _notify_account: Option<String>,
171) -> Result<BuiltBlobs> {
172    Err(Error::NoGcsSupport)
173}
174
175#[cfg(feature = "s3")]
176async fn build_s3(
177    args: &BlobArgs,
178    notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
179    notify_account: Option<String>,
180) -> Result<BuiltBlobs> {
181    let bucket = args.s3_bucket.clone().ok_or(Error::S3BucketRequired)?;
182    let opts = boatramp_storage::S3Options {
183        bucket,
184        endpoint: args.s3_endpoint.clone(),
185        region: args.s3_region.clone(),
186        force_path_style: args.s3_path_style,
187    };
188    match notify_tier {
189        // Blob-change notification provisioning is enabled: build the
190        // consumer-wired storage + the S3→SQS provider from one AWS config.
191        Some(tier) => {
192            let account = notify_account.unwrap_or_default();
193            let (storage, provider) =
194                boatramp_storage::S3Storage::connect_with_notify(opts, account).await;
195            Ok(BuiltBlobs {
196                storage: Arc::new(storage),
197                watch_provider: Some(Arc::new(provider)),
198                provision_tier: tier,
199            })
200        }
201        // No provisioning configured: a plain S3 backend (blob triggers refuse).
202        None => Ok(BuiltBlobs {
203            storage: Arc::new(boatramp_storage::S3Storage::connect(opts).await),
204            watch_provider: None,
205            provision_tier: boatramp_core::blob_notify::ProvisionTier::default(),
206        }),
207    }
208}
209
210#[cfg(not(feature = "s3"))]
211async fn build_s3(
212    _args: &BlobArgs,
213    _notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
214    _notify_account: Option<String>,
215) -> Result<BuiltBlobs> {
216    Err(Error::NoS3Support)
217}