use crate::{Error, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileType {
Config,
Keys,
Data,
Index,
Snapshots,
Locks,
}
impl FileType {
pub fn as_str(&self) -> &'static str {
match self {
FileType::Config => "config",
FileType::Keys => "keys",
FileType::Data => "data",
FileType::Index => "index",
FileType::Snapshots => "snapshots",
FileType::Locks => "locks",
}
}
pub fn subdir(&self) -> &'static str {
match self {
FileType::Config => "",
FileType::Keys => "keys",
FileType::Data => "data",
FileType::Index => "index",
FileType::Snapshots => "snapshots",
FileType::Locks => "locks",
}
}
}
#[async_trait]
pub trait Backend: Send + Sync + Debug {
async fn list_files(&self, file_type: FileType) -> Result<Vec<String>>;
async fn read_range(
&self,
file_type: FileType,
id: &str,
offset: u64,
length: u64,
) -> Result<Vec<u8>>;
async fn read_full(&self, file_type: FileType, id: &str) -> Result<Vec<u8>>;
async fn write(&self, file_type: FileType, id: &str, data: Vec<u8>) -> Result<()>;
async fn delete(&self, file_type: FileType, id: &str) -> Result<()>;
async fn exists(&self, file_type: FileType, id: &str) -> Result<bool>;
async fn metadata(&self, file_type: FileType, id: &str) -> Result<FileMetadata>;
async fn create_lock(&self, lock_name: &str, timeout_secs: u64) -> Result<Lock>;
async fn test_connection(&self) -> Result<()>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetadata {
pub size: u64,
pub modified: chrono::DateTime<chrono::Utc>,
pub etag: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Lock {
pub name: String,
pub expires_at: chrono::DateTime<chrono::Utc>,
pub holder_id: String,
}
impl Lock {
pub fn new(name: String, timeout_secs: u64) -> Self {
let holder_id = uuid::Uuid::new_v4().to_string();
let expires_at = chrono::Utc::now() + chrono::Duration::seconds(timeout_secs as i64);
Self {
name,
expires_at,
holder_id,
}
}
pub fn is_expired(&self) -> bool {
chrono::Utc::now() > self.expires_at
}
}
mod filesystem;
mod s3;
mod sftp;
pub use filesystem::FilesystemBackend;
#[cfg(feature = "s3")]
pub use s3::S3Backend;
#[cfg(feature = "sftp")]
pub use sftp::SftpBackend;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BackendConfig {
Filesystem {
path: String,
},
#[cfg(feature = "s3")]
S3 {
bucket: String,
prefix: Option<String>,
region: String,
access_key_id: Option<String>,
secret_access_key: Option<String>,
endpoint: Option<String>,
},
#[cfg(feature = "sftp")]
Sftp {
host: String,
port: u16,
username: String,
password: Option<String>,
private_key_path: Option<String>,
path: String,
},
}
impl BackendConfig {
pub async fn create_backend(&self) -> Result<Box<dyn Backend>> {
match self {
BackendConfig::Filesystem { path } => {
Ok(Box::new(FilesystemBackend::new(path.clone())?))
}
#[cfg(feature = "s3")]
BackendConfig::S3 {
bucket,
prefix,
region,
access_key_id,
secret_access_key,
endpoint,
} => Ok(Box::new(
S3Backend::new(
bucket.clone(),
prefix.clone(),
region.clone(),
access_key_id.clone(),
secret_access_key.clone(),
endpoint.clone(),
)
.await?,
)),
#[cfg(feature = "sftp")]
BackendConfig::Sftp {
host,
port,
username,
password,
private_key_path,
path,
} => Ok(Box::new(
SftpBackend::new(
host.clone(),
*port,
username.clone(),
password.clone(),
private_key_path.clone(),
path.clone(),
)
.await?,
)),
}
}
pub fn from_url(url: &str) -> Result<Self> {
let parsed = url::Url::parse(url)
.map_err(|e| Error::configuration(format!("Invalid URL: {}", e)))?;
match parsed.scheme() {
"file" => Ok(BackendConfig::Filesystem {
path: parsed.path().to_string(),
}),
#[cfg(feature = "s3")]
"s3" => {
let bucket = parsed
.host_str()
.ok_or_else(|| Error::configuration("S3 URL must specify bucket as host"))?
.to_string();
let prefix = if parsed.path().len() > 1 {
Some(parsed.path()[1..].to_string()) } else {
None
};
let query_pairs: std::collections::HashMap<String, String> = parsed
.query_pairs()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
Ok(BackendConfig::S3 {
bucket,
prefix,
region: query_pairs
.get("region")
.cloned()
.unwrap_or_else(|| "us-east-1".to_string()),
access_key_id: query_pairs.get("access_key_id").cloned(),
secret_access_key: query_pairs.get("secret_access_key").cloned(),
endpoint: query_pairs.get("endpoint").cloned(),
})
}
#[cfg(feature = "sftp")]
"sftp" => {
let host = parsed
.host_str()
.ok_or_else(|| Error::configuration("SFTP URL must specify host"))?
.to_string();
let port = parsed.port().unwrap_or(22);
let username = parsed.username().to_string();
let password = parsed.password().map(|p| p.to_string());
Ok(BackendConfig::Sftp {
host,
port,
username,
password,
private_key_path: None,
path: parsed.path().to_string(),
})
}
scheme => Err(Error::configuration(format!(
"Unsupported URL scheme: {}",
scheme
))),
}
}
}