use super::{StorageBackend, StorageError};
use async_trait::async_trait;
use std::path::{Component, Path, PathBuf};
use tokio::fs;
#[allow(unused_imports)]
use tracing::{info, warn};
pub struct FileSystemStorageBackend {
base_path: PathBuf,
}
impl FileSystemStorageBackend {
pub fn new(base_path: impl AsRef<Path>) -> Self {
Self {
base_path: base_path.as_ref().to_path_buf(),
}
}
fn resolve_path(&self, path: &str) -> Result<PathBuf, StorageError> {
let normalized = path.trim_start_matches('/');
if normalized.contains("..") {
return Err(StorageError::PermissionDenied(
"Path traversal (..) not allowed".to_string(),
));
}
let full = self.base_path.join(normalized);
for component in full.components() {
if matches!(component, Component::ParentDir) {
return Err(StorageError::PermissionDenied(
"Path traversal not allowed".to_string(),
));
}
}
if full.exists() {
let canonical = full
.canonicalize()
.map_err(|e| StorageError::IoError(format!("Failed to resolve path: {}", e)))?;
let base_canonical = self
.base_path
.canonicalize()
.unwrap_or_else(|_| self.base_path.clone());
if !canonical.starts_with(&base_canonical) {
return Err(StorageError::PermissionDenied(
"Path escapes base directory".to_string(),
));
}
return Ok(canonical);
}
if let Some(parent) = full.parent()
&& parent.exists()
{
let parent_canonical = parent.canonicalize().map_err(|e| {
StorageError::IoError(format!("Failed to resolve parent path: {}", e))
})?;
let base_canonical = self
.base_path
.canonicalize()
.unwrap_or_else(|_| self.base_path.clone());
if !parent_canonical.starts_with(&base_canonical) {
return Err(StorageError::PermissionDenied(
"Path escapes base directory".to_string(),
));
}
}
Ok(full)
}
}
#[async_trait(?Send)]
impl StorageBackend for FileSystemStorageBackend {
async fn read_file(&self, path: &str) -> Result<Vec<u8>, StorageError> {
let full_path = self.resolve_path(path)?;
fs::read(&full_path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StorageError::FileNotFound(path.to_string())
} else {
StorageError::IoError(format!("Failed to read file {}: {}", path, e))
}
})
}
async fn write_file(&self, path: &str, content: &[u8]) -> Result<(), StorageError> {
let full_path = self.resolve_path(path)?;
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
StorageError::IoError(format!("Failed to create directory for {}: {}", path, e))
})?;
}
fs::write(&full_path, content)
.await
.map_err(|e| StorageError::IoError(format!("Failed to write file {}: {}", path, e)))
}
async fn list_files(&self, dir: &str) -> Result<Vec<String>, StorageError> {
let full_path = self.resolve_path(dir)?;
let mut entries = Vec::new();
let mut read_dir = fs::read_dir(&full_path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StorageError::DirectoryNotFound(dir.to_string())
} else {
StorageError::IoError(format!("Failed to read directory {}: {}", dir, e))
}
})?;
while let Some(entry) = read_dir
.next_entry()
.await
.map_err(|e| StorageError::IoError(format!("Failed to read directory entry: {}", e)))?
{
if let Ok(file_type) = entry.file_type().await
&& file_type.is_file()
&& let Some(file_name) = entry.file_name().to_str()
{
entries.push(file_name.to_string());
}
}
Ok(entries)
}
async fn file_exists(&self, path: &str) -> Result<bool, StorageError> {
let full_path = self.resolve_path(path)?;
match fs::metadata(&full_path).await {
Ok(metadata) => Ok(metadata.is_file()),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(false)
} else {
Err(StorageError::IoError(format!(
"Failed to check file existence {}: {}",
path, e
)))
}
}
}
}
async fn delete_file(&self, path: &str) -> Result<(), StorageError> {
let full_path = self.resolve_path(path)?;
fs::remove_file(&full_path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StorageError::FileNotFound(path.to_string())
} else {
StorageError::IoError(format!("Failed to delete file {}: {}", path, e))
}
})
}
async fn create_dir(&self, path: &str) -> Result<(), StorageError> {
let full_path = self.resolve_path(path)?;
fs::create_dir_all(&full_path).await.map_err(|e| {
StorageError::IoError(format!("Failed to create directory {}: {}", path, e))
})
}
async fn dir_exists(&self, path: &str) -> Result<bool, StorageError> {
let full_path = self.resolve_path(path)?;
match fs::metadata(&full_path).await {
Ok(metadata) => Ok(metadata.is_dir()),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(false)
} else {
Err(StorageError::IoError(format!(
"Failed to check directory existence {}: {}",
path, e
)))
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_path_traversal_blocked() {
let temp = TempDir::new().unwrap();
let backend = FileSystemStorageBackend::new(temp.path());
let result = backend.resolve_path("../etc/passwd");
assert!(matches!(result, Err(StorageError::PermissionDenied(_))));
let result = backend.resolve_path("/foo/../../../etc/passwd");
assert!(matches!(result, Err(StorageError::PermissionDenied(_))));
let result = backend.resolve_path("valid/path/file.txt");
assert!(result.is_ok());
}
#[test]
fn test_valid_paths_allowed() {
let temp = TempDir::new().unwrap();
let backend = FileSystemStorageBackend::new(temp.path());
let result = backend.resolve_path("file.txt");
assert!(result.is_ok());
let result = backend.resolve_path("foo/bar/baz.txt");
assert!(result.is_ok());
let result = backend.resolve_path("/file.txt");
assert!(result.is_ok());
}
}