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;
15use uuid::Uuid;
16
17#[derive(Debug, Clone)]
19pub struct Aim {
20 cache: SqliteCache,
21 calendar_path: PathBuf,
22}
23
24impl Aim {
25 pub async fn new(config: &Config) -> Result<Self, Box<dyn Error>> {
27 let cache = SqliteCache::open()
28 .await
29 .map_err(|e| format!("Failed to initialize cache: {e}"))?;
30
31 let that = Self {
32 cache,
33 calendar_path: config.calendar_path.clone(),
34 };
35 that.add_calendar(&config.calendar_path)
36 .await
37 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
38
39 Ok(that)
40 }
41
42 pub async fn list_events(
44 &self,
45 conds: &EventConditions,
46 pager: &Pager,
47 ) -> Result<Vec<impl Event>, sqlx::Error> {
48 self.cache.events.list(conds, pager).await
49 }
50
51 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, sqlx::Error> {
53 self.cache.events.count(conds).await
54 }
55
56 pub async fn new_todo(&self, draft: TodoDraft) -> Result<impl Todo, Box<dyn Error>> {
58 let uid = self.generate_uid().await?;
59 let todo = draft.into_todo(&uid);
60 let path = self.get_path(&uid);
61
62 let calendar = Calendar::new().push(todo.clone()).done();
63 fs::write(&path, calendar.to_string())
64 .await
65 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
66
67 self.cache.upsert_todo(&path, &todo).await?;
68 Ok(todo)
69 }
70
71 pub async fn update_todo(&self, patch: TodoPatch) -> Result<impl Todo, Box<dyn Error>> {
73 let todo = match self.cache.todos.get(&patch.uid).await? {
74 Some(todo) => todo,
75 None => return Err("Todo not found".into()),
76 };
77
78 let path: PathBuf = todo.path().into();
79 let mut calendar = parse_ics(&path).await?;
80 let t = calendar
81 .components
82 .iter_mut()
83 .filter_map(|a| match a {
84 CalendarComponent::Todo(a) => Some(a),
85 _ => None,
86 })
87 .find(|a| a.get_uid() == Some(todo.uid()))
88 .ok_or("Todo not found in calendar")?;
89
90 patch.apply_to(t);
91 let todo = t.clone();
92 fs::write(&path, calendar.done().to_string())
93 .await
94 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
95
96 self.cache.upsert_todo(&path, &todo).await?;
97 Ok(todo)
98 }
99
100 pub async fn get_todo(&self, uid: &str) -> Result<Option<impl Todo>, sqlx::Error> {
102 self.cache.todos.get(uid).await
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 async fn generate_uid(&self) -> Result<String, Box<dyn Error>> {
174 for _ in 0..16 {
175 let uid = Uuid::new_v4().to_string(); if self.cache.todos.get(&uid).await?.is_some()
177 || fs::try_exists(&self.get_path(&uid)).await?
178 {
179 continue;
180 }
181 return Ok(uid);
182 }
183
184 Err("Failed to generate a unique UID after multiple attempts".into())
185 }
186
187 fn get_path(&self, uid: &str) -> PathBuf {
188 self.calendar_path.join(format!("{uid}.ics"))
189 }
190}
191
192#[derive(Debug)]
194pub struct Config {
195 pub calendar_path: PathBuf,
197}
198
199async fn parse_ics(path: &Path) -> Result<Calendar, Box<dyn Error>> {
200 fs::read_to_string(path)
201 .await
202 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?
203 .parse()
204 .map_err(|e| format!("Failed to parse calendar: {e}").into())
205}