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#[derive(Clone)]
10pub struct StorageManager {
11 base_path: PathBuf,
12}
13
14impl StorageManager {
15 pub fn new() -> Result<Self> {
17 let base_path = Self::get_base_path()?;
18 Ok(Self { base_path })
19 }
20
21 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_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 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 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 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 fn get_backpack_path(&self, name: &str) -> PathBuf {
62 self.base_path.join(format!("data/backpacks/{}/manifest.json", name))
63 }
64
65 fn get_config_path(&self) -> PathBuf {
67 self.base_path.join("config.toml")
68 }
69
70 fn get_workflow_path(&self, name: &str) -> PathBuf {
72 self.base_path.join(format!("data/workflows/{}.workflow", name))
73 }
74
75 pub fn save_entry(&self, entry: &Entry, content: &str, backpack: Option<&str>) -> Result<()> {
77 if let Some(name) = backpack {
79 create_dir_all(self.base_path.join(format!("data/backpacks/{}/entries", name)))?;
80 }
81
82 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 let content_path = self.get_entry_content_path(&entry.id, backpack);
89 fs::write(content_path, content)?;
90
91 Ok(())
92 }
93
94 pub fn load_entry(&self, id: &str, backpack: Option<&str>) -> Result<(Entry, String)> {
96 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 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 pub fn remove_entry(&self, id: &str, backpack: Option<&str>) -> Result<()> {
113 let metadata_path = self.get_entry_metadata_path(id, backpack);
115 if metadata_path.exists() {
116 fs::remove_file(&metadata_path)?;
117 }
118
119 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 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 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 entries.sort_by(|a, b| b.created_at.cmp(&a.created_at));
154
155 Ok(entries)
156 }
157
158 pub fn create_backpack(&self, backpack: &Backpack) -> Result<()> {
160 let backpack_dir = self.base_path.join(format!("data/backpacks/{}", backpack.name));
162 create_dir_all(&backpack_dir.join("entries"))?;
163
164 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 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 backpacks.sort_by(|a, b| a.name.cmp(&b.name));
197
198 Ok(backpacks)
199 }
200
201 pub fn load_config(&self) -> Result<Config> {
203 let config_path = self.get_config_path();
204
205 if !config_path.exists() {
206 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 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 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 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 if let Some(parent) = workflow_path.parent() {
245 println!("Creating directory: {}", parent.display());
246 create_dir_all(parent)?;
247 }
248
249 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 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 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 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 pub fn search_entries(&self, query: &str, backpack: Option<&str>, limit: usize) -> Result<Vec<(Entry, String)>> {
304 let mut results = Vec::new();
305
306 let entries = self.list_entries(backpack)?;
308
309 let query_lower = query.to_lowercase();
311
312 for entry in entries {
313 let content = match fs::read_to_string(self.get_entry_content_path(&entry.id, backpack)) {
315 Ok(content) => content,
316 Err(_) => continue, };
318
319 if entry.title.to_lowercase().contains(&query_lower) ||
321 content.to_lowercase().contains(&query_lower) {
322 results.push((entry, content));
323
324 if results.len() >= limit {
326 break;
327 }
328 }
329 }
330
331 Ok(results)
332 }
333
334 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}