use async_trait::async_trait;
use std::path::{Path, PathBuf};
use crate::{FileError, Result};
#[async_trait]
pub trait StorageBackend: Send + Sync {
async fn initialize(&self) -> Result<()>;
async fn write(&self, key: &str, data: &[u8]) -> Result<PathBuf>;
async fn read(&self, path: &Path) -> Result<Vec<u8>>;
async fn delete(&self, path: &Path) -> Result<()>;
async fn exists(&self, key: &str) -> bool;
fn full_path(&self, relative_path: &Path) -> PathBuf;
async fn stats(&self) -> Result<BackendStats>;
}
#[derive(Debug, Clone, Default)]
pub struct BackendStats {
pub total_objects: usize,
pub total_size: u64,
pub available_space: Option<u64>,
}
pub struct LocalStorageBackend {
data_dir: PathBuf,
}
impl LocalStorageBackend {
pub fn new(data_dir: PathBuf) -> Self {
Self { data_dir }
}
pub fn data_dir(&self) -> &Path {
&self.data_dir
}
fn hash_to_path(&self, hash: &str) -> PathBuf {
if hash.len() < 4 {
return PathBuf::from(hash);
}
let prefix = &hash[0..2];
let rest = &hash[2..];
PathBuf::from(prefix).join(rest)
}
}
#[async_trait]
impl StorageBackend for LocalStorageBackend {
async fn initialize(&self) -> Result<()> {
use tokio::fs;
fs::create_dir_all(&self.data_dir).await?;
for first in 'a'..='f' {
for second in '0'..='9' {
let subdir = self.data_dir.join(format!("{}{}", first, second));
fs::create_dir_all(&subdir).await?;
}
for second in 'a'..='f' {
let subdir = self.data_dir.join(format!("{}{}", first, second));
fs::create_dir_all(&subdir).await?;
}
}
tracing::info!("Local storage backend initialized at {:?}", self.data_dir);
Ok(())
}
async fn write(&self, key: &str, data: &[u8]) -> Result<PathBuf> {
use tokio::fs;
let relative_path = self.hash_to_path(key);
let full_path = self.data_dir.join(&relative_path);
if full_path.exists() {
tracing::debug!("File with key {} already exists, skipping write", key);
return Ok(relative_path);
}
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await?;
}
let temp_path = full_path.with_extension("tmp");
fs::write(&temp_path, data).await?;
fs::rename(&temp_path, &full_path).await?;
tracing::debug!("Stored file with key {} at {:?}", key, full_path);
Ok(relative_path)
}
async fn read(&self, relative_path: &Path) -> Result<Vec<u8>> {
use tokio::fs;
let full_path = self.data_dir.join(relative_path);
if !full_path.exists() {
return Err(FileError::NotFound(format!(
"File not found at {:?}",
full_path
)));
}
Ok(fs::read(&full_path).await?)
}
async fn delete(&self, relative_path: &Path) -> Result<()> {
use tokio::fs;
let full_path = self.data_dir.join(relative_path);
if full_path.exists() {
fs::remove_file(&full_path).await?;
tracing::debug!("Deleted file at {:?}", full_path);
}
Ok(())
}
async fn exists(&self, key: &str) -> bool {
let path = self.data_dir.join(self.hash_to_path(key));
path.exists()
}
fn full_path(&self, relative_path: &Path) -> PathBuf {
self.data_dir.join(relative_path)
}
async fn stats(&self) -> Result<BackendStats> {
let mut stats = BackendStats::default();
let mut entries = tokio::fs::read_dir(&self.data_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_dir() {
let mut sub_entries = tokio::fs::read_dir(&path).await?;
while let Some(sub_entry) = sub_entries.next_entry().await? {
let sub_path = sub_entry.path();
if sub_path.is_file() {
stats.total_objects += 1;
if let Ok(metadata) = sub_entry.metadata().await {
stats.total_size += metadata.len();
}
}
}
} else if path.is_file() {
stats.total_objects += 1;
if let Ok(metadata) = entry.metadata().await {
stats.total_size += metadata.len();
}
}
}
Ok(stats)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_local_storage_backend() {
let temp_dir = TempDir::new().unwrap();
let backend = LocalStorageBackend::new(temp_dir.path().to_path_buf());
backend.initialize().await.unwrap();
let data = b"test content";
let path = backend.write("abc123", data).await.unwrap();
assert_eq!(path, PathBuf::from("ab/c123"));
assert!(backend.exists("abc123").await);
assert!(!backend.exists("xyz789").await);
let read_data = backend.read(&path).await.unwrap();
assert_eq!(read_data, data);
let full = backend.full_path(&path);
assert!(full.to_string_lossy().contains("ab"));
assert!(full.to_string_lossy().contains("c123"));
backend.delete(&path).await.unwrap();
assert!(!backend.exists("abc123").await);
}
#[tokio::test]
async fn test_deduplication() {
let temp_dir = TempDir::new().unwrap();
let backend = LocalStorageBackend::new(temp_dir.path().to_path_buf());
backend.initialize().await.unwrap();
let data = b"duplicate content";
let path1 = backend.write("same_key", data).await.unwrap();
let path2 = backend.write("same_key", data).await.unwrap();
assert_eq!(path1, path2);
let stats = backend.stats().await.unwrap();
assert_eq!(stats.total_objects, 1);
}
#[test]
fn test_hash_to_path() {
let backend = LocalStorageBackend::new(PathBuf::from("/tmp"));
assert_eq!(
backend.hash_to_path("abcdef123456"),
PathBuf::from("ab/cdef123456")
);
assert_eq!(backend.hash_to_path("ab"), PathBuf::from("ab"));
assert_eq!(backend.hash_to_path("a"), PathBuf::from("a"));
assert_eq!(backend.hash_to_path(""), PathBuf::from(""));
}
}