pocket_cli/storage/
mod.rs

1use crate::models::{Entry, Backpack, Config, ContentType, Workflow};
2use anyhow::{Result, Context, anyhow};
3use dirs::home_dir;
4use serde_json;
5use std::fs::{self, create_dir_all};
6use std::path::{Path, PathBuf};
7
8/// Storage manager for pocket data
9#[derive(Clone)]
10pub struct StorageManager {
11    base_path: PathBuf,
12}
13
14impl StorageManager {
15    /// Create a new storage manager
16    pub fn new() -> Result<Self> {
17        let base_path = Self::get_base_path()?;
18        Ok(Self { base_path })
19    }
20
21    /// Get the base path for pocket data
22    fn get_base_path() -> Result<PathBuf> {
23        let home = home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
24        let pocket_dir = home.join(".pocket");
25        
26        // Create directories if they don't exist
27        create_dir_all(&pocket_dir.join("data/entries"))?;
28        create_dir_all(&pocket_dir.join("data/backpacks"))?;
29        create_dir_all(&pocket_dir.join("data/workflows"))?;
30        create_dir_all(&pocket_dir.join("wallet"))?;
31        
32        Ok(pocket_dir)
33    }
34
35    /// Get the workflows directory path
36    pub fn get_workflows_dir(&self) -> Result<PathBuf> {
37        let workflows_dir = self.base_path.join("data/workflows");
38        if !workflows_dir.exists() {
39            create_dir_all(&workflows_dir)?;
40        }
41        Ok(workflows_dir)
42    }
43
44    /// Get the path for an entry's metadata
45    fn get_entry_metadata_path(&self, id: &str, backpack: Option<&str>) -> PathBuf {
46        match backpack {
47            Some(name) => self.base_path.join(format!("data/backpacks/{}/entries/{}.json", name, id)),
48            None => self.base_path.join(format!("data/entries/{}.json", id)),
49        }
50    }
51
52    /// Get the path for an entry's content
53    fn get_entry_content_path(&self, id: &str, backpack: Option<&str>) -> PathBuf {
54        match backpack {
55            Some(name) => self.base_path.join(format!("data/backpacks/{}/entries/{}.content", name, id)),
56            None => self.base_path.join(format!("data/entries/{}.content", id)),
57        }
58    }
59
60    /// Get the path for a backpack's metadata
61    fn get_backpack_path(&self, name: &str) -> PathBuf {
62        self.base_path.join(format!("data/backpacks/{}/manifest.json", name))
63    }
64
65    /// Get the config file path
66    fn get_config_path(&self) -> PathBuf {
67        self.base_path.join("config.toml")
68    }
69
70    /// Get the path for a workflow
71    fn get_workflow_path(&self, name: &str) -> PathBuf {
72        self.base_path.join(format!("data/workflows/{}.workflow", name))
73    }
74
75    /// Save an entry to storage
76    pub fn save_entry(&self, entry: &Entry, content: &str, backpack: Option<&str>) -> Result<()> {
77        // Create backpack directory if needed
78        if let Some(name) = backpack {
79            create_dir_all(self.base_path.join(format!("data/backpacks/{}/entries", name)))?;
80        }
81
82        // Save metadata
83        let metadata_path = self.get_entry_metadata_path(&entry.id, backpack);
84        let metadata_json = serde_json::to_string_pretty(entry)?;
85        fs::write(metadata_path, metadata_json)?;
86
87        // Save content
88        let content_path = self.get_entry_content_path(&entry.id, backpack);
89        fs::write(content_path, content)?;
90
91        Ok(())
92    }
93
94    /// Load an entry from storage
95    pub fn load_entry(&self, id: &str, backpack: Option<&str>) -> Result<(Entry, String)> {
96        // Load metadata
97        let metadata_path = self.get_entry_metadata_path(id, backpack);
98        let metadata_json = fs::read_to_string(&metadata_path)
99            .with_context(|| format!("Failed to read entry metadata from {}", metadata_path.display()))?;
100        let entry: Entry = serde_json::from_str(&metadata_json)
101            .with_context(|| format!("Failed to parse entry metadata from {}", metadata_path.display()))?;
102
103        // Load content
104        let content_path = self.get_entry_content_path(id, backpack);
105        let content = fs::read_to_string(&content_path)
106            .with_context(|| format!("Failed to read entry content from {}", content_path.display()))?;
107
108        Ok((entry, content))
109    }
110
111    /// Remove an entry from storage
112    pub fn remove_entry(&self, id: &str, backpack: Option<&str>) -> Result<()> {
113        // Remove metadata
114        let metadata_path = self.get_entry_metadata_path(id, backpack);
115        if metadata_path.exists() {
116            fs::remove_file(&metadata_path)?;
117        }
118
119        // Remove content
120        let content_path = self.get_entry_content_path(id, backpack);
121        if content_path.exists() {
122            fs::remove_file(&content_path)?;
123        }
124
125        Ok(())
126    }
127
128    /// List all entries in a backpack or the general pocket
129    pub fn list_entries(&self, backpack: Option<&str>) -> Result<Vec<Entry>> {
130        let entries_dir = match backpack {
131            Some(name) => self.base_path.join(format!("data/backpacks/{}/entries", name)),
132            None => self.base_path.join("data/entries"),
133        };
134
135        if !entries_dir.exists() {
136            return Ok(Vec::new());
137        }
138
139        let mut entries = Vec::new();
140        for entry in fs::read_dir(entries_dir)? {
141            let entry = entry?;
142            let path = entry.path();
143            
144            // Only process JSON files (metadata)
145            if path.is_file() && path.extension().map_or(false, |ext| ext == "json") {
146                let metadata_json = fs::read_to_string(&path)?;
147                let entry: Entry = serde_json::from_str(&metadata_json)?;
148                entries.push(entry);
149            }
150        }
151
152        // Sort by creation date (newest first)
153        entries.sort_by(|a, b| b.created_at.cmp(&a.created_at));
154        
155        Ok(entries)
156    }
157
158    /// Create a new backpack
159    pub fn create_backpack(&self, backpack: &Backpack) -> Result<()> {
160        // Create backpack directory
161        let backpack_dir = self.base_path.join(format!("data/backpacks/{}", backpack.name));
162        create_dir_all(&backpack_dir.join("entries"))?;
163
164        // Save backpack metadata
165        let manifest_path = self.get_backpack_path(&backpack.name);
166        let manifest_json = serde_json::to_string_pretty(backpack)?;
167        fs::write(manifest_path, manifest_json)?;
168
169        Ok(())
170    }
171
172    /// List all backpacks
173    pub fn list_backpacks(&self) -> Result<Vec<Backpack>> {
174        let backpacks_dir = self.base_path.join("data/backpacks");
175        
176        if !backpacks_dir.exists() {
177            return Ok(Vec::new());
178        }
179
180        let mut backpacks = Vec::new();
181        for entry in fs::read_dir(backpacks_dir)? {
182            let entry = entry?;
183            let path = entry.path();
184            
185            if path.is_dir() {
186                let manifest_path = path.join("manifest.json");
187                if manifest_path.exists() {
188                    let manifest_json = fs::read_to_string(&manifest_path)?;
189                    let backpack: Backpack = serde_json::from_str(&manifest_json)?;
190                    backpacks.push(backpack);
191                }
192            }
193        }
194
195        // Sort by name
196        backpacks.sort_by(|a, b| a.name.cmp(&b.name));
197        
198        Ok(backpacks)
199    }
200
201    /// Load the configuration
202    pub fn load_config(&self) -> Result<Config> {
203        let config_path = self.get_config_path();
204        
205        if !config_path.exists() {
206            // Create default config if it doesn't exist
207            let config = Config::default();
208            self.save_config(&config)?;
209            return Ok(config);
210        }
211
212        let config_str = fs::read_to_string(config_path)?;
213        let config: Config = toml::from_str(&config_str)?;
214        
215        Ok(config)
216    }
217
218    /// Save the configuration
219    pub fn save_config(&self, config: &Config) -> Result<()> {
220        let config_path = self.get_config_path();
221        let config_str = toml::to_string_pretty(config)?;
222        fs::write(config_path, config_str)?;
223        
224        Ok(())
225    }
226
227    /// Determine the content type based on file extension
228    pub fn determine_content_type(path: &Path) -> ContentType {
229        match path.extension().and_then(|ext| ext.to_str()) {
230            Some("rs" | "go" | "js" | "ts" | "py" | "java" | "c" | "cpp" | "h" | "hpp" | "cs" | 
231                 "php" | "rb" | "swift" | "kt" | "scala" | "sh" | "bash" | "pl" | "sql" | "html" | 
232                 "css" | "scss" | "sass" | "less" | "jsx" | "tsx" | "vue" | "json" | "yaml" | "yml" | 
233                 "toml" | "xml" | "md" | "markdown") => ContentType::Code,
234            _ => ContentType::Text,
235        }
236    }
237
238    /// Save a workflow
239    pub fn save_workflow(&self, workflow: &Workflow) -> Result<()> {
240        let workflow_path = self.get_workflow_path(&workflow.name);
241        println!("Saving workflow to: {}", workflow_path.display());
242        
243        // Create workflows directory if it doesn't exist
244        if let Some(parent) = workflow_path.parent() {
245            println!("Creating directory: {}", parent.display());
246            create_dir_all(parent)?;
247        }
248        
249        // Save workflow
250        let workflow_json = serde_json::to_string_pretty(workflow)?;
251        println!("Writing workflow JSON: {}", workflow_json);
252        fs::write(workflow_path, workflow_json)?;
253        
254        Ok(())
255    }
256
257    /// Load a workflow
258    pub fn load_workflow(&self, name: &str) -> Result<Workflow> {
259        let workflow_path = self.get_workflow_path(name);
260        let workflow_json = fs::read_to_string(&workflow_path)
261            .with_context(|| format!("Failed to read workflow '{}'", name))?;
262        let workflow: Workflow = serde_json::from_str(&workflow_json)
263            .with_context(|| format!("Failed to parse workflow '{}'", name))?;
264        Ok(workflow)
265    }
266
267    /// Delete a workflow
268    pub fn delete_workflow(&self, name: &str) -> Result<()> {
269        let workflow_path = self.get_workflow_path(name);
270        if workflow_path.exists() {
271            fs::remove_file(&workflow_path)?;
272            Ok(())
273        } else {
274            Err(anyhow!("Workflow '{}' not found", name))
275        }
276    }
277
278    /// List all workflows
279    pub fn list_workflows(&self) -> Result<Vec<Workflow>> {
280        let workflows_dir = self.base_path.join("data/workflows");
281        
282        if !workflows_dir.exists() {
283            return Ok(Vec::new());
284        }
285
286        let mut workflows = Vec::new();
287        for entry in fs::read_dir(workflows_dir)? {
288            let entry = entry?;
289            let path = entry.path();
290            
291            if path.is_file() && path.extension().map_or(false, |ext| ext == "workflow") {
292                let workflow_json = fs::read_to_string(&path)?;
293                let workflow: Workflow = serde_json::from_str(&workflow_json)?;
294                workflows.push(workflow);
295            }
296        }
297
298        workflows.sort_by(|a, b| a.name.cmp(&b.name));
299        Ok(workflows)
300    }
301
302    /// Search for entries by query string
303    pub fn search_entries(&self, query: &str, backpack: Option<&str>, limit: usize) -> Result<Vec<(Entry, String)>> {
304        let mut results = Vec::new();
305        
306        // Get entries to search
307        let entries = self.list_entries(backpack)?;
308        
309        // Simple case-insensitive search
310        let query_lower = query.to_lowercase();
311        
312        for entry in entries {
313            // Load the content
314            let content = match fs::read_to_string(self.get_entry_content_path(&entry.id, backpack)) {
315                Ok(content) => content,
316                Err(_) => continue, // Skip entries with missing content
317            };
318            
319            // Check if query matches title or content
320            if entry.title.to_lowercase().contains(&query_lower) || 
321               content.to_lowercase().contains(&query_lower) {
322                results.push((entry, content));
323                
324                // Check if we've reached the limit
325                if results.len() >= limit {
326                    break;
327                }
328            }
329        }
330        
331        Ok(results)
332    }
333    
334    /// Load entry content only
335    pub fn load_entry_content(&self, id: &str, backpack: Option<&str>) -> Result<String> {
336        let content_path = self.get_entry_content_path(id, backpack);
337        fs::read_to_string(&content_path)
338            .with_context(|| format!("Failed to read entry content from {}", content_path.display()))
339    }
340}