use crate::cache::contract::CacheDriverContract;
use crate::results::AppResult;
use async_trait::async_trait;
use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter};
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct FilesystemCacheDriver {
base_path: Arc<PathBuf>,
path_cache: Arc<RwLock<HashMap<String, PathBuf>>>,
}
impl FilesystemCacheDriver {
pub fn new(base_path: impl AsRef<Path>) -> Self {
Self {
base_path: Arc::new(PathBuf::from(base_path.as_ref())),
path_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
async fn key_to_path(&self, key: &str) -> PathBuf {
if let Some(path) = self.path_cache.read().await.get(key) {
return path.clone();
}
let safe_key = if key.is_empty() {
"empty_key".to_string()
} else {
key.replace([':', '/', '\\', '<', '>', '"', '|', '?', '*'], "_")
};
let path = self.base_path.join(format!("{safe_key}.cache"));
self.path_cache
.write()
.await
.insert(key.to_string(), path.clone());
path
}
}
#[async_trait]
impl CacheDriverContract for FilesystemCacheDriver {
async fn keys(&self) -> AppResult<Vec<String>> {
let path_cache = self.path_cache.read().await;
let mut keys: Vec<String> = path_cache.keys().cloned().collect();
let mut dir = fs::read_dir(&*self.base_path).await?;
while let Some(entry) = dir.next_entry().await? {
if entry.file_type().await?.is_file()
&& let Some(file_name) = entry.file_name().to_str()
{
if let Some(stripped) = file_name.strip_suffix(".cache") {
let original_key = if stripped == "empty_key" {
"".to_string()
} else {
stripped.to_string()
};
if !keys.contains(&original_key) {
keys.push(original_key);
}
}
}
}
Ok(keys)
}
async fn keys_by_pattern(&self, pattern: &str) -> AppResult<Vec<String>> {
let regex = fancy_regex::Regex::new(pattern)?;
let all_keys = self.keys().await?;
Ok(all_keys
.into_iter()
.filter(|key| matches!(regex.is_match(key), Ok(true)))
.collect())
}
async fn put_raw(&self, key: &str, value: String) -> AppResult<String> {
let path = self.key_to_path(key).await;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
let file = fs::File::create(&path).await?;
let mut writer = BufWriter::new(file);
writer.write_all(value.as_bytes()).await?;
writer.flush().await?;
Ok(key.to_string())
}
async fn get_raw(&self, key: &str) -> AppResult<Option<String>> {
let path = self.key_to_path(key).await;
match fs::File::open(&path).await {
Ok(file) => {
let mut reader = BufReader::new(file);
let mut contents = String::with_capacity(1024); reader.read_to_string(&mut contents).await?;
Ok(Some(contents))
}
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
async fn forget(&self, key: &str) -> AppResult<i32> {
let path = self.key_to_path(key).await;
self.path_cache.write().await.remove(key);
match fs::remove_file(&path).await {
Ok(_) => Ok(1),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(0),
Err(e) => Err(e.into()),
}
}
async fn forget_by_pattern(&self, pattern: &str) -> AppResult<i32> {
let regex = fancy_regex::Regex::new(pattern)?;
let mut removed_count = 0;
let path_cache = self.path_cache.read().await;
let keys_to_remove: Vec<String> = path_cache
.keys()
.filter_map(|key| match regex.is_match(key) {
Ok(true) => Some(key.clone()),
_ => None,
})
.collect();
drop(path_cache);
for key in keys_to_remove {
let path = self.key_to_path(&key).await;
self.path_cache.write().await.remove(&key);
match fs::remove_file(&path).await {
Ok(_) => removed_count += 1,
Err(e) if e.kind() == ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
}
Ok(removed_count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
async fn setup_test_cache() -> (FilesystemCacheDriver, TempDir) {
let temp_dir = TempDir::new().unwrap();
let driver = FilesystemCacheDriver::new(temp_dir.path());
(driver, temp_dir)
}
#[tokio::test]
async fn test_forget_by_pattern_basic() {
let (driver, _temp_dir) = setup_test_cache().await;
let test_data = [
("user:123", "data1"),
("user:456", "data2"),
("cache:temp:1", "temp1"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let removed = driver.forget_by_pattern("^user:.*").await.unwrap();
assert_eq!(removed, 2);
assert_eq!(driver.get_raw("user:123").await.unwrap(), None);
assert_eq!(driver.get_raw("user:456").await.unwrap(), None);
assert!(driver.get_raw("cache:temp:1").await.unwrap().is_some());
}
#[tokio::test]
async fn test_forget_by_pattern_comprehensive() {
let (driver, _temp_dir) = setup_test_cache().await;
let test_data = [
("user:123", "data1"),
("user:456", "data2"),
("cache:temp:1", "temp1"),
("cache:temp:2", "temp2"),
("session:abc", "session1"),
("SESSION:xyz", "session2"),
("test.key", "value"),
("test-key", "value"),
("", "empty"),
("special*char", "special"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let removed = driver.forget_by_pattern("^user:.*").await.unwrap();
assert_eq!(removed, 2);
assert_eq!(driver.get_raw("user:123").await.unwrap(), None);
assert_eq!(driver.get_raw("user:456").await.unwrap(), None);
assert!(driver.get_raw("cache:temp:1").await.unwrap().is_some());
let removed = driver.forget_by_pattern("^cache:temp:.*").await.unwrap();
assert_eq!(removed, 2);
assert_eq!(driver.get_raw("cache:temp:1").await.unwrap(), None);
assert_eq!(driver.get_raw("cache:temp:2").await.unwrap(), None);
let removed = driver.forget_by_pattern("(?i)^session:.*").await.unwrap();
assert_eq!(removed, 2);
assert_eq!(driver.get_raw("session:abc").await.unwrap(), None);
assert_eq!(driver.get_raw("SESSION:xyz").await.unwrap(), None);
let removed = driver.forget_by_pattern("test[.-]key").await.unwrap();
assert_eq!(removed, 2);
assert_eq!(driver.get_raw("test.key").await.unwrap(), None);
assert_eq!(driver.get_raw("test-key").await.unwrap(), None);
let removed = driver.forget_by_pattern("^$").await.unwrap(); assert_eq!(removed, 1); assert_eq!(driver.get_raw("").await.unwrap(), None);
let empty_key = "";
assert!(driver.put_raw(empty_key, "empty".to_string()).await.is_ok());
assert_eq!(
driver.get_raw(empty_key).await.unwrap(),
Some("empty".to_string())
);
let removed = driver.forget_by_pattern("^$").await.unwrap();
assert_eq!(removed, 1);
assert_eq!(driver.get_raw(empty_key).await.unwrap(), None);
let removed = driver.forget_by_pattern(r"special\*char").await.unwrap();
assert_eq!(removed, 1);
assert_eq!(driver.get_raw("special*char").await.unwrap(), None);
}
#[tokio::test]
async fn test_forget_by_pattern_concurrent() {
let (driver, _temp_dir) = setup_test_cache().await;
let driver_clone = driver.clone();
for i in 0..100 {
driver
.put_raw(&format!("test:{i}"), format!("value{i}"))
.await
.unwrap();
}
let driver_clone_1 = driver_clone.clone();
let handle1 = tokio::spawn(async move {
driver_clone_1
.forget_by_pattern("^test:([0-4]\\d|[0-9])$")
.await
.unwrap()
});
let driver_clone_2 = driver_clone.clone();
let handle2 = tokio::spawn(async move {
driver_clone_2
.forget_by_pattern("^test:[5-9]\\d$")
.await
.unwrap()
});
let (result1, result2) = tokio::join!(handle1, handle2);
let total_removed = result1.unwrap() + result2.unwrap();
assert_eq!(total_removed, 100, "Failed to remove all items");
for i in 0..100 {
assert!(
driver_clone
.get_raw(&format!("test:{i}"))
.await
.unwrap()
.is_none()
);
}
}
#[tokio::test]
async fn test_forget_by_pattern_invalid_regex() {
let (driver, _temp_dir) = setup_test_cache().await;
let result = driver.forget_by_pattern("[").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_forget_by_pattern_no_matches() {
let (driver, _temp_dir) = setup_test_cache().await;
driver
.put_raw("test:1", "value1".to_string())
.await
.unwrap();
driver
.put_raw("test:2", "value2".to_string())
.await
.unwrap();
let removed = driver.forget_by_pattern("^nonexistent:.*").await.unwrap();
assert_eq!(removed, 0);
assert!(driver.get_raw("test:1").await.unwrap().is_some());
assert!(driver.get_raw("test:2").await.unwrap().is_some());
}
#[tokio::test]
async fn test_keys_with_data() {
let (driver, _temp_dir) = setup_test_cache().await;
let test_data = [
("user_123", "data1"), ("user_456", "data2"), ("cache_temp_1", "temp1"), ("", "empty"), ];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let mut keys = driver.keys().await.unwrap();
keys.sort();
let mut expected: Vec<String> = test_data.iter().map(|(k, _)| k.to_string()).collect();
expected.sort();
assert_eq!(keys, expected, "Retrieved keys should match inserted keys");
}
#[tokio::test]
async fn test_keys_after_deletion() {
let (driver, _temp_dir) = setup_test_cache().await;
let test_data = [("key1", "value1"), ("key2", "value2"), ("key3", "value3")];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
driver.forget("key2").await.unwrap();
let mut keys = driver.keys().await.unwrap();
keys.sort();
let expected = vec!["key1".to_string(), "key3".to_string()];
assert_eq!(keys, expected, "Keys should reflect deletion");
}
#[tokio::test]
async fn test_keys_by_pattern_basic() {
let (driver, _temp_dir) = setup_test_cache().await;
let test_data = [
("user:123", "data1"),
("user:456", "data2"),
("cache:temp:1", "temp1"),
("cache:temp:2", "temp2"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let mut keys = driver.keys_by_pattern("^user:.*").await.unwrap();
keys.sort();
assert_eq!(
keys,
vec!["user:123".to_string(), "user:456".to_string()],
"Should match user: prefix"
);
let mut keys = driver.keys_by_pattern("^cache:temp:.*").await.unwrap();
keys.sort();
assert_eq!(
keys,
vec!["cache:temp:1".to_string(), "cache:temp:2".to_string()],
"Should match cache:temp: prefix"
);
}
#[tokio::test]
async fn test_keys_by_pattern_complex() {
let (driver, _temp_dir) = setup_test_cache().await;
let test_data = [
("abc123", "value1"),
("ABC456", "value2"),
("test_key", "value3"),
("test_key2", "value4"),
("123test", "value5"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let mut keys = driver.keys_by_pattern("(?i)^abc").await.unwrap();
keys.sort_by_key(|k| k.to_lowercase());
let mut expected = vec!["abc123".to_string(), "ABC456".to_string()];
expected.sort_by_key(|k| k.to_lowercase());
assert_eq!(keys, expected, "Should match case-insensitive");
let mut keys = driver.keys_by_pattern("test_key.*").await.unwrap();
keys.sort();
let mut expected = vec!["test_key".to_string(), "test_key2".to_string()];
expected.sort();
assert_eq!(keys, expected, "Should match keys with underscore");
let keys = driver.keys_by_pattern("^\\d+").await.unwrap();
assert_eq!(
keys,
vec!["123test".to_string()],
"Should match numeric prefix"
);
}
#[tokio::test]
async fn test_keys_by_pattern_no_matches() {
let (driver, _temp_dir) = setup_test_cache().await;
driver
.put_raw("test:1", "value1".to_string())
.await
.unwrap();
driver
.put_raw("test:2", "value2".to_string())
.await
.unwrap();
let keys = driver.keys_by_pattern("^nonexistent:.*").await.unwrap();
assert!(keys.is_empty(), "Should return empty vec for no matches");
}
#[tokio::test]
async fn test_keys_by_pattern_invalid_regex() {
let (driver, _temp_dir) = setup_test_cache().await;
let result = driver.keys_by_pattern("[").await;
assert!(result.is_err(), "Should return error for invalid regex");
}
}