use std::sync::Arc;
use eyre::Result;
use serde::{Deserialize, Serialize};
use super::{
backend::{LocalBackend, UserStorageBackend},
local_cache::LocalObjectCache,
resolver::PersistedImmutableDataResolver,
};
const fn default_max_object_bytes() -> usize {
16 * 1024 * 1024
}
const fn default_max_objects_per_owner() -> usize {
256
}
const fn default_local_cache_max_bytes() -> u64 {
20 * 1024 * 1024 * 1024
}
fn default_local_cache_dir() -> std::path::PathBuf {
std::path::PathBuf::from("./data/object-cache")
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct LocalCacheConfig {
pub dir: std::path::PathBuf,
pub max_bytes: u64,
}
impl Default for LocalCacheConfig {
fn default() -> Self {
Self {
dir: default_local_cache_dir(),
max_bytes: default_local_cache_max_bytes(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum StorageBackendConfig {
Local {
dir: std::path::PathBuf,
},
S3 {
bucket: String,
#[serde(default = "default_s3_region")]
region: String,
},
}
fn default_s3_region() -> String {
"us-west-2".to_string()
}
impl Default for StorageBackendConfig {
fn default() -> Self {
Self::Local {
dir: std::path::PathBuf::from("./data/persisted-immutable-data"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct StorageConfig {
pub backend: StorageBackendConfig,
#[serde(default = "default_max_object_bytes")]
pub max_object_bytes: usize,
#[serde(default = "default_max_objects_per_owner")]
pub max_objects_per_owner: usize,
pub local_cache: LocalCacheConfig,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
backend: StorageBackendConfig::default(),
max_object_bytes: default_max_object_bytes(),
max_objects_per_owner: default_max_objects_per_owner(),
local_cache: LocalCacheConfig::default(),
}
}
}
impl StorageConfig {
pub async fn build_backend(&self) -> Result<Arc<dyn UserStorageBackend>> {
match &self.backend {
StorageBackendConfig::Local { dir } => Ok(Arc::new(LocalBackend::new(dir))),
StorageBackendConfig::S3 { bucket, region } => {
#[cfg(feature = "persisted-immutable-data-s3")]
{
let backend = super::backend::S3Backend::new(bucket.clone(), region.clone()).await?;
Ok(Arc::new(backend))
}
#[cfg(not(feature = "persisted-immutable-data-s3"))]
{
let _ = (bucket, region);
eyre::bail!("S3 storage backend requires the `persisted-immutable-data-s3` feature")
}
}
}
}
pub async fn build(&self) -> Result<ObjectStorage> {
let backend = self.build_backend().await?;
let resolver = PersistedImmutableDataResolver::new(backend.clone());
Ok(ObjectStorage {
config: self.clone(),
backend,
resolver,
})
}
pub async fn build_with_local_cache(&self) -> Result<ObjectStorage> {
let mut storage = self.build().await?;
storage.resolver = self.front_with_local_cache(storage.resolver).await?;
Ok(storage)
}
async fn front_with_local_cache(
&self,
resolver: PersistedImmutableDataResolver,
) -> Result<PersistedImmutableDataResolver> {
match self.backend {
StorageBackendConfig::Local { .. } => Ok(resolver),
StorageBackendConfig::S3 { .. } => {
let local = LocalObjectCache::shared(&self.local_cache.dir, self.local_cache.max_bytes).await?;
Ok(resolver.with_local_cache(local))
}
}
}
}
#[derive(Clone)]
pub struct ObjectStorage {
pub config: StorageConfig,
pub backend: Arc<dyn UserStorageBackend>,
pub resolver: PersistedImmutableDataResolver,
}
impl std::fmt::Debug for ObjectStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ObjectStorage")
.field("config", &self.config)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_local() {
let cfg = StorageConfig::default();
assert!(matches!(cfg.backend, StorageBackendConfig::Local { .. }));
assert_eq!(cfg.max_object_bytes, 16 * 1024 * 1024);
assert_eq!(cfg.max_objects_per_owner, 256);
}
#[test]
fn local_config_roundtrips() {
let json = serde_json::json!({
"max_object_bytes": 1024,
"max_objects_per_owner": 8,
"backend": { "kind": "local", "dir": "/var/newton/persisted-immutable-data" }
});
let cfg: StorageConfig = serde_json::from_value(json).unwrap();
assert_eq!(cfg.max_object_bytes, 1024);
assert_eq!(cfg.max_objects_per_owner, 8);
assert_eq!(
cfg.backend,
StorageBackendConfig::Local {
dir: "/var/newton/persisted-immutable-data".into()
}
);
}
#[test]
fn s3_config_parses_with_default_region() {
let json = serde_json::json!({
"backend": { "kind": "s3", "bucket": "newton-persisted-immutable-data-prod" }
});
let cfg: StorageConfig = serde_json::from_value(json).unwrap();
assert_eq!(
cfg.backend,
StorageBackendConfig::S3 {
bucket: "newton-persisted-immutable-data-prod".into(),
region: "us-west-2".into(),
}
);
}
#[tokio::test]
async fn build_local_backend_works() {
let tmp = tempfile::tempdir().unwrap();
let cfg = StorageConfig {
backend: StorageBackendConfig::Local {
dir: tmp.path().to_path_buf(),
},
..Default::default()
};
let backend = cfg.build_backend().await.unwrap();
backend.put("k", b"v").await.unwrap();
assert_eq!(backend.get("k").await.unwrap(), b"v");
}
#[tokio::test]
async fn s3_backend_is_fronted_by_the_local_cache() {
let backend_dir = tempfile::tempdir().unwrap();
let cache_root = tempfile::tempdir().unwrap();
let cache_dir = cache_root.path().join("objects-cache");
let cfg = StorageConfig {
backend: StorageBackendConfig::S3 {
bucket: "bucket".into(),
region: default_s3_region(),
},
local_cache: LocalCacheConfig {
dir: cache_dir.clone(),
..Default::default()
},
..Default::default()
};
let resolver = cfg
.front_with_local_cache(PersistedImmutableDataResolver::new(Arc::new(LocalBackend::new(
backend_dir.path(),
))))
.await
.unwrap();
let cid = resolver.put(b"rego").await.unwrap();
assert_eq!(resolver.get(&cid).await.unwrap(), b"rego");
assert!(cache_dir.join("objects").join(&cid).is_file());
}
#[tokio::test]
async fn local_backend_is_read_directly() {
let backend_dir = tempfile::tempdir().unwrap();
let cache_root = tempfile::tempdir().unwrap();
let cache_dir = cache_root.path().join("objects-cache");
let cfg = StorageConfig {
backend: StorageBackendConfig::Local {
dir: backend_dir.path().to_path_buf(),
},
local_cache: LocalCacheConfig {
dir: cache_dir.clone(),
..Default::default()
},
..Default::default()
};
let resolver = cfg.build_with_local_cache().await.unwrap().resolver;
let cid = resolver.put(b"rego").await.unwrap();
assert_eq!(resolver.get(&cid).await.unwrap(), b"rego");
assert!(!cache_dir.exists());
}
#[test]
fn local_cache_config_defaults_each_field() {
let unset: StorageConfig = serde_json::from_value(serde_json::json!({
"backend": { "kind": "local", "dir": "/tmp/x" }
}))
.unwrap();
assert_eq!(unset.local_cache.dir, default_local_cache_dir());
assert_eq!(unset.local_cache.max_bytes, 20 * 1024 * 1024 * 1024);
let dir_only: StorageConfig = serde_json::from_value(serde_json::json!({
"backend": { "kind": "local", "dir": "/tmp/x" },
"local_cache": { "dir": "/var/cache/newton" }
}))
.unwrap();
assert_eq!(dir_only.local_cache.dir, std::path::PathBuf::from("/var/cache/newton"));
assert_eq!(dir_only.local_cache.max_bytes, default_local_cache_max_bytes());
}
}