1use std::error::Error;
6use std::path::{Path, PathBuf};
7
8use chrono::{DateTime, Local};
9use icalendar::{Calendar, CalendarComponent, Component};
10use tokio::fs;
11use uuid::Uuid;
12
13use crate::event::ParsedEventConditions;
14use crate::localdb::LocalDb;
15use crate::short_id::ShortIds;
16use crate::todo::{ParsedTodoConditions, ParsedTodoSort};
17use crate::{
18 Config, Event, EventConditions, EventDraft, EventPatch, Id, Kind, Pager, Todo, TodoConditions,
19 TodoDraft, TodoPatch, TodoSort,
20};
21
22#[derive(Debug, Clone)]
24pub struct Aim {
25 now: DateTime<Local>,
26 config: Config,
27 db: LocalDb,
28 short_ids: ShortIds,
29 calendar_path: PathBuf,
30}
31
32impl Aim {
33 pub async fn new(mut config: Config) -> Result<Self, Box<dyn Error>> {
35 let now = Local::now();
36
37 config.normalize()?;
38 prepare(&config).await?;
39
40 let db = LocalDb::open(&config.state_dir)
41 .await
42 .map_err(|e| format!("Failed to initialize db: {e}"))?;
43
44 let short_ids = ShortIds::new(db.clone());
45 let calendar_path = config.calendar_path.clone();
46 let that = Self {
47 now,
48 config,
49 db,
50 short_ids,
51 calendar_path,
52 };
53 that.add_calendar(&that.calendar_path)
54 .await
55 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
56
57 Ok(that)
58 }
59
60 pub fn now(&self) -> DateTime<Local> {
62 self.now
63 }
64
65 pub fn refresh_now(&mut self) {
67 self.now = Local::now();
68 }
69
70 pub fn default_event_draft(&self) -> EventDraft {
72 EventDraft::default(self.now)
73 }
74
75 pub async fn new_event(
77 &self,
78 draft: EventDraft,
79 ) -> Result<impl Event + 'static, Box<dyn Error>> {
80 let uid = self.generate_uid(Kind::Event).await?;
81 let event = draft.into_ics(&self.now, &uid);
82 let path = self.get_path(&uid);
83
84 let calendar = Calendar::new().push(event.clone()).done();
85 fs::write(&path, calendar.to_string())
86 .await
87 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
88
89 self.db.upsert_event(&path, &event).await?;
90
91 let todo = self.short_ids.event(event).await?;
92 Ok(todo)
93 }
94
95 pub async fn update_event(
97 &self,
98 id: &Id,
99 patch: EventPatch,
100 ) -> Result<impl Event + 'static, Box<dyn Error>> {
101 let uid = self.short_ids.get_uid(id).await?;
102 let event = match self.db.events.get(&uid).await? {
103 Some(todo) => todo,
104 None => return Err("Todo not found".into()),
105 };
106
107 let path: PathBuf = event.path().into();
108 let mut calendar = parse_ics(&path).await?;
109 let t = calendar
110 .components
111 .iter_mut()
112 .filter_map(|a| match a {
113 CalendarComponent::Event(a) => Some(a),
114 _ => None,
115 })
116 .find(|a| a.get_uid() == Some(event.uid()))
117 .ok_or("Event not found in calendar")?;
118
119 patch.apply_to(t);
120 let todo = t.clone();
121 fs::write(&path, calendar.done().to_string())
122 .await
123 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
124
125 self.db.upsert_event(&path, &todo).await?;
126
127 let todo = self.short_ids.event(todo).await?;
128 Ok(todo)
129 }
130
131 pub async fn get_kind(&self, id: &Id) -> Result<Kind, Box<dyn Error>> {
133 tracing::debug!(?id, "getting kind of id");
134 if let Some(data) = self.short_ids.get(id).await? {
135 return Ok(data.kind);
136 }
137
138 let uid = id.as_uid();
139
140 tracing::debug!(uid, "checking if id is an event");
141 if self.db.events.get(uid).await?.is_some() {
142 return Ok(Kind::Event);
143 }
144
145 tracing::debug!(uid, "checking if id is a todo");
146 if self.db.todos.get(uid).await?.is_some() {
147 return Ok(Kind::Todo);
148 }
149
150 Err("Id not found".into())
151 }
152
153 pub async fn get_event(&self, id: &Id) -> Result<impl Event + 'static, Box<dyn Error>> {
155 let uid = self.short_ids.get_uid(id).await?;
156 match self.db.events.get(&uid).await {
157 Ok(Some(event)) => Ok(self.short_ids.event(event).await?),
158 Ok(None) => Err("Event not found".into()),
159 Err(e) => Err(e.into()),
160 }
161 }
162
163 pub async fn list_events(
165 &self,
166 conds: &EventConditions,
167 pager: &Pager,
168 ) -> Result<Vec<impl Event + 'static>, Box<dyn Error>> {
169 let conds = ParsedEventConditions::parse(&self.now, conds);
170 let events = self.db.events.list(&conds, pager).await?;
171 let events = self.short_ids.events(events).await?;
172 Ok(events)
173 }
174
175 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, sqlx::Error> {
177 let conds = ParsedEventConditions::parse(&self.now, conds);
178 self.db.events.count(&conds).await
179 }
180
181 pub fn default_todo_draft(&self) -> TodoDraft {
183 TodoDraft::default(&self.config, &self.now)
184 }
185
186 pub async fn new_todo(&self, draft: TodoDraft) -> Result<impl Todo + 'static, Box<dyn Error>> {
188 let uid = self.generate_uid(Kind::Todo).await?;
189 let todo = draft.into_ics(&self.config, &self.now, &uid);
190 let path = self.get_path(&uid);
191
192 let calendar = Calendar::new().push(todo.clone()).done();
193 fs::write(&path, calendar.to_string())
194 .await
195 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
196
197 self.db.upsert_todo(&path, &todo).await?;
198
199 let todo = self.short_ids.todo(todo).await?;
200 Ok(todo)
201 }
202
203 pub async fn update_todo(
205 &self,
206 id: &Id,
207 patch: TodoPatch,
208 ) -> Result<impl Todo + 'static, Box<dyn Error>> {
209 let uid = self.short_ids.get_uid(id).await?;
210 let todo = match self.db.todos.get(&uid).await? {
211 Some(todo) => todo,
212 None => return Err("Todo not found".into()),
213 };
214
215 let path: PathBuf = todo.path().into();
216 let mut calendar = parse_ics(&path).await?;
217 let t = calendar
218 .components
219 .iter_mut()
220 .filter_map(|a| match a {
221 CalendarComponent::Todo(a) => Some(a),
222 _ => None,
223 })
224 .find(|a| a.get_uid() == Some(todo.uid()))
225 .ok_or("Todo not found in calendar")?;
226
227 patch.apply_to(&self.now, t);
228 let todo = t.clone();
229 fs::write(&path, calendar.done().to_string())
230 .await
231 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
232
233 self.db.upsert_todo(&path, &todo).await?;
234
235 let todo = self.short_ids.todo(todo).await?;
236 Ok(todo)
237 }
238
239 pub async fn get_todo(&self, id: &Id) -> Result<impl Todo + 'static, Box<dyn Error>> {
241 let uid = self.short_ids.get_uid(id).await?;
242 match self.db.todos.get(&uid).await {
243 Ok(Some(todo)) => Ok(self.short_ids.todo(todo).await?),
244 Ok(None) => Err("Event not found".into()),
245 Err(e) => Err(e.into()),
246 }
247 }
248
249 pub async fn list_todos(
251 &self,
252 conds: &TodoConditions,
253 sort: &[TodoSort],
254 pager: &Pager,
255 ) -> Result<Vec<impl Todo + 'static>, Box<dyn Error>> {
256 let conds = ParsedTodoConditions::parse(&self.now, conds);
257 let sort = ParsedTodoSort::parse_vec(&self.config, sort);
258 let todos = self.db.todos.list(&conds, &sort, pager).await?;
259 let todos = self.short_ids.todos(todos).await?;
260 Ok(todos)
261 }
262
263 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, sqlx::Error> {
265 let conds = ParsedTodoConditions::parse(&self.now, conds);
266 self.db.todos.count(&conds).await
267 }
268
269 pub async fn flush_short_ids(&self) -> Result<(), Box<dyn Error>> {
271 self.short_ids.flush().await
272 }
273
274 pub async fn close(self) -> Result<(), Box<dyn Error>> {
276 self.db.close().await
277 }
278
279 #[tracing::instrument(skip(self))]
280 async fn add_calendar(&self, calendar_path: &PathBuf) -> Result<(), Box<dyn Error>> {
281 let mut reader = fs::read_dir(calendar_path)
282 .await
283 .map_err(|e| format!("Failed to read directory: {e}"))?;
284
285 let mut handles = vec![];
286 let mut count_ics = 0;
287
288 while let Some(entry) = reader.next_entry().await? {
289 let path = entry.path();
290 match path.extension() {
291 Some(ext) if ext == "ics" => {
292 count_ics += 1;
293 let that = self.clone();
294 handles.push(tokio::spawn(async move {
295 if let Err(err) = that.add_ics(&path).await {
296 tracing::error!(path = %path.display(), err, "failed to process file");
297 }
298 }));
299 }
300 _ => {}
301 }
302 }
303
304 for handle in handles {
305 handle.await?;
306 }
307
308 tracing::debug!(count = count_ics, "total .ics files processed");
309 Ok(())
310 }
311
312 async fn add_ics(self, path: &Path) -> Result<(), Box<dyn Error>> {
313 tracing::debug!(path = %path.display(), "parsing file");
314 let calendar = parse_ics(path).await?;
315
316 tracing::debug!(path = %path.display(), components = calendar.components.len(), "found components");
317 for component in calendar.components {
318 tracing::debug!(?component, "processing component");
319 match component {
320 CalendarComponent::Event(event) => self.db.upsert_event(path, &event).await?,
321 CalendarComponent::Todo(todo) => self.db.upsert_todo(path, &todo).await?,
322 _ => tracing::warn!(?component, "ignoring unsupported component type"),
323 }
324 }
325
326 Ok(())
327 }
328
329 async fn generate_uid(&self, kind: Kind) -> Result<String, Box<dyn Error>> {
330 for i in 0..16 {
331 let uid = Uuid::new_v4().to_string(); tracing::debug!(
333 ?uid,
334 attempt = i + 1,
335 "generated uid, checking for uniqueness"
336 );
337
338 let exists = match kind {
339 Kind::Event => self.db.events.get(&uid).await?.is_some(),
340 Kind::Todo => self.db.todos.get(&uid).await?.is_some(),
341 };
342 if exists {
343 tracing::debug!(uid, ?kind, "uid already exists in db");
344 continue;
345 }
346
347 let path = self.get_path(&uid);
348 if fs::try_exists(&path).await? {
349 tracing::debug!(uid, ?path, "uid already exists as a file");
350 continue;
351 }
352 return Ok(uid);
353 }
354
355 tracing::warn!("failed to generate a unique uid after multiple attempts");
356 Err("Failed to generate a unique UID after multiple attempts".into())
357 }
358
359 fn get_path(&self, uid: &str) -> PathBuf {
360 self.calendar_path.join(format!("{uid}.ics"))
361 }
362}
363
364async fn prepare(config: &Config) -> Result<(), Box<dyn Error>> {
365 if let Some(parent) = &config.state_dir {
366 tracing::debug!(path = %parent.display(), "ensuring state directory exists");
367 fs::create_dir_all(parent).await?;
368 }
369 Ok(())
370}
371
372async fn parse_ics(path: &Path) -> Result<Calendar, Box<dyn Error>> {
373 fs::read_to_string(path)
374 .await
375 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?
376 .parse()
377 .map_err(|e| format!("Failed to parse calendar: {e}").into())
378}