1use crate::{
6 Event, EventConditions, Pager, Todo, TodoConditions, TodoSort, cache::SqliteCache,
7 todo::TodoPatch,
8};
9use icalendar::{Calendar, CalendarComponent, Component};
10use std::{
11 error::Error,
12 path::{Path, PathBuf},
13};
14use tokio::fs;
15
16#[derive(Debug, Clone)]
18pub struct Aim {
19 cache: SqliteCache,
20}
21
22impl Aim {
23 pub async fn new(config: &Config) -> Result<Self, Box<dyn Error>> {
25 let cache = SqliteCache::open()
26 .await
27 .map_err(|e| format!("Failed to initialize cache: {e}"))?;
28
29 let that = Self { cache };
30 that.add_calendar(&config.calendar_path)
31 .await
32 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
33
34 Ok(that)
35 }
36
37 pub async fn list_events(
39 &self,
40 conds: &EventConditions,
41 pager: &Pager,
42 ) -> Result<Vec<impl Event>, sqlx::Error> {
43 self.cache.events.list(conds, pager).await
44 }
45
46 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, sqlx::Error> {
48 self.cache.events.count(conds).await
49 }
50
51 pub async fn upsert_todo(&self, patch: TodoPatch) -> Result<impl Todo, Box<dyn Error>> {
53 let mut todo = match self.cache.todos.get(&patch.uid).await? {
54 Some(todo) => todo,
55 None => return Err("Todo not found".into()),
56 };
57
58 let path: PathBuf = todo.path().into();
59 let mut calendar = read_calendar_file(&path).await?;
60 let mut flag = true;
61 for component in &mut calendar.components.iter_mut() {
62 if let CalendarComponent::Todo(t) = component {
63 if t.get_uid() == Some(todo.uid()) {
64 patch.apply_to(t);
65 flag = false;
66 break;
67 }
68 }
69 }
70 if flag {
71 return Err("Todo not found in calendar".into());
72 }
73 let ics = calendar.done();
74 fs::write(path, ics.to_string())
75 .await
76 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
77
78 todo.apply(patch);
79 self.cache.todos.upsert(&todo).await?;
80
81 Ok(todo)
82 }
83
84 pub async fn list_todos(
86 &self,
87 conds: &TodoConditions,
88 sort: &[TodoSort],
89 pager: &Pager,
90 ) -> Result<Vec<impl Todo>, sqlx::Error> {
91 self.cache.todos.list(conds, sort, pager).await
92 }
93
94 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, sqlx::Error> {
96 self.cache.todos.count(conds).await
97 }
98
99 async fn add_calendar(&self, calendar_path: &PathBuf) -> Result<(), Box<dyn Error>> {
100 let mut reader = fs::read_dir(calendar_path)
101 .await
102 .map_err(|e| format!("Failed to read directory: {e}"))?;
103
104 let mut handles = vec![];
105 let mut count_ics = 0;
106
107 while let Some(entry) = reader.next_entry().await? {
108 let path = entry.path();
109 match path.extension() {
110 Some(ext) if ext == "ics" => {
111 count_ics += 1;
112 let that = self.clone();
113 handles.push(tokio::spawn(async move {
114 if let Err(e) = that.add_ics(&path).await {
115 log::error!("Failed to process file {}: {}", path.display(), e);
116 }
117 }));
118 }
119 _ => {}
120 }
121 }
122
123 for handle in handles {
124 handle.await?;
125 }
126
127 log::debug!("Total .ics files processed: {count_ics}");
128 Ok(())
129 }
130
131 async fn add_ics(self, path: &Path) -> Result<(), Box<dyn Error>> {
132 log::debug!("Parsing file: {}", path.display());
133 let calendar = read_calendar_file(path).await?;
134 log::debug!(
135 "Found {} components in {}.",
136 calendar.components.len(),
137 path.display()
138 );
139
140 for component in calendar.components {
141 log::debug!("Processing component: {component:?}");
142 match component {
143 CalendarComponent::Event(event) => self.cache.upsert_event(path, event).await?,
144 CalendarComponent::Todo(todo) => self.cache.upsert_todo(path, todo).await?,
145 _ => log::warn!("Ignoring unsupported component type: {component:?}"),
146 }
147 }
148
149 Ok(())
150 }
151}
152
153#[derive(Debug)]
155pub struct Config {
156 pub calendar_path: PathBuf,
158}
159
160async fn read_calendar_file(path: &Path) -> Result<Calendar, Box<dyn Error>> {
161 fs::read_to_string(path)
162 .await
163 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?
164 .parse()
165 .map_err(|e| format!("Failed to parse calendar: {e}").into())
166}