Skip to main content

fastskill_core/storage/
filesystem.rs

1//! Filesystem storage backend
2
3use crate::core::metadata::SkillMetadata;
4use crate::core::service::ServiceError;
5use async_trait::async_trait;
6use serde_json;
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10use tokio::fs;
11use tokio::sync::RwLock;
12use tracing::{debug, info};
13
14/// Filesystem storage backend for skills
15pub struct FilesystemStorage {
16    /// Base directory for skill storage
17    base_path: PathBuf,
18
19    /// Metadata cache for performance
20    metadata_cache: Arc<RwLock<HashMap<String, SkillMetadata>>>,
21
22    /// Cache statistics
23    cache_hits: Arc<RwLock<usize>>,
24    cache_misses: Arc<RwLock<usize>>,
25}
26
27impl FilesystemStorage {
28    /// Create a new filesystem storage backend
29    pub async fn new(base_path: PathBuf) -> Result<Self, ServiceError> {
30        // Create base directory if it doesn't exist
31        fs::create_dir_all(&base_path).await.map_err(|e| {
32            ServiceError::Custom(format!("Failed to create storage directory: {}", e))
33        })?;
34
35        info!("Initialized filesystem storage at: {}", base_path.display());
36
37        Ok(Self {
38            base_path,
39            metadata_cache: Arc::new(RwLock::new(HashMap::new())),
40            cache_hits: Arc::new(RwLock::new(0)),
41            cache_misses: Arc::new(RwLock::new(0)),
42        })
43    }
44
45    /// Get the path for a skill's metadata file
46    fn get_skill_metadata_path(&self, skill_id: &str) -> PathBuf {
47        self.base_path.join(skill_id).join("metadata.json")
48    }
49
50    /// Get the path for a skill's SKILL.md file
51    fn get_skill_content_path(&self, skill_id: &str) -> PathBuf {
52        self.base_path.join(skill_id).join("SKILL.md")
53    }
54
55    /// Load skill metadata from disk or cache
56    pub async fn load_skill_metadata(
57        &self,
58        skill_id: &str,
59    ) -> Result<Option<SkillMetadata>, ServiceError> {
60        // Check cache first
61        {
62            let cache = self.metadata_cache.read().await;
63            if let Some(metadata) = cache.get(skill_id) {
64                let mut hits = self.cache_hits.write().await;
65                *hits += 1;
66                return Ok(Some(metadata.clone()));
67            }
68        }
69
70        // Cache miss - load from disk
71        let mut misses = self.cache_misses.write().await;
72        *misses += 1;
73
74        let metadata_path = self.get_skill_metadata_path(skill_id);
75
76        if !metadata_path.exists() {
77            return Ok(None);
78        }
79
80        // Read and parse metadata file
81        let content = fs::read_to_string(&metadata_path)
82            .await
83            .map_err(|e| ServiceError::Custom(format!("Failed to read metadata file: {}", e)))?;
84
85        let metadata: SkillMetadata = serde_json::from_str(&content)
86            .map_err(|e| ServiceError::Custom(format!("Failed to parse metadata JSON: {}", e)))?;
87
88        // Cache the metadata
89        {
90            let mut cache = self.metadata_cache.write().await;
91            cache.insert(skill_id.to_string(), metadata.clone());
92        }
93
94        Ok(Some(metadata))
95    }
96
97    /// Save skill metadata to disk and cache
98    pub async fn save_skill_metadata(
99        &self,
100        skill_id: &str,
101        metadata: &SkillMetadata,
102    ) -> Result<(), ServiceError> {
103        let metadata_path = self.get_skill_metadata_path(skill_id);
104
105        // Create skill directory if it doesn't exist
106        if let Some(parent) = metadata_path.parent() {
107            fs::create_dir_all(parent).await.map_err(|e| {
108                ServiceError::Custom(format!("Failed to create skill directory: {}", e))
109            })?;
110        }
111
112        // Serialize metadata to JSON
113        let content = serde_json::to_string_pretty(metadata)
114            .map_err(|e| ServiceError::Custom(format!("Failed to serialize metadata: {}", e)))?;
115
116        // Write to disk
117        fs::write(&metadata_path, &content)
118            .await
119            .map_err(|e| ServiceError::Custom(format!("Failed to write metadata file: {}", e)))?;
120
121        // Update cache
122        {
123            let mut cache = self.metadata_cache.write().await;
124            cache.insert(skill_id.to_string(), metadata.clone());
125        }
126
127        debug!("Saved metadata for skill: {}", skill_id);
128        Ok(())
129    }
130
131    /// Load skill content (SKILL.md)
132    pub async fn load_skill_content(&self, skill_id: &str) -> Result<Option<String>, ServiceError> {
133        let content_path = self.get_skill_content_path(skill_id);
134
135        if !content_path.exists() {
136            return Ok(None);
137        }
138
139        let content = fs::read_to_string(&content_path)
140            .await
141            .map_err(|e| ServiceError::Custom(format!("Failed to read skill content: {}", e)))?;
142
143        Ok(Some(content))
144    }
145
146    /// Save skill content (SKILL.md)
147    pub async fn save_skill_content(
148        &self,
149        skill_id: &str,
150        content: &str,
151    ) -> Result<(), ServiceError> {
152        let content_path = self.get_skill_content_path(skill_id);
153
154        // Create skill directory if it doesn't exist
155        if let Some(parent) = content_path.parent() {
156            fs::create_dir_all(parent).await.map_err(|e| {
157                ServiceError::Custom(format!("Failed to create skill directory: {}", e))
158            })?;
159        }
160
161        // Write content to disk
162        fs::write(&content_path, content)
163            .await
164            .map_err(|e| ServiceError::Custom(format!("Failed to write skill content: {}", e)))?;
165
166        debug!("Saved content for skill: {}", skill_id);
167        Ok(())
168    }
169
170    /// List all skill IDs in storage
171    pub async fn list_skill_ids(&self) -> Result<Vec<String>, ServiceError> {
172        let mut skill_ids = Vec::new();
173
174        // Read base directory
175        let mut read_dir = fs::read_dir(&self.base_path).await.map_err(|e| {
176            ServiceError::Custom(format!("Failed to read storage directory: {}", e))
177        })?;
178
179        // Collect all subdirectories (skill directories)
180        while let Some(entry) = read_dir
181            .next_entry()
182            .await
183            .map_err(|e| ServiceError::Custom(format!("Failed to read directory entry: {}", e)))?
184        {
185            let path = entry.path();
186
187            if path.is_dir() {
188                if let Some(skill_id) = path.file_name() {
189                    // Check if this directory has a valid skill structure
190                    let skill_file = path.join("SKILL.md");
191                    if skill_file.exists() {
192                        skill_ids.push(skill_id.to_string_lossy().to_string());
193                    }
194                }
195            }
196        }
197
198        Ok(skill_ids)
199    }
200
201    /// Delete a skill from storage
202    pub async fn delete_skill(&self, skill_id: &str) -> Result<(), ServiceError> {
203        let skill_path = self.base_path.join(skill_id);
204
205        if skill_path.exists() {
206            fs::remove_dir_all(&skill_path).await.map_err(|e| {
207                ServiceError::Custom(format!("Failed to delete skill directory: {}", e))
208            })?;
209
210            // Remove from cache
211            {
212                let mut cache = self.metadata_cache.write().await;
213                cache.remove(skill_id);
214            }
215
216            debug!("Deleted skill: {}", skill_id);
217        }
218
219        Ok(())
220    }
221
222    /// Get cache statistics
223    pub async fn get_cache_stats(&self) -> (usize, usize, usize, usize) {
224        let cache_size = self.metadata_cache.read().await.len();
225        let hits = *self.cache_hits.read().await;
226        let misses = *self.cache_misses.read().await;
227
228        (cache_size, hits, misses, hits + misses)
229    }
230
231    /// Clear metadata cache
232    pub async fn clear_cache(&self) {
233        self.metadata_cache.write().await.clear();
234        *self.cache_hits.write().await = 0;
235        *self.cache_misses.write().await = 0;
236    }
237
238    /// Get storage statistics
239    pub async fn get_storage_stats(&self) -> Result<StorageStats, ServiceError> {
240        let total_skills = self.list_skill_ids().await?.len();
241
242        // Calculate total size (simplified)
243        let mut total_size = 0u64;
244        for skill_id in self.list_skill_ids().await? {
245            let metadata_path = self.get_skill_metadata_path(&skill_id);
246            let content_path = self.get_skill_content_path(&skill_id);
247
248            if let Ok(metadata) = fs::metadata(&metadata_path).await {
249                total_size += metadata.len();
250            }
251
252            if let Ok(metadata) = fs::metadata(&content_path).await {
253                total_size += metadata.len();
254            }
255        }
256
257        Ok(StorageStats {
258            total_skills,
259            total_size_bytes: total_size,
260            base_path: self.base_path.clone(),
261        })
262    }
263}
264
265/// Storage statistics
266#[derive(Debug, Clone)]
267pub struct StorageStats {
268    pub total_skills: usize,
269    pub total_size_bytes: u64,
270    pub base_path: PathBuf,
271}
272
273#[async_trait]
274impl crate::storage::StorageBackend for FilesystemStorage {
275    async fn initialize(&self) -> Result<(), ServiceError> {
276        // Create base directory if it doesn't exist
277        fs::create_dir_all(&self.base_path).await.map_err(|e| {
278            ServiceError::Custom(format!("Failed to create storage directory: {}", e))
279        })?;
280
281        info!(
282            "Filesystem storage initialized at: {}",
283            self.base_path.display()
284        );
285        Ok(())
286    }
287
288    async fn clear_cache(&self) -> Result<(), ServiceError> {
289        self.clear_cache().await;
290        Ok(())
291    }
292}