1use crate::reminder::Reminder;
2use anyhow::{Context, Result};
3use fs2::FileExt;
4use std::fs;
5use std::fs::{File, OpenOptions};
6use std::io::{Read, Write};
7use std::path::{Path, PathBuf};
8use uuid::Uuid;
9
10pub struct Storage {
11 path: PathBuf,
12}
13
14impl Storage {
15 pub fn new() -> Result<Self> {
16 let data_dir = dirs::data_local_dir()
17 .context("Failed to get local data directory")?
18 .join("reminder-cli");
19
20 fs::create_dir_all(&data_dir)?;
21
22 Ok(Self {
23 path: data_dir.join("reminders.json"),
24 })
25 }
26
27 pub fn load(&self) -> Result<Vec<Reminder>> {
28 if !self.path.exists() {
29 return Ok(Vec::new());
30 }
31
32 let file = File::open(&self.path).context("Failed to open reminders file")?;
33 file.lock_shared().context("Failed to acquire read lock")?;
34
35 let mut content = String::new();
36 let mut reader = &file;
37 reader
38 .read_to_string(&mut content)
39 .context("Failed to read reminders file")?;
40
41 file.unlock().context("Failed to release lock")?;
42
43 if content.trim().is_empty() {
44 return Ok(Vec::new());
45 }
46
47 let reminders: Vec<Reminder> =
48 serde_json::from_str(&content).context("Failed to parse reminders JSON")?;
49
50 Ok(reminders)
51 }
52
53 pub fn save(&self, reminders: &[Reminder]) -> Result<()> {
54 let file = OpenOptions::new()
55 .write(true)
56 .create(true)
57 .truncate(true)
58 .open(&self.path)
59 .context("Failed to open reminders file for writing")?;
60
61 file.lock_exclusive()
62 .context("Failed to acquire write lock")?;
63
64 let content =
65 serde_json::to_string_pretty(reminders).context("Failed to serialize reminders")?;
66
67 let mut writer = &file;
68 writer
69 .write_all(content.as_bytes())
70 .context("Failed to write reminders file")?;
71
72 file.unlock().context("Failed to release lock")?;
73
74 Ok(())
75 }
76
77 pub fn add(&self, reminder: Reminder) -> Result<()> {
78 let mut reminders = self.load()?;
79 reminders.push(reminder);
80 self.save(&reminders)
81 }
82
83 pub fn delete(&self, id: Uuid) -> Result<bool> {
84 let mut reminders = self.load()?;
85 let initial_len = reminders.len();
86 reminders.retain(|r| r.id != id);
87
88 if reminders.len() == initial_len {
89 return Ok(false);
90 }
91
92 self.save(&reminders)?;
93 Ok(true)
94 }
95
96 pub fn update(&self, id: Uuid, updater: impl FnOnce(&mut Reminder)) -> Result<bool> {
97 let mut reminders = self.load()?;
98
99 if let Some(reminder) = reminders.iter_mut().find(|r| r.id == id) {
100 updater(reminder);
101 self.save(&reminders)?;
102 Ok(true)
103 } else {
104 Ok(false)
105 }
106 }
107
108 pub fn get(&self, id: Uuid) -> Result<Option<Reminder>> {
109 let reminders = self.load()?;
110 Ok(reminders.into_iter().find(|r| r.id == id))
111 }
112
113 pub fn find_by_short_id(&self, short_id: &str) -> Result<Option<Reminder>> {
115 let reminders = self.load()?;
116 let matches: Vec<_> = reminders
117 .into_iter()
118 .filter(|r| r.id.to_string().starts_with(short_id))
119 .collect();
120
121 match matches.len() {
122 0 => Ok(None),
123 1 => Ok(Some(matches.into_iter().next().unwrap())),
124 _ => anyhow::bail!(
125 "Ambiguous ID '{}': matches {} reminders. Please use more characters.",
126 short_id,
127 matches.len()
128 ),
129 }
130 }
131
132 pub fn delete_by_short_id(&self, short_id: &str) -> Result<Option<Uuid>> {
134 let mut reminders = self.load()?;
135 let matches: Vec<_> = reminders
136 .iter()
137 .filter(|r| r.id.to_string().starts_with(short_id))
138 .map(|r| r.id)
139 .collect();
140
141 match matches.len() {
142 0 => Ok(None),
143 1 => {
144 let id = matches[0];
145 reminders.retain(|r| r.id != id);
146 self.save(&reminders)?;
147 Ok(Some(id))
148 }
149 _ => anyhow::bail!(
150 "Ambiguous ID '{}': matches {} reminders. Please use more characters.",
151 short_id,
152 matches.len()
153 ),
154 }
155 }
156
157 pub fn clean_completed(&self) -> Result<usize> {
159 let mut reminders = self.load()?;
160 let initial_len = reminders.len();
161 reminders.retain(|r| !r.completed);
162 let removed = initial_len - reminders.len();
163
164 if removed > 0 {
165 self.save(&reminders)?;
166 }
167
168 Ok(removed)
169 }
170
171 pub fn pid_file_path() -> Result<PathBuf> {
172 let data_dir = dirs::data_local_dir()
173 .context("Failed to get local data directory")?
174 .join("reminder-cli");
175
176 fs::create_dir_all(&data_dir)?;
177 Ok(data_dir.join("daemon.pid"))
178 }
179
180 pub fn log_file_path() -> Result<PathBuf> {
181 let data_dir = dirs::data_local_dir()
182 .context("Failed to get local data directory")?
183 .join("reminder-cli");
184
185 fs::create_dir_all(&data_dir)?;
186 Ok(data_dir.join("daemon.log"))
187 }
188
189 pub fn export_to_file(&self, path: &Path) -> Result<usize> {
191 let reminders = self.load()?;
192 let count = reminders.len();
193
194 let content = serde_json::to_string_pretty(&reminders)
195 .context("Failed to serialize reminders for export")?;
196
197 fs::write(path, content).context("Failed to write export file")?;
198
199 Ok(count)
200 }
201
202 pub fn import_from_file(&self, path: &Path, overwrite: bool) -> Result<(usize, usize)> {
205 let content = fs::read_to_string(path).context("Failed to read import file")?;
206
207 let imported: Vec<Reminder> =
208 serde_json::from_str(&content).context("Failed to parse import JSON")?;
209
210 let mut existing = self.load()?;
211 let existing_ids: std::collections::HashSet<Uuid> = existing.iter().map(|r| r.id).collect();
212
213 let mut imported_count = 0;
214 let mut skipped_count = 0;
215
216 for reminder in imported {
217 if existing_ids.contains(&reminder.id) {
218 if overwrite {
219 existing.retain(|r| r.id != reminder.id);
220 existing.push(reminder);
221 imported_count += 1;
222 } else {
223 skipped_count += 1;
224 }
225 } else {
226 existing.push(reminder);
227 imported_count += 1;
228 }
229 }
230
231 self.save(&existing)?;
232 Ok((imported_count, skipped_count))
233 }
234}