#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
pub mod driver;
pub mod filesystem;
pub mod local;
pub mod memory;
#[cfg(feature = "s3")]
pub mod s3;
pub use driver::FilesystemDriver;
pub use filesystem::{normalise_path, normalise_prefix, Filesystem, FilesystemExt, Metadata};
pub use local::LocalFilesystem;
pub use memory::MemoryFilesystem;
#[cfg(feature = "s3")]
pub use s3::S3Filesystem;
use std::sync::Arc;
#[derive(Clone)]
pub struct Storage {
disk: Arc<dyn Filesystem>,
}
impl Storage {
pub fn new(disk: Arc<dyn Filesystem>) -> Self {
Self { disk }
}
pub fn local(root: impl Into<std::path::PathBuf>) -> Self {
Self::new(Arc::new(LocalFilesystem::new(root)))
}
pub fn memory() -> Self {
Self::new(Arc::new(MemoryFilesystem::new()))
}
pub fn disk(&self) -> &Arc<dyn Filesystem> {
&self.disk
}
pub fn driver(&self) -> &str {
self.disk.name()
}
}
impl std::ops::Deref for Storage {
type Target = Arc<dyn Filesystem>;
fn deref(&self) -> &Self::Target {
&self.disk
}
}
impl std::fmt::Debug for Storage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Storage").field("driver", &self.driver()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
#[tokio::test]
async fn the_facade_value_delegates_to_its_disk() {
let storage = Storage::memory();
storage.put_string("a.txt", "hello").await.unwrap();
assert_eq!(storage.get_string("a.txt").await.unwrap().as_deref(), Some("hello"));
assert_eq!(storage.driver(), "memory");
}
#[tokio::test]
async fn every_local_driver_behaves_identically() {
let root =
std::env::temp_dir().join("rainier-fs-parity").join(format!("{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let disks: Vec<(&str, Arc<dyn Filesystem>)> = vec![
("memory", Arc::new(MemoryFilesystem::new())),
("local", Arc::new(LocalFilesystem::new(&root))),
];
for (name, disk) in disks {
assert_eq!(disk.get("absent.txt").await.unwrap(), None, "{name}");
assert!(!disk.exists("absent.txt").await.unwrap(), "{name}");
assert!(!disk.delete("absent.txt").await.unwrap(), "{name}");
disk.put("a/b.txt", Bytes::from_static(b"hello")).await.unwrap();
assert_eq!(disk.get("a/b.txt").await.unwrap().unwrap(), "hello", "{name}");
assert_eq!(disk.size("a/b.txt").await.unwrap(), Some(5), "{name}");
assert_eq!(disk.get("/a//b.txt").await.unwrap().unwrap(), "hello", "{name}");
assert!(disk.get("../escape").await.is_err(), "{name}");
disk.put("a/deeper/c.txt", Bytes::from_static(b"x")).await.unwrap();
let listed: Vec<String> =
disk.list("a").await.unwrap().into_iter().map(|m| m.path).collect();
assert_eq!(listed, vec!["a/b.txt".to_string()], "{name}");
disk.copy("a/b.txt", "copied.txt").await.unwrap();
assert!(disk.exists("a/b.txt").await.unwrap(), "{name}");
disk.rename("copied.txt", "moved.txt").await.unwrap();
assert!(!disk.exists("copied.txt").await.unwrap(), "{name}");
assert_eq!(disk.get("moved.txt").await.unwrap().unwrap(), "hello", "{name}");
assert_eq!(disk.get_or_fail("absent.txt").await.unwrap_err().status(), 404, "{name}");
assert_eq!(disk.copy("absent.txt", "x.txt").await.unwrap_err().status(), 404, "{name}");
}
let _ = std::fs::remove_dir_all(&root);
}
}