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 list_todos(
107 &self,
108 conds: &TodoConditions,
109 sort: &[TodoSort],
110 pager: &Pager,
111 ) -> Result<Vec<impl Todo>, sqlx::Error> {
112 self.cache.todos.list(conds, sort, pager).await
113 }
114
115 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, sqlx::Error> {
117 self.cache.todos.count(conds).await
118 }
119
120 async fn add_calendar(&self, calendar_path: &PathBuf) -> Result<(), Box<dyn Error>> {
121 let mut reader = fs::read_dir(calendar_path)
122 .await
123 .map_err(|e| format!("Failed to read directory: {e}"))?;
124
125 let mut handles = vec![];
126 let mut count_ics = 0;
127
128 while let Some(entry) = reader.next_entry().await? {
129 let path = entry.path();
130 match path.extension() {
131 Some(ext) if ext == "ics" => {
132 count_ics += 1;
133 let that = self.clone();
134 handles.push(tokio::spawn(async move {
135 if let Err(e) = that.add_ics(&path).await {
136 log::error!("Failed to process file {}: {}", path.display(), e);
137 }
138 }));
139 }
140 _ => {}
141 }
142 }
143
144 for handle in handles {
145 handle.await?;
146 }
147
148 log::debug!("Total .ics files processed: {count_ics}");
149 Ok(())
150 }
151
152 async fn add_ics(self, path: &Path) -> Result<(), Box<dyn Error>> {
153 log::debug!("Parsing file: {}", path.display());
154 let calendar = parse_ics(path).await?;
155 log::debug!(
156 "Found {} components in {}.",
157 calendar.components.len(),
158 path.display()
159 );
160
161 for component in calendar.components {
162 log::debug!("Processing component: {component:?}");
163 match component {
164 CalendarComponent::Event(event) => self.cache.upsert_event(path, &event).await?,
165 CalendarComponent::Todo(todo) => self.cache.upsert_todo(path, &todo).await?,
166 _ => log::warn!("Ignoring unsupported component type: {component:?}"),
167 }
168 }
169
170 Ok(())
171 }
172}
173
174#[derive(Debug)]
176pub struct Config {
177 pub calendar_path: PathBuf,
179}
180
181async fn parse_ics(path: &Path) -> Result<Calendar, Box<dyn Error>> {
182 fs::read_to_string(path)
183 .await
184 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?
185 .parse()
186 .map_err(|e| format!("Failed to parse calendar: {e}").into())
187}