fastskill_core/storage/
filesystem.rs1use 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
14pub struct FilesystemStorage {
16 base_path: PathBuf,
18
19 metadata_cache: Arc<RwLock<HashMap<String, SkillMetadata>>>,
21
22 cache_hits: Arc<RwLock<usize>>,
24 cache_misses: Arc<RwLock<usize>>,
25}
26
27impl FilesystemStorage {
28 pub async fn new(base_path: PathBuf) -> Result<Self, ServiceError> {
30 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 fn get_skill_metadata_path(&self, skill_id: &str) -> PathBuf {
47 self.base_path.join(skill_id).join("metadata.json")
48 }
49
50 fn get_skill_content_path(&self, skill_id: &str) -> PathBuf {
52 self.base_path.join(skill_id).join("SKILL.md")
53 }
54
55 pub async fn load_skill_metadata(
57 &self,
58 skill_id: &str,
59 ) -> Result<Option<SkillMetadata>, ServiceError> {
60 {
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 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 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 {
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 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 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 let content = serde_json::to_string_pretty(metadata)
114 .map_err(|e| ServiceError::Custom(format!("Failed to serialize metadata: {}", e)))?;
115
116 fs::write(&metadata_path, &content)
118 .await
119 .map_err(|e| ServiceError::Custom(format!("Failed to write metadata file: {}", e)))?;
120
121 {
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 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 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 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 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 pub async fn list_skill_ids(&self) -> Result<Vec<String>, ServiceError> {
172 let mut skill_ids = Vec::new();
173
174 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 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 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 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 {
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 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 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 pub async fn get_storage_stats(&self) -> Result<StorageStats, ServiceError> {
240 let total_skills = self.list_skill_ids().await?.len();
241
242 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#[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 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}