use super::{Storage, StorageError};
use std::fs;
use std::path::PathBuf;
pub struct LocalStorage {
root: PathBuf,
base_url: String,
}
impl LocalStorage {
pub fn new(root: impl Into<String>) -> Self {
LocalStorage { root: PathBuf::from(root.into()), base_url: String::new() }
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
fn resolve(&self, key: &str) -> Result<PathBuf, StorageError> {
if key.split('/').any(|segment| segment == "..") {
return Err(StorageError::new(format!("key '{key}' must not contain '..' segments")));
}
Ok(self.root.join(key.trim_start_matches('/')))
}
}
impl Storage for LocalStorage {
fn put(&self, key: &str, data: &[u8], _content_type: &str) -> Result<String, StorageError> {
let path = self.resolve(key)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| StorageError::new(format!("failed to create '{}': {e}", parent.display())))?;
}
fs::write(&path, data).map_err(|e| StorageError::new(format!("failed to write '{key}': {e}")))?;
Ok(key.to_string())
}
fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
let path = self.resolve(key)?;
fs::read(&path).map_err(|e| StorageError::new(format!("failed to read '{key}': {e}")))
}
fn delete(&self, key: &str) -> Result<(), StorageError> {
let path = self.resolve(key)?;
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(StorageError::new(format!("failed to delete '{key}': {e}"))),
}
}
fn url(&self, key: &str) -> String {
if self.base_url.is_empty() {
match self.resolve(key) {
Ok(path) => path.to_string_lossy().to_string(),
Err(_) => key.to_string(),
}
} else {
format!("{}/{}", self.base_url.trim_end_matches('/'), key.trim_start_matches('/'))
}
}
}
#[cfg(test)]
mod tests;