use super::super::StorageError;
#[cfg(feature = "async")]
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectMetadata {
pub size_bytes: u64,
pub checksum: Option<String>,
pub created_at: Option<i64>,
pub modified_at: Option<i64>,
pub custom: HashMap<String, String>,
}
#[cfg(feature = "async")]
#[async_trait]
pub trait VcsProvider: Send + Sync + std::fmt::Debug {
async fn write_object(&self, path: &str, data: &[u8]) -> Result<(), StorageError>;
async fn read_object(&self, path: &str) -> Result<Vec<u8>, StorageError>;
async fn list_objects(&self, prefix: &str) -> Result<Vec<String>, StorageError>;
async fn delete_object(&self, path: &str) -> Result<bool, StorageError>;
async fn create_version(&self, message: &str) -> Result<String, StorageError>;
async fn get_object_metadata(&self, _path: &str) -> Result<ObjectMetadata, StorageError> {
Err(StorageError::IoError(
"get_object_metadata not supported by this provider".into(),
))
}
async fn health_check(&self) -> Result<bool, StorageError>;
fn provider_name(&self) -> &'static str;
fn config_summary(&self) -> String;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VcsProviderConfig {
pub provider_type: String,
pub endpoint: Option<String>,
pub access_key: Option<String>,
pub secret_key: Option<String>,
pub token: Option<String>,
pub repository: Option<String>,
pub branch: Option<String>,
pub extra: HashMap<String, String>,
}
impl VcsProviderConfig {
pub fn new(provider_type: impl Into<String>) -> Self {
Self {
provider_type: provider_type.into(),
endpoint: None,
access_key: None,
secret_key: None,
token: None,
repository: None,
branch: None,
extra: HashMap::new(),
}
}
pub fn from_env(provider_type: &str) -> Self {
let prefix = format!("BRIEFCASE_{}", provider_type.to_uppercase());
let endpoint = std::env::var(format!("{}_ENDPOINT", prefix)).ok();
let access_key = std::env::var(format!("{}_ACCESS_KEY", prefix)).ok();
let secret_key = std::env::var(format!("{}_PRIVATE_KEY", prefix)).ok();
let token = std::env::var(format!("{}_TOKEN", prefix)).ok();
let repository = std::env::var(format!("{}_REPOSITORY", prefix)).ok();
let branch = std::env::var(format!("{}_BRANCH", prefix)).ok();
let standard_suffixes = [
"ENDPOINT",
"ACCESS_KEY",
"PRIVATE_KEY",
"TOKEN",
"REPOSITORY",
"BRANCH",
];
let mut extra = HashMap::new();
let prefix_with_underscore = format!("{}_", prefix);
for (key, value) in std::env::vars() {
if let Some(suffix) = key.strip_prefix(&prefix_with_underscore) {
if !standard_suffixes.contains(&suffix) {
extra.insert(suffix.to_lowercase(), value);
}
}
}
Self {
provider_type: provider_type.to_string(),
endpoint,
access_key,
secret_key,
token,
repository,
branch,
extra,
}
}
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
pub fn with_access_key(mut self, key: impl Into<String>) -> Self {
self.access_key = Some(key.into());
self
}
pub fn with_secret_key(mut self, key: impl Into<String>) -> Self {
self.secret_key = Some(key.into());
self
}
pub fn with_token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
self
}
pub fn with_repository(mut self, repo: impl Into<String>) -> Self {
self.repository = Some(repo.into());
self
}
pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
self.branch = Some(branch.into());
self
}
pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.insert(key.into(), value.into());
self
}
}
#[cfg(feature = "async")]
#[async_trait]
impl VcsProvider for Box<dyn VcsProvider> {
async fn write_object(&self, path: &str, data: &[u8]) -> Result<(), StorageError> {
(**self).write_object(path, data).await
}
async fn read_object(&self, path: &str) -> Result<Vec<u8>, StorageError> {
(**self).read_object(path).await
}
async fn list_objects(&self, prefix: &str) -> Result<Vec<String>, StorageError> {
(**self).list_objects(prefix).await
}
async fn delete_object(&self, path: &str) -> Result<bool, StorageError> {
(**self).delete_object(path).await
}
async fn create_version(&self, message: &str) -> Result<String, StorageError> {
(**self).create_version(message).await
}
async fn get_object_metadata(&self, path: &str) -> Result<ObjectMetadata, StorageError> {
(**self).get_object_metadata(path).await
}
async fn health_check(&self) -> Result<bool, StorageError> {
(**self).health_check().await
}
fn provider_name(&self) -> &'static str {
(**self).provider_name()
}
fn config_summary(&self) -> String {
(**self).config_summary()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_new() {
let config = VcsProviderConfig::new("nessie");
assert_eq!(config.provider_type, "nessie");
assert!(config.endpoint.is_none());
assert!(config.extra.is_empty());
}
#[test]
fn test_config_builder() {
let config = VcsProviderConfig::new("dvc")
.with_endpoint("https://dvc.example.com")
.with_repository("my-repo")
.with_branch("main")
.with_extra("remote_name", "myremote");
assert_eq!(config.provider_type, "dvc");
assert_eq!(config.endpoint.unwrap(), "https://dvc.example.com");
assert_eq!(config.repository.unwrap(), "my-repo");
assert_eq!(config.branch.unwrap(), "main");
assert_eq!(config.extra.get("remote_name").unwrap(), "myremote");
}
#[test]
fn test_config_from_env() {
std::env::set_var("BRIEFCASE_TESTPROV_ENDPOINT", "https://test.example.com");
std::env::set_var("BRIEFCASE_TESTPROV_REPOSITORY", "test-repo");
std::env::set_var("BRIEFCASE_TESTPROV_CUSTOM_FIELD", "custom_value");
let config = VcsProviderConfig::from_env("testprov");
assert_eq!(config.provider_type, "testprov");
assert_eq!(config.endpoint.unwrap(), "https://test.example.com");
assert_eq!(config.repository.unwrap(), "test-repo");
assert_eq!(config.extra.get("custom_field").unwrap(), "custom_value");
std::env::remove_var("BRIEFCASE_TESTPROV_ENDPOINT");
std::env::remove_var("BRIEFCASE_TESTPROV_REPOSITORY");
std::env::remove_var("BRIEFCASE_TESTPROV_CUSTOM_FIELD");
}
}