use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
use crate::api::config::ConfigInfo;
use crate::api::naming::Service;
use crate::common::{build_config_key, build_service_key, md5_hash};
pub struct FileCache {
cache_dir: PathBuf,
}
impl FileCache {
pub fn new(cache_dir: impl AsRef<Path>) -> std::io::Result<Self> {
let cache_dir = cache_dir.as_ref().to_path_buf();
fs::create_dir_all(&cache_dir)?;
fs::create_dir_all(cache_dir.join("config"))?;
fs::create_dir_all(cache_dir.join("naming"))?;
Ok(Self { cache_dir })
}
fn config_dir(&self) -> PathBuf {
self.cache_dir.join("config")
}
fn naming_dir(&self) -> PathBuf {
self.cache_dir.join("naming")
}
fn encode_key(key: &str) -> String {
md5_hash(key)
}
pub fn save_config(&self, config: &ConfigInfo) -> std::io::Result<()> {
let key = build_config_key(&config.data_id, &config.group, &config.tenant);
let filename = Self::encode_key(&key);
let path = self.config_dir().join(&filename);
let json = serde_json::to_string(&ConfigCacheEntry {
data_id: config.data_id.clone(),
group: config.group.clone(),
tenant: config.tenant.clone(),
content: config.content.clone(),
md5: config.md5.clone(),
last_modified: config.last_modified,
content_type: config.content_type.clone(),
})
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut file = fs::File::create(&path)?;
file.write_all(json.as_bytes())?;
debug!("Saved config to cache: {}", key);
Ok(())
}
pub fn load_config(&self, data_id: &str, group: &str, tenant: &str) -> Option<ConfigInfo> {
let key = build_config_key(data_id, group, tenant);
let filename = Self::encode_key(&key);
let path = self.config_dir().join(&filename);
if !path.exists() {
return None;
}
match fs::File::open(&path) {
Ok(mut file) => {
let mut contents = String::new();
if file.read_to_string(&mut contents).is_err() {
return None;
}
match serde_json::from_str::<ConfigCacheEntry>(&contents) {
Ok(entry) => {
debug!("Loaded config from cache: {}", key);
Some(ConfigInfo {
data_id: entry.data_id,
group: entry.group,
tenant: entry.tenant,
content: entry.content,
md5: entry.md5,
last_modified: entry.last_modified,
content_type: entry.content_type,
})
}
Err(e) => {
warn!("Failed to parse cached config {}: {}", key, e);
None
}
}
}
Err(e) => {
warn!("Failed to read cached config {}: {}", key, e);
None
}
}
}
pub fn remove_config(&self, data_id: &str, group: &str, tenant: &str) -> std::io::Result<()> {
let key = build_config_key(data_id, group, tenant);
let filename = Self::encode_key(&key);
let path = self.config_dir().join(&filename);
if path.exists() {
fs::remove_file(&path)?;
debug!("Removed config from cache: {}", key);
}
Ok(())
}
pub fn save_service(&self, namespace: &str, service: &Service) -> std::io::Result<()> {
let key = build_service_key(&service.name, &service.group_name, namespace);
let filename = Self::encode_key(&key);
let path = self.naming_dir().join(&filename);
let json = serde_json::to_string(service)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut file = fs::File::create(&path)?;
file.write_all(json.as_bytes())?;
debug!("Saved service to cache: {}", key);
Ok(())
}
pub fn load_service(
&self,
namespace: &str,
group_name: &str,
service_name: &str,
) -> Option<Service> {
let key = build_service_key(service_name, group_name, namespace);
let filename = Self::encode_key(&key);
let path = self.naming_dir().join(&filename);
if !path.exists() {
return None;
}
match fs::File::open(&path) {
Ok(mut file) => {
let mut contents = String::new();
if file.read_to_string(&mut contents).is_err() {
return None;
}
match serde_json::from_str::<Service>(&contents) {
Ok(service) => {
debug!("Loaded service from cache: {}", key);
Some(service)
}
Err(e) => {
warn!("Failed to parse cached service {}: {}", key, e);
None
}
}
}
Err(e) => {
warn!("Failed to read cached service {}: {}", key, e);
None
}
}
}
pub fn remove_service(
&self,
namespace: &str,
group_name: &str,
service_name: &str,
) -> std::io::Result<()> {
let key = build_service_key(service_name, group_name, namespace);
let filename = Self::encode_key(&key);
let path = self.naming_dir().join(&filename);
if path.exists() {
fs::remove_file(&path)?;
debug!("Removed service from cache: {}", key);
}
Ok(())
}
pub fn clear(&self) -> std::io::Result<()> {
for entry in fs::read_dir(self.config_dir())?.flatten() {
fs::remove_file(entry.path())?;
}
for entry in fs::read_dir(self.naming_dir())?.flatten() {
fs::remove_file(entry.path())?;
}
debug!("Cleared file cache");
Ok(())
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct ConfigCacheEntry {
data_id: String,
group: String,
tenant: String,
content: String,
md5: String,
last_modified: i64,
content_type: String,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_config_cache() {
let dir = tempdir().unwrap();
let cache = FileCache::new(dir.path()).unwrap();
let mut config = ConfigInfo::new("test-data-id", "test-group", "test-tenant");
config.content = "test content".to_string();
config.md5 = md5_hash(&config.content);
cache.save_config(&config).unwrap();
let loaded = cache
.load_config("test-data-id", "test-group", "test-tenant")
.unwrap();
assert_eq!(loaded.content, "test content");
cache
.remove_config("test-data-id", "test-group", "test-tenant")
.unwrap();
assert!(cache
.load_config("test-data-id", "test-group", "test-tenant")
.is_none());
}
#[test]
fn test_service_cache() {
let dir = tempdir().unwrap();
let cache = FileCache::new(dir.path()).unwrap();
let service = Service::new("test-service", "test-group");
cache.save_service("test-namespace", &service).unwrap();
let loaded = cache
.load_service("test-namespace", "test-group", "test-service")
.unwrap();
assert_eq!(loaded.name, "test-service");
cache
.remove_service("test-namespace", "test-group", "test-service")
.unwrap();
assert!(cache
.load_service("test-namespace", "test-group", "test-service")
.is_none());
}
}