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(
97 &self,
98 id: Uuid,
99 updater: impl FnOnce(&mut Reminder) -> Result<()>,
100 ) -> Result<bool> {
101 let mut reminders = self.load()?;
102
103 if let Some(reminder) = reminders.iter_mut().find(|r| r.id == id) {
104 updater(reminder)?;
105 self.save(&reminders)?;
106 Ok(true)
107 } else {
108 Ok(false)
109 }
110 }
111
112 pub fn get(&self, id: Uuid) -> Result<Option<Reminder>> {
113 let reminders = self.load()?;
114 Ok(reminders.into_iter().find(|r| r.id == id))
115 }
116
117 pub fn find_by_short_id(&self, short_id: &str) -> Result<Option<Reminder>> {
119 let reminders = self.load()?;
120 let matches: Vec<_> = reminders
121 .into_iter()
122 .filter(|r| r.id.to_string().starts_with(short_id))
123 .collect();
124
125 match matches.len() {
126 0 => Ok(None),
127 1 => Ok(Some(matches.into_iter().next().unwrap())),
128 _ => anyhow::bail!(
129 "Ambiguous ID '{}': matches {} reminders. Please use more characters.",
130 short_id,
131 matches.len()
132 ),
133 }
134 }
135
136 pub fn delete_by_short_id(&self, short_id: &str) -> Result<Option<Uuid>> {
138 let mut reminders = self.load()?;
139 let matches: Vec<_> = reminders
140 .iter()
141 .filter(|r| r.id.to_string().starts_with(short_id))
142 .map(|r| r.id)
143 .collect();
144
145 match matches.len() {
146 0 => Ok(None),
147 1 => {
148 let id = matches[0];
149 reminders.retain(|r| r.id != id);
150 self.save(&reminders)?;
151 Ok(Some(id))
152 }
153 _ => anyhow::bail!(
154 "Ambiguous ID '{}': matches {} reminders. Please use more characters.",
155 short_id,
156 matches.len()
157 ),
158 }
159 }
160
161 pub fn clean_completed(&self) -> Result<usize> {
163 let mut reminders = self.load()?;
164 let initial_len = reminders.len();
165 reminders.retain(|r| !r.completed);
166 let removed = initial_len - reminders.len();
167
168 if removed > 0 {
169 self.save(&reminders)?;
170 }
171
172 Ok(removed)
173 }
174
175 pub fn pid_file_path() -> Result<PathBuf> {
176 let data_dir = dirs::data_local_dir()
177 .context("Failed to get local data directory")?
178 .join("reminder-cli");
179
180 fs::create_dir_all(&data_dir)?;
181 Ok(data_dir.join("daemon.pid"))
182 }
183
184 pub fn log_file_path() -> Result<PathBuf> {
185 let data_dir = dirs::data_local_dir()
186 .context("Failed to get local data directory")?
187 .join("reminder-cli");
188
189 fs::create_dir_all(&data_dir)?;
190 Ok(data_dir.join("daemon.log"))
191 }
192
193 pub fn heartbeat_file_path() -> Result<PathBuf> {
194 let data_dir = dirs::data_local_dir()
195 .context("Failed to get local data directory")?
196 .join("reminder-cli");
197
198 fs::create_dir_all(&data_dir)?;
199 Ok(data_dir.join("daemon.heartbeat"))
200 }
201
202 pub fn filter_by_tag(&self, tag: &str) -> Result<Vec<Reminder>> {
204 let reminders = self.load()?;
205 Ok(reminders
206 .into_iter()
207 .filter(|r| r.tags.contains(tag))
208 .collect())
209 }
210
211 pub fn get_all_tags(&self) -> Result<Vec<String>> {
213 let reminders = self.load()?;
214 let mut tags: Vec<String> = reminders
215 .iter()
216 .flat_map(|r| r.tags.iter().cloned())
217 .collect();
218 tags.sort();
219 tags.dedup();
220 Ok(tags)
221 }
222
223 pub fn pause_by_short_id(&self, short_id: &str) -> Result<Option<Uuid>> {
225 let reminder = self.find_by_short_id(short_id)?;
226 if let Some(r) = reminder {
227 let id = r.id;
228 self.update(id, |rem| {
229 rem.pause();
230 Ok(())
231 })?;
232 Ok(Some(id))
233 } else {
234 Ok(None)
235 }
236 }
237
238 pub fn resume_by_short_id(&self, short_id: &str) -> Result<Option<Uuid>> {
240 let reminder = self.find_by_short_id(short_id)?;
241 if let Some(r) = reminder {
242 let id = r.id;
243 self.update(id, |rem| {
244 rem.resume();
245 Ok(())
246 })?;
247 Ok(Some(id))
248 } else {
249 Ok(None)
250 }
251 }
252
253 pub fn export_to_file(&self, path: &Path) -> Result<usize> {
255 let reminders = self.load()?;
256 let count = reminders.len();
257
258 let content = serde_json::to_string_pretty(&reminders)
259 .context("Failed to serialize reminders for export")?;
260
261 fs::write(path, content).context("Failed to write export file")?;
262
263 Ok(count)
264 }
265
266 pub fn import_from_file(&self, path: &Path, overwrite: bool) -> Result<(usize, usize)> {
269 let content = fs::read_to_string(path).context("Failed to read import file")?;
270
271 let imported: Vec<Reminder> =
272 serde_json::from_str(&content).context("Failed to parse import JSON")?;
273
274 let mut existing = self.load()?;
275 let existing_ids: std::collections::HashSet<Uuid> = existing.iter().map(|r| r.id).collect();
276
277 let mut imported_count = 0;
278 let mut skipped_count = 0;
279
280 for reminder in imported {
281 if existing_ids.contains(&reminder.id) {
282 if overwrite {
283 existing.retain(|r| r.id != reminder.id);
284 existing.push(reminder);
285 imported_count += 1;
286 } else {
287 skipped_count += 1;
288 }
289 } else {
290 existing.push(reminder);
291 imported_count += 1;
292 }
293 }
294
295 self.save(&existing)?;
296 Ok((imported_count, skipped_count))
297 }
298}