diff --git a/Cargo.lock b/Cargo.lock index 2a6c0a3..482a4bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,7 @@ dependencies = [ "log", "minidom", "reqwest", + "sanitize-filename", "serde", "serde_json", "tokio", @@ -736,6 +737,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "sanitize-filename" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "schannel" version = "0.1.19" diff --git a/Cargo.toml b/Cargo.toml index e513727..9a277f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ serde_json = "1.0" async-trait = "0.1" chrono = { version = "0.4", features = ["serde"] } uuid = { version = "0.8", features = ["v4"] } +sanitize-filename = "0.3" diff --git a/src/cache.rs b/src/cache.rs index 06d5c65..8c4a208 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::collections::HashSet; use std::hash::Hash; use std::sync::{Arc, Mutex}; +use std::ffi::OsStr; use serde::{Deserialize, Serialize}; use async_trait::async_trait; @@ -19,66 +20,102 @@ use crate::traits::CompleteCalendar; use crate::calendar::cached_calendar::CachedCalendar; use crate::calendar::CalendarId; +const MAIN_FILE: &str = "data.json"; -/// A CalDAV source that stores its item in a local file -#[derive(Debug, PartialEq)] +/// A CalDAV source that stores its item in a local folder +#[derive(Debug)] pub struct Cache { - backing_file: PathBuf, + backing_folder: PathBuf, data: CachedData, } -#[derive(Default, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Default, Debug, Serialize, Deserialize)] struct CachedData { + #[serde(skip)] calendars: HashMap>>, last_sync: Option>, } impl Cache { - /// Get the path to the cache file - pub fn cache_file() -> PathBuf { - return PathBuf::from(String::from("~/.config/my-tasks/cache.json")) + /// Get the path to the cache folder + pub fn cache_folder() -> PathBuf { + return PathBuf::from(String::from("~/.config/my-tasks/cache/")) } - /// Initialize a cache from the content of a valid backing file if it exists. + /// Initialize a cache from the content of a valid backing folder if it exists. /// Returns an error otherwise - pub fn from_file(path: &Path) -> Result> { - let data = match std::fs::File::open(path) { + pub fn from_folder(folder: &Path) -> Result> { + // Load shared data... + let main_file = folder.join(MAIN_FILE); + let mut data: CachedData = match std::fs::File::open(&main_file) { Err(err) => { - return Err(format!("Unable to open file {:?}: {}", path, err).into()); + return Err(format!("Unable to open file {:?}: {}", main_file, err).into()); }, Ok(file) => serde_json::from_reader(file)?, }; + // ...and every calendar + for entry in std::fs::read_dir(folder)? { + match entry { + Err(err) => { + log::error!("Unable to read dir: {:?}", err); + continue; + }, + Ok(entry) => { + let cal_path = entry.path(); + log::debug!("Considering {:?}", cal_path); + if cal_path.extension() == Some(OsStr::new("cal")) { + match Self::load_calendar(&cal_path) { + Err(err) => { + log::error!("Unable to load calendar {:?} from cache: {:?}", cal_path, err); + continue; + }, + Ok(cal) => + data.calendars.insert(cal.id().clone(), Arc::new(Mutex::new(cal))), + }; + } + }, + } + } + Ok(Self{ - backing_file: PathBuf::from(path), + backing_folder: PathBuf::from(folder), data, }) } + fn load_calendar(path: &Path) -> Result> { + let file = std::fs::File::open(&path)?; + Ok(serde_json::from_reader(file)?) + } + /// Initialize a cache with the default contents - pub fn new(path: &Path) -> Self { + pub fn new(folder_path: &Path) -> Self { Self{ - backing_file: PathBuf::from(path), + backing_folder: PathBuf::from(folder_path), data: CachedData::default(), } } - /// Store the current Cache to its backing file - fn save_to_file(&mut self) { - // Save the contents to the file - let path = &self.backing_file; - let file = match std::fs::File::create(path) { - Err(err) => { - log::warn!("Unable to save file {:?}: {}", path, err); - return; - }, - Ok(f) => f, - }; + /// Store the current Cache to its backing folder + fn save_to_folder(&mut self) -> Result<(), std::io::Error> { + let folder = &self.backing_folder; + std::fs::create_dir_all(folder)?; - if let Err(err) = serde_json::to_writer(file, &self.data) { - log::warn!("Unable to serialize: {}", err); - return; - }; + // Save the general data + let main_file_path = folder.join(MAIN_FILE); + let file = std::fs::File::create(&main_file_path)?; + serde_json::to_writer(file, &self.data)?; + + // Save each calendar + for (cal_id, cal_mutex) in &self.data.calendars { + let file_name = sanitize_filename::sanitize(cal_id.as_str()) + ".cal"; + let cal_file = folder.join(file_name); + let file = std::fs::File::create(&cal_file)?; + let cal = cal_mutex.lock().unwrap(); + serde_json::to_writer(file, &*cal)?; + } + Ok(()) } @@ -95,6 +132,7 @@ impl Cache { let calendars_r = other.get_calendars().await?; if keys_are_the_same(&calendars_l, &calendars_r) == false { + log::debug!("Different keys for calendars"); return Ok(false); } @@ -109,6 +147,7 @@ impl Cache { let items_r = cal_r.get_items(); if keys_are_the_same(&items_l, &items_r) == false { + log::debug!("Different keys for items"); return Ok(false); } for (id_l, item_l) in items_l { @@ -116,8 +155,8 @@ impl Cache { Some(c) => c, None => return Err("should not happen, we've just tested keys are the same".into()), }; - //println!(" items {} {}", item_r.name(), item_l.name()); if &item_l != item_r { + log::debug!("Different items"); return Ok(false); } } @@ -170,9 +209,11 @@ mod tests { use url::Url; use crate::calendar::SupportedComponents; - #[test] - fn serde_cache() { - let cache_path = PathBuf::from(String::from("cache.json")); + #[tokio::test] + async fn serde_cache() { + let _ = env_logger::builder().is_test(true).try_init(); + + let cache_path = PathBuf::from(String::from("test_cache/")); let mut cache = Cache::new(&cache_path); @@ -181,9 +222,14 @@ mod tests { SupportedComponents::TODO); cache.add_calendar(Arc::new(Mutex::new(cal1))); - cache.save_to_file(); - let retrieved_cache = Cache::from_file(&cache_path).unwrap(); - assert_eq!(cache, retrieved_cache); + cache.save_to_folder().unwrap(); + + let retrieved_cache = Cache::from_folder(&cache_path).unwrap(); + assert_eq!(cache.backing_folder, retrieved_cache.backing_folder); + assert_eq!(cache.data.last_sync, retrieved_cache.data.last_sync); + let test = cache.has_same_contents_than(&retrieved_cache).await; + println!("Equal? {:?}", test); + assert_eq!(test.unwrap(), true); } } diff --git a/src/client.rs b/src/client.rs index 21cf1a2..14ee71a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -45,27 +45,6 @@ static CAL_BODY: &str = r#" "#; -static TASKS_BODY: &str = r#" - - - - - - - - - - - - - CANCELLED - - - - - -"#; /// A CalDAV source that fetches its data from a CalDAV server @@ -226,79 +205,6 @@ impl Client { #[async_trait] impl CalDavSource for Client { - /// Return the list of calendars, or fetch from server if not known yet - /* - async fn get_calendars(&self) -> Result<&HashMap, Box> { - let mut replies = self.cached_replies.lock().unwrap(); - - if let Some(c) = &replies.calendars { - return Ok(c); - } - let cal_home_set = self.get_cal_home_set().await?; - - let text = self.sub_request(&cal_home_set, CAL_BODY.into(), 1).await?; - - let root: Element = text.parse().unwrap(); - let reps = find_elems(&root, "response"); - let mut calendars = HashMap::new(); - for rep in reps { - let display_name = find_elem(rep, "displayname").map(|e| e.text()).unwrap_or("".to_string()); - log::debug!("Considering calendar {}", display_name); - - // We filter out non-calendar items - let resource_types = match find_elem(rep, "resourcetype") { - None => continue, - Some(rt) => rt, - }; - let mut found_calendar_type = false; - for resource_type in resource_types.children() { - if resource_type.name() == "calendar" { - found_calendar_type = true; - break; - } - } - if found_calendar_type == false { - continue; - } - - // We filter out the root calendar collection, that has an empty supported-calendar-component-set - let el_supported_comps = match find_elem(rep, "supported-calendar-component-set") { - None => continue, - Some(comps) => comps, - }; - if el_supported_comps.children().count() == 0 { - continue; - } - - let calendar_href = match find_elem(rep, "href") { - None => { - log::warn!("Calendar {} has no URL! Ignoring it.", display_name); - continue; - }, - Some(h) => h.text(), - }; - - let mut this_calendar_url = self.url.clone(); - this_calendar_url.set_path(&calendar_href); - - let supported_components = match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) { - Err(err) => { - log::warn!("Calendar {} has invalid supported components ({})! Ignoring it.", display_name, err); - continue; - }, - Ok(sc) => sc, - }; - let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components); - log::info!("Found calendar {}", this_calendar.name()); - calendars.insert(this_calendar.id().clone(), this_calendar); - } - - replies.calendars = Some(calendars); - Ok(&calendars) - } - */ - - async fn get_calendars(&self) -> Result>>, Box> { self.populate_calendars().await?; @@ -318,6 +224,5 @@ impl CalDavSource for Client { .and_then(|cals| cals.get(&id)) .map(|cal| cal.clone()) } - } diff --git a/src/utils.rs b/src/utils.rs index b5c900c..595e908 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,6 +7,7 @@ use minidom::Element; use crate::traits::CompleteCalendar; use crate::calendar::CalendarId; +use crate::Item; /// Walks an XML tree and returns every element that has the given name pub fn find_elems>(root: &Element, searched_name: S) -> Vec<&Element> { @@ -64,9 +65,17 @@ where for (id, cal) in cals { println!("CAL {}", id); for (_, item) in cal.lock().unwrap().get_items() { - let task = item.unwrap_task(); - let completion = if task.completed() {"✓"} else {" "}; - println!(" {} {}\t{}", completion, task.name(), task.id()); + print_task(item); } } } + +pub fn print_task(item: &Item) { + match item { + Item::Task(task) => { + let completion = if task.completed() {"✓"} else {" "}; + println!(" {} {}\t{}", completion, task.name(), task.id()); + }, + _ => return, + } +}