1use crate::{
6 Event, EventConditions, Pager, Todo, TodoConditions, TodoDraft, 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 calendar_path: PathBuf,
21}
22
23impl Aim {
24 pub async fn new(config: &Config) -> Result<Self, Box<dyn Error>> {
26 let cache = SqliteCache::open()
27 .await
28 .map_err(|e| format!("Failed to initialize cache: {e}"))?;
29
30 let that = Self {
31 cache,
32 calendar_path: config.calendar_path.clone(),
33 };
34 that.add_calendar(&config.calendar_path)
35 .await
36 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
37
38 Ok(that)
39 }
40
41 pub async fn list_events(
43 &self,
44 conds: &EventConditions,
45 pager: &Pager,
46 ) -> Result<Vec<impl Event>, sqlx::Error> {
47 self.cache.events.list(conds, pager).await
48 }
49
50 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, sqlx::Error> {
52 self.cache.events.count(conds).await
53 }
54
55 pub async fn new_todo(&self, draft: TodoDraft) -> Result<impl Todo, Box<dyn Error>> {
57 if self.cache.todos.get(&draft.uid).await?.is_some() {
58 return Err("Todo with this UID already exists".into());
59 }
60
61 let path = self.calendar_path.join(format!("{}.ics", draft.uid));
62 if fs::try_exists(&path).await? {
63 return Err(format!("File already exists: {}", path.display()).into());
64 }
65
66 let todo = draft.into_todo()?;
67 let calendar = Calendar::new().push(todo.clone()).done();
68 fs::write(&path, calendar.to_string())
69 .await
70 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
71
72 self.cache.upsert_todo(&path, &todo).await?;
73 Ok(todo)
74 }
75
76 pub async fn update_todo(&self, patch: TodoPatch) -> Result<impl Todo, Box<dyn Error>> {
78 let todo = match self.cache.todos.get(&patch.uid).await? {
79 Some(todo) => todo,
80 None => return Err("Todo not found".into()),
81 };
82
83 let path: PathBuf = todo.path().into();
84 let mut calendar = parse_ics(&path).await?;
85 let t = calendar
86 .components
87 .iter_mut()
88 .filter_map(|a| match a {
89 CalendarComponent::Todo(a) => Some(a),
90 _ => None,
91 })
92 .find(|a| a.get_uid() == Some(todo.uid()))
93 .ok_or("Todo not found in calendar")?;
94
95 patch.apply_to(t);
96 let todo = t.clone();
97 fs::write(&path, calendar.done().to_string())
98 .await
99 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
100
101 self.cache.upsert_todo(&path, &todo).await?;
102 Ok(todo)
103 }
104
105 pub async fn get_todo(&self, uid: &str) -> Result<Option<impl Todo>, sqlx::Error> {
107 self.cache.todos.get(uid).await
108 }
109
110 pub async fn list_todos(
112 &self,
113 conds: &TodoConditions,
114 sort: &[TodoSort],
115 pager: &Pager,
116 ) -> Result<Vec<impl Todo>, sqlx::Error> {
117 self.cache.todos.list(conds, sort, pager).await
118 }
119
120 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, sqlx::Error> {
122 self.cache.todos.count(conds).await
123 }
124
125 async fn add_calendar(&self, calendar_path: &PathBuf) -> Result<(), Box<dyn Error>> {
126 let mut reader = fs::read_dir(calendar_path)
127 .await
128 .map_err(|e| format!("Failed to read directory: {e}"))?;
129
130 let mut handles = vec![];
131 let mut count_ics = 0;
132
133 while let Some(entry) = reader.next_entry().await? {
134 let path = entry.path();
135 match path.extension() {
136 Some(ext) if ext == "ics" => {
137 count_ics += 1;
138 let that = self.clone();
139 handles.push(tokio::spawn(async move {
140 if let Err(e) = that.add_ics(&path).await {
141 log::error!("Failed to process file {}: {}", path.display(), e);
142 }
143 }));
144 }
145 _ => {}
146 }
147 }
148
149 for handle in handles {
150 handle.await?;
151 }
152
153 log::debug!("Total .ics files processed: {count_ics}");
154 Ok(())
155 }
156
157 async fn add_ics(self, path: &Path) -> Result<(), Box<dyn Error>> {
158 log::debug!("Parsing file: {}", path.display());
159 let calendar = parse_ics(path).await?;
160 log::debug!(
161 "Found {} components in {}.",
162 calendar.components.len(),
163 path.display()
164 );
165
166 for component in calendar.components {
167 log::debug!("Processing component: {component:?}");
168 match component {
169 CalendarComponent::Event(event) => self.cache.upsert_event(path, &event).await?,
170 CalendarComponent::Todo(todo) => self.cache.upsert_todo(path, &todo).await?,
171 _ => log::warn!("Ignoring unsupported component type: {component:?}"),
172 }
173 }
174
175 Ok(())
176 }
177}
178
179#[derive(Debug)]
181pub struct Config {
182 pub calendar_path: PathBuf,
184}
185
186async fn parse_ics(path: &Path) -> Result<Calendar, Box<dyn Error>> {
187 fs::read_to_string(path)
188 .await
189 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?
190 .parse()
191 .map_err(|e| format!("Failed to parse calendar: {e}").into())
192}