use crate::core::metadata::SkillMetadata;
use crate::core::service::ServiceError;
use async_trait::async_trait;
use serde_json;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
use tokio::sync::RwLock;
use tracing::{debug, info};
pub struct FilesystemStorage {
base_path: PathBuf,
metadata_cache: Arc<RwLock<HashMap<String, SkillMetadata>>>,
cache_hits: Arc<RwLock<usize>>,
cache_misses: Arc<RwLock<usize>>,
}
impl FilesystemStorage {
pub async fn new(base_path: PathBuf) -> Result<Self, ServiceError> {
fs::create_dir_all(&base_path).await.map_err(|e| {
ServiceError::Custom(format!("Failed to create storage directory: {}", e))
})?;
info!("Initialized filesystem storage at: {}", base_path.display());
Ok(Self {
base_path,
metadata_cache: Arc::new(RwLock::new(HashMap::new())),
cache_hits: Arc::new(RwLock::new(0)),
cache_misses: Arc::new(RwLock::new(0)),
})
}
fn get_skill_metadata_path(&self, skill_id: &str) -> PathBuf {
self.base_path.join(skill_id).join("metadata.json")
}
fn get_skill_content_path(&self, skill_id: &str) -> PathBuf {
self.base_path.join(skill_id).join("SKILL.md")
}
pub async fn load_skill_metadata(
&self,
skill_id: &str,
) -> Result<Option<SkillMetadata>, ServiceError> {
{
let cache = self.metadata_cache.read().await;
if let Some(metadata) = cache.get(skill_id) {
let mut hits = self.cache_hits.write().await;
*hits += 1;
return Ok(Some(metadata.clone()));
}
}
let mut misses = self.cache_misses.write().await;
*misses += 1;
let metadata_path = self.get_skill_metadata_path(skill_id);
if !metadata_path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&metadata_path)
.await
.map_err(|e| ServiceError::Custom(format!("Failed to read metadata file: {}", e)))?;
let metadata: SkillMetadata = serde_json::from_str(&content)
.map_err(|e| ServiceError::Custom(format!("Failed to parse metadata JSON: {}", e)))?;
{
let mut cache = self.metadata_cache.write().await;
cache.insert(skill_id.to_string(), metadata.clone());
}
Ok(Some(metadata))
}
pub async fn save_skill_metadata(
&self,
skill_id: &str,
metadata: &SkillMetadata,
) -> Result<(), ServiceError> {
let metadata_path = self.get_skill_metadata_path(skill_id);
if let Some(parent) = metadata_path.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
ServiceError::Custom(format!("Failed to create skill directory: {}", e))
})?;
}
let content = serde_json::to_string_pretty(metadata)
.map_err(|e| ServiceError::Custom(format!("Failed to serialize metadata: {}", e)))?;
fs::write(&metadata_path, &content)
.await
.map_err(|e| ServiceError::Custom(format!("Failed to write metadata file: {}", e)))?;
{
let mut cache = self.metadata_cache.write().await;
cache.insert(skill_id.to_string(), metadata.clone());
}
debug!("Saved metadata for skill: {}", skill_id);
Ok(())
}
pub async fn load_skill_content(&self, skill_id: &str) -> Result<Option<String>, ServiceError> {
let content_path = self.get_skill_content_path(skill_id);
if !content_path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&content_path)
.await
.map_err(|e| ServiceError::Custom(format!("Failed to read skill content: {}", e)))?;
Ok(Some(content))
}
pub async fn save_skill_content(
&self,
skill_id: &str,
content: &str,
) -> Result<(), ServiceError> {
let content_path = self.get_skill_content_path(skill_id);
if let Some(parent) = content_path.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
ServiceError::Custom(format!("Failed to create skill directory: {}", e))
})?;
}
fs::write(&content_path, content)
.await
.map_err(|e| ServiceError::Custom(format!("Failed to write skill content: {}", e)))?;
debug!("Saved content for skill: {}", skill_id);
Ok(())
}
pub async fn list_skill_ids(&self) -> Result<Vec<String>, ServiceError> {
let mut skill_ids = Vec::new();
let mut read_dir = fs::read_dir(&self.base_path).await.map_err(|e| {
ServiceError::Custom(format!("Failed to read storage directory: {}", e))
})?;
while let Some(entry) = read_dir
.next_entry()
.await
.map_err(|e| ServiceError::Custom(format!("Failed to read directory entry: {}", e)))?
{
let path = entry.path();
if path.is_dir() {
if let Some(skill_id) = path.file_name() {
let skill_file = path.join("SKILL.md");
if skill_file.exists() {
skill_ids.push(skill_id.to_string_lossy().to_string());
}
}
}
}
Ok(skill_ids)
}
pub async fn delete_skill(&self, skill_id: &str) -> Result<(), ServiceError> {
let skill_path = self.base_path.join(skill_id);
if skill_path.exists() {
fs::remove_dir_all(&skill_path).await.map_err(|e| {
ServiceError::Custom(format!("Failed to delete skill directory: {}", e))
})?;
{
let mut cache = self.metadata_cache.write().await;
cache.remove(skill_id);
}
debug!("Deleted skill: {}", skill_id);
}
Ok(())
}
pub async fn get_cache_stats(&self) -> (usize, usize, usize, usize) {
let cache_size = self.metadata_cache.read().await.len();
let hits = *self.cache_hits.read().await;
let misses = *self.cache_misses.read().await;
(cache_size, hits, misses, hits + misses)
}
pub async fn clear_cache(&self) {
self.metadata_cache.write().await.clear();
*self.cache_hits.write().await = 0;
*self.cache_misses.write().await = 0;
}
pub async fn get_storage_stats(&self) -> Result<StorageStats, ServiceError> {
let total_skills = self.list_skill_ids().await?.len();
let mut total_size = 0u64;
for skill_id in self.list_skill_ids().await? {
let metadata_path = self.get_skill_metadata_path(&skill_id);
let content_path = self.get_skill_content_path(&skill_id);
if let Ok(metadata) = fs::metadata(&metadata_path).await {
total_size += metadata.len();
}
if let Ok(metadata) = fs::metadata(&content_path).await {
total_size += metadata.len();
}
}
Ok(StorageStats {
total_skills,
total_size_bytes: total_size,
base_path: self.base_path.clone(),
})
}
}
#[derive(Debug, Clone)]
pub struct StorageStats {
pub total_skills: usize,
pub total_size_bytes: u64,
pub base_path: PathBuf,
}
#[async_trait]
impl crate::storage::StorageBackend for FilesystemStorage {
async fn initialize(&self) -> Result<(), ServiceError> {
fs::create_dir_all(&self.base_path).await.map_err(|e| {
ServiceError::Custom(format!("Failed to create storage directory: {}", e))
})?;
info!(
"Filesystem storage initialized at: {}",
self.base_path.display()
);
Ok(())
}
async fn clear_cache(&self) -> Result<(), ServiceError> {
self.clear_cache().await;
Ok(())
}
}