use std::path::Path;
use std::sync::Arc;
#[cfg(feature = "fallback")]
use std::time::Duration;
use boatramp_core::Storage;
use crate::backends::BlobBackend;
use crate::error::{Error, Result};
#[cfg(feature = "fs")]
use boatramp_storage::FsStorage;
#[cfg(feature = "fallback")]
pub fn blob_fallback_when() -> boatramp_storage::FallbackWhen {
Arc::new(|k: &str| {
boatramp_core::deploy::is_blob_key(k)
|| k.starts_with("hblob/")
|| k.starts_with("mqp/")
|| k.starts_with("mqgp/")
})
}
#[cfg(feature = "fallback")]
pub const DEFAULT_SCOPE_BLOB_FAMILIES: &[&str] = &["hblob/", "mqp/", "mqgp/"];
#[cfg(feature = "fallback")]
const DEFAULT_SCOPE_FALLBACK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
#[cfg(feature = "fallback")]
pub fn default_scope_to_old() -> boatramp_storage::KeyTranslate {
Arc::new(|k: &str| {
for fam in DEFAULT_SCOPE_BLOB_FAMILIES {
if let Some(rest) = k.strip_prefix(fam) {
let suffix = rest.strip_prefix("default/")?;
return Some(format!("{fam}{suffix}"));
}
}
None
})
}
#[cfg(feature = "fallback")]
pub fn default_scope_to_new(
projects: std::sync::Arc<std::collections::HashSet<String>>,
) -> boatramp_storage::KeyTranslate {
Arc::new(move |k: &str| {
for fam in DEFAULT_SCOPE_BLOB_FAMILIES {
if let Some(rest) = k.strip_prefix(fam) {
let first = rest.split('/').next().unwrap_or("");
if first.is_empty()
|| first == boatramp_core::project::DEFAULT_PROJECT
|| projects.contains(first)
{
return None;
}
return Some(format!(
"{fam}{}/{rest}",
boatramp_core::project::DEFAULT_PROJECT
));
}
}
None
})
}
#[cfg(feature = "fallback")]
pub struct DefaultScopeFallbackBackends {
inner: Arc<dyn boatramp_core::blob_backends::BlobBackends>,
to_old: boatramp_storage::KeyTranslate,
to_new: boatramp_storage::KeyTranslate,
}
#[cfg(feature = "fallback")]
impl boatramp_core::blob_backends::BlobBackends for DefaultScopeFallbackBackends {
fn backend(&self, project: boatramp_core::project::ProjectRef<'_>) -> Arc<dyn Storage> {
Arc::new(boatramp_storage::TranslatingFallbackStorage::new(
self.inner.backend(project),
self.to_old.clone(),
self.to_new.clone(),
DEFAULT_SCOPE_FALLBACK_TIMEOUT,
))
}
fn default_backend(&self) -> Arc<dyn Storage> {
Arc::new(boatramp_storage::TranslatingFallbackStorage::new(
self.inner.default_backend(),
self.to_old.clone(),
self.to_new.clone(),
DEFAULT_SCOPE_FALLBACK_TIMEOUT,
))
}
}
#[cfg(feature = "fallback")]
pub async fn default_scope_blob_fallback(
kv: &dyn boatramp_core::kv::KvStore,
inner: Arc<dyn boatramp_core::blob_backends::BlobBackends>,
) -> Arc<dyn boatramp_core::blob_backends::BlobBackends> {
let in_soak = boatramp_core::migrate::read_state(kv)
.await
.map(|s| {
s.unfinalized
.contains(&boatramp_core::migrate::DEFAULT_SCOPE_REHOME_VERSION)
})
.unwrap_or(false);
if !in_soak {
return inner;
}
let mut projects = std::collections::HashSet::new();
if let Ok(keys) = kv.list_prefix(boatramp_core::project::POINTER_PREFIX).await {
for key in keys {
if let Some(name) = key.strip_prefix(boatramp_core::project::POINTER_PREFIX)
&& !name.is_empty()
&& !name.contains('/')
{
projects.insert(name.to_string());
}
}
}
tracing::warn!(
"default-scope blob read-fallback ACTIVE (migration {} in dual soak): serving bare \
default-scoped blobs at their new default/ spelling. Run the blob re-home drain, then \
`boatramp migrate --finalize` + retire the old blobs, and restart to disable it.",
boatramp_core::migrate::DEFAULT_SCOPE_REHOME_VERSION
);
Arc::new(DefaultScopeFallbackBackends {
inner,
to_old: default_scope_to_old(),
to_new: default_scope_to_new(std::sync::Arc::new(projects)),
})
}
#[derive(Debug, Clone)]
pub struct BlobArgs {
pub blobs: BlobBackend,
pub s3_bucket: Option<String>,
pub s3_endpoint: Option<String>,
pub s3_region: Option<String>,
pub s3_path_style: bool,
pub s3_credential: Option<crate::s3_credential::SealedS3Credential>,
pub gcs_bucket: Option<String>,
pub gcs_endpoint: Option<String>,
pub gcs_anonymous: bool,
pub azure_account: Option<String>,
pub azure_container: Option<String>,
pub azure_access_key: Option<String>,
pub azure_emulator: bool,
}
pub struct BuiltBlobs {
pub storage: Arc<dyn Storage>,
#[cfg_attr(not(feature = "handlers"), allow(dead_code))]
pub watch_provider: Option<Arc<dyn boatramp_core::blob_provision::WatchProvider>>,
#[cfg_attr(not(feature = "handlers"), allow(dead_code))]
pub provision_tier: boatramp_core::blob_notify::ProvisionTier,
}
#[cfg_attr(not(feature = "fs"), allow(unused_variables))]
pub async fn build_blobs(
args: &BlobArgs,
data_dir: &Path,
notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
notify_account: Option<String>,
) -> Result<BuiltBlobs> {
match args.blobs {
#[cfg(feature = "fs")]
BlobBackend::Fs => Ok(BuiltBlobs {
storage: Arc::new(FsStorage::new(data_dir.join("blobs"))),
watch_provider: None,
provision_tier: boatramp_core::blob_notify::ProvisionTier::default(),
}),
#[cfg(not(feature = "fs"))]
BlobBackend::Fs => Err(Error::NoFsSupport),
BlobBackend::S3 => build_s3(args, notify_tier, notify_account).await,
BlobBackend::Gcs => build_gcs(args, notify_tier, notify_account).await,
BlobBackend::Azure => build_azure(args, notify_tier, notify_account).await,
}
}
#[cfg(feature = "fallback")]
pub async fn build_blobs_with_fallback(
args: &BlobArgs,
fallback: Option<(&BlobArgs, Duration)>,
data_dir: &Path,
notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
notify_account: Option<String>,
) -> Result<BuiltBlobs> {
let primary = build_blobs(args, data_dir, notify_tier, notify_account).await?;
let Some((secondary_args, timeout)) = fallback else {
return Ok(primary);
};
let secondary = build_blobs(secondary_args, data_dir, None, None).await?;
let composite: Arc<dyn Storage> = Arc::new(boatramp_storage::FallbackStorage::new(
primary.storage,
secondary.storage,
blob_fallback_when(),
timeout,
));
Ok(BuiltBlobs {
storage: composite,
watch_provider: primary.watch_provider,
provision_tier: primary.provision_tier,
})
}
#[cfg(feature = "azure")]
async fn build_azure(
args: &BlobArgs,
notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
_notify_account: Option<String>,
) -> Result<BuiltBlobs> {
let (Some(account), Some(container)) =
(args.azure_account.clone(), args.azure_container.clone())
else {
return Err(Error::AzureConfigRequired);
};
let opts = boatramp_storage::AzureOptions {
account,
container,
access_key: args.azure_access_key.clone(),
emulator: args.azure_emulator,
};
match notify_tier {
Some(tier) => {
let (storage, provider) = boatramp_storage::AzureStorage::connect_with_notify(opts)
.map_err(|err| Error::AzureConnect(err.to_string()))?;
Ok(BuiltBlobs {
storage: Arc::new(storage),
watch_provider: Some(Arc::new(provider)),
provision_tier: tier,
})
}
None => {
let storage = boatramp_storage::AzureStorage::connect(opts)
.map_err(|err| Error::AzureConnect(err.to_string()))?;
Ok(BuiltBlobs {
storage: Arc::new(storage),
watch_provider: None,
provision_tier: boatramp_core::blob_notify::ProvisionTier::default(),
})
}
}
}
#[cfg(not(feature = "azure"))]
async fn build_azure(
_args: &BlobArgs,
_notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
_notify_account: Option<String>,
) -> Result<BuiltBlobs> {
Err(Error::NoAzureSupport)
}
#[cfg(feature = "gcs")]
async fn build_gcs(
args: &BlobArgs,
notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
notify_account: Option<String>,
) -> Result<BuiltBlobs> {
let bucket = args.gcs_bucket.clone().ok_or(Error::GcsBucketRequired)?;
let opts = boatramp_storage::GcsOptions {
bucket,
endpoint: args.gcs_endpoint.clone(),
anonymous: args.gcs_anonymous,
};
match notify_tier {
Some(tier) => {
let project = notify_account.unwrap_or_default();
let (storage, provider) =
boatramp_storage::GcsStorage::connect_with_notify(opts, project)
.await
.map_err(|err| Error::GcsConnect(err.to_string()))?;
Ok(BuiltBlobs {
storage: Arc::new(storage),
watch_provider: Some(Arc::new(provider)),
provision_tier: tier,
})
}
None => {
let storage = boatramp_storage::GcsStorage::connect(opts)
.await
.map_err(|err| Error::GcsConnect(err.to_string()))?;
Ok(BuiltBlobs {
storage: Arc::new(storage),
watch_provider: None,
provision_tier: boatramp_core::blob_notify::ProvisionTier::default(),
})
}
}
}
#[cfg(not(feature = "gcs"))]
async fn build_gcs(
_args: &BlobArgs,
_notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
_notify_account: Option<String>,
) -> Result<BuiltBlobs> {
Err(Error::NoGcsSupport)
}
#[cfg(feature = "s3")]
async fn build_s3(
args: &BlobArgs,
notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
notify_account: Option<String>,
) -> Result<BuiltBlobs> {
let bucket = args.s3_bucket.clone().ok_or(Error::S3BucketRequired)?;
let opts = boatramp_storage::S3Options {
bucket,
endpoint: args.s3_endpoint.clone(),
region: args.s3_region.clone(),
force_path_style: args.s3_path_style,
credential: args.s3_credential.as_ref().map(|c| c.as_pair()),
};
match notify_tier {
Some(tier) => {
let account = notify_account.unwrap_or_default();
let (storage, provider) =
boatramp_storage::S3Storage::connect_with_notify(opts, account).await;
Ok(BuiltBlobs {
storage: Arc::new(storage),
watch_provider: Some(Arc::new(provider)),
provision_tier: tier,
})
}
None => Ok(BuiltBlobs {
storage: Arc::new(boatramp_storage::S3Storage::connect(opts).await),
watch_provider: None,
provision_tier: boatramp_core::blob_notify::ProvisionTier::default(),
}),
}
}
#[cfg(not(feature = "s3"))]
async fn build_s3(
_args: &BlobArgs,
_notify_tier: Option<boatramp_core::blob_notify::ProvisionTier>,
_notify_account: Option<String>,
) -> Result<BuiltBlobs> {
Err(Error::NoS3Support)
}