diff --git a/src/cache.rs b/src/cache.rs index a398d6d..c8b2edc 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,22 +1,22 @@ //! This module provides a local cache for CalDAV data -use std::path::PathBuf; -use std::path::Path; -use std::error::Error; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::error::Error; use std::ffi::OsStr; +use std::path::Path; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; -use serde::{Deserialize, Serialize}; use async_trait::async_trait; use csscolorparser::Color; +use serde::{Deserialize, Serialize}; use url::Url; -use crate::traits::CalDavSource; -use crate::traits::BaseCalendar; -use crate::traits::CompleteCalendar; use crate::calendar::cached_calendar::CachedCalendar; use crate::calendar::SupportedComponents; +use crate::traits::BaseCalendar; +use crate::traits::CalDavSource; +use crate::traits::CompleteCalendar; #[cfg(feature = "local_calendar_mocks_remote_calendars")] use crate::mock_behaviour::MockBehaviour; @@ -52,10 +52,9 @@ impl Cache { self.mock_behaviour = mock_behaviour; } - /// Get the path to the cache folder pub fn cache_folder() -> PathBuf { - return PathBuf::from(String::from("~/.config/my-tasks/cache/")) + PathBuf::from(String::from("~/.config/my-tasks/cache/")) } /// Initialize a cache from the content of a valid backing folder if it exists. @@ -66,7 +65,7 @@ impl Cache { let mut data: CachedData = match std::fs::File::open(&main_file) { Err(err) => { return Err(format!("Unable to open file {:?}: {}", main_file, err).into()); - }, + } Ok(file) => serde_json::from_reader(file)?, }; @@ -76,25 +75,30 @@ impl Cache { 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); + log::error!( + "Unable to load calendar {:?} from cache: {:?}", + cal_path, + err + ); continue; - }, - Ok(cal) => - data.calendars.insert(cal.url().clone(), Arc::new(Mutex::new(cal))), + } + Ok(cal) => data + .calendars + .insert(cal.url().clone(), Arc::new(Mutex::new(cal))), }; } - }, + } } } - Ok(Self{ + Ok(Self { backing_folder: PathBuf::from(folder), data, @@ -104,13 +108,13 @@ impl Cache { } fn load_calendar(path: &Path) -> Result> { - let file = std::fs::File::open(&path)?; + let file = std::fs::File::open(path)?; Ok(serde_json::from_reader(file)?) } /// Initialize a cache with the default contents pub fn new(folder_path: &Path) -> Self { - Self{ + Self { backing_folder: PathBuf::from(folder_path), data: CachedData::default(), @@ -143,16 +147,18 @@ impl Cache { Ok(()) } - /// Compares two Caches to check they have the same current content /// /// This is not a complete equality test: some attributes (sync status...) may differ. This should mostly be used in tests #[cfg(any(test, feature = "integration_tests"))] - pub async fn has_same_observable_content_as(&self, other: &Self) -> Result> { + pub async fn has_same_observable_content_as( + &self, + other: &Self, + ) -> Result> { let calendars_l = self.get_calendars().await?; let calendars_r = other.get_calendars().await?; - if crate::utils::keys_are_the_same(&calendars_l, &calendars_r) == false { + if !crate::utils::keys_are_the_same(&calendars_l, &calendars_r) { log::debug!("Different keys for calendars"); return Ok(false); } @@ -166,11 +172,10 @@ impl Cache { }; // TODO: check calendars have the same names/ID/whatever - if cal_l.has_same_observable_content_as(&cal_r).await? == false { + if !(cal_l.has_same_observable_content_as(&cal_r).await?) { log::debug!("Different calendars"); - return Ok(false) + return Ok(false); } - } Ok(true) } @@ -179,32 +184,43 @@ impl Cache { impl Drop for Cache { fn drop(&mut self) { if let Err(err) = self.save_to_folder() { - log::error!("Unable to automatically save the cache when it's no longer required: {}", err); + log::error!( + "Unable to automatically save the cache when it's no longer required: {}", + err + ); } } } impl Cache { /// The non-async version of [`crate::traits::CalDavSource::get_calendars`] - pub fn get_calendars_sync(&self) -> Result>>, Box> { + pub fn get_calendars_sync( + &self, + ) -> Result>>, Box> { #[cfg(feature = "local_calendar_mocks_remote_calendars")] - self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?; + self.mock_behaviour + .as_ref() + .map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?; - Ok(self.data.calendars.iter() + Ok(self + .data + .calendars + .iter() .map(|(url, cal)| (url.clone(), cal.clone())) - .collect() - ) + .collect()) } /// The non-async version of [`crate::traits::CalDavSource::get_calendar`] pub fn get_calendar_sync(&self, url: &Url) -> Option>> { - self.data.calendars.get(url).map(|arc| arc.clone()) + self.data.calendars.get(url).cloned() } } #[async_trait] impl CalDavSource for Cache { - async fn get_calendars(&self) -> Result>>, Box> { + async fn get_calendars( + &self, + ) -> Result>>, Box> { self.get_calendars_sync() } @@ -212,21 +228,33 @@ impl CalDavSource for Cache { self.get_calendar_sync(url) } - async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option) -> Result>, Box> { + async fn create_calendar( + &mut self, + url: Url, + name: String, + supported_components: SupportedComponents, + color: Option, + ) -> Result>, Box> { log::debug!("Inserting local calendar {}", url); #[cfg(feature = "local_calendar_mocks_remote_calendars")] - self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?; + self.mock_behaviour + .as_ref() + .map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?; let new_calendar = CachedCalendar::new(name, url.clone(), supported_components, color); let arc = Arc::new(Mutex::new(new_calendar)); #[cfg(feature = "local_calendar_mocks_remote_calendars")] if let Some(behaviour) = &self.mock_behaviour { - arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour))); + arc.lock() + .unwrap() + .set_mock_behaviour(Some(Arc::clone(behaviour))); }; match self.data.calendars.insert(url, arc.clone()) { - Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()), + Some(_) => { + Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()) + } None => Ok(arc), } } @@ -236,38 +264,54 @@ impl CalDavSource for Cache { mod tests { use super::*; - use url::Url; use crate::calendar::SupportedComponents; use crate::item::Item; use crate::task::Task; + use url::Url; async fn populate_cache(cache_path: &Path) -> Cache { - let mut cache = Cache::new(&cache_path); + let mut cache = Cache::new(cache_path); - let _shopping_list = cache.create_calendar( - Url::parse("https://caldav.com/shopping").unwrap(), - "My shopping list".to_string(), - SupportedComponents::TODO, - Some(csscolorparser::parse("lime").unwrap()), - ).await.unwrap(); + let _shopping_list = cache + .create_calendar( + Url::parse("https://caldav.com/shopping").unwrap(), + "My shopping list".to_string(), + SupportedComponents::TODO, + Some(csscolorparser::parse("lime").unwrap()), + ) + .await + .unwrap(); - let bucket_list = cache.create_calendar( - Url::parse("https://caldav.com/bucket-list").unwrap(), - "My bucket list".to_string(), - SupportedComponents::TODO, - Some(csscolorparser::parse("#ff8000").unwrap()), - ).await.unwrap(); + let bucket_list = cache + .create_calendar( + Url::parse("https://caldav.com/bucket-list").unwrap(), + "My bucket list".to_string(), + SupportedComponents::TODO, + Some(csscolorparser::parse("#ff8000").unwrap()), + ) + .await + .unwrap(); { let mut bucket_list = bucket_list.lock().unwrap(); let cal_url = bucket_list.url().clone(); - bucket_list.add_item(Item::Task(Task::new( - String::from("Attend a concert of JS Bach"), false, &cal_url - ))).await.unwrap(); + bucket_list + .add_item(Item::Task(Task::new( + String::from("Attend a concert of JS Bach"), + false, + &cal_url, + ))) + .await + .unwrap(); - bucket_list.add_item(Item::Task(Task::new( - String::from("Climb the Lighthouse of Alexandria"), true, &cal_url - ))).await.unwrap(); + bucket_list + .add_item(Item::Task(Task::new( + String::from("Climb the Lighthouse of Alexandria"), + true, + &cal_url, + ))) + .await + .unwrap(); } cache @@ -285,7 +329,7 @@ mod tests { assert_eq!(cache.backing_folder, retrieved_cache.backing_folder); let test = cache.has_same_observable_content_as(&retrieved_cache).await; println!("Equal? {:?}", test); - assert_eq!(test.unwrap(), true); + assert!(test.unwrap(), true); } #[tokio::test] @@ -295,12 +339,14 @@ mod tests { let mut cache = populate_cache(&cache_path).await; // We should not be able to add a second calendar with the same URL - let second_addition_same_calendar = cache.create_calendar( - Url::parse("https://caldav.com/shopping").unwrap(), - "My shopping list".to_string(), - SupportedComponents::TODO, - None, - ).await; + let second_addition_same_calendar = cache + .create_calendar( + Url::parse("https://caldav.com/shopping").unwrap(), + "My shopping list".to_string(), + SupportedComponents::TODO, + None, + ) + .await; assert!(second_addition_same_calendar.is_err()); } } diff --git a/src/client.rs b/src/client.rs index 993bec1..97cbb88 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,25 +1,24 @@ //! This module provides a client to connect to a CalDAV server -use std::error::Error; -use std::convert::TryFrom; use std::collections::HashMap; +use std::convert::TryFrom; +use std::error::Error; use std::sync::{Arc, Mutex}; use async_trait::async_trait; -use reqwest::{Method, StatusCode}; -use reqwest::header::CONTENT_TYPE; -use minidom::Element; -use url::Url; use csscolorparser::Color; +use minidom::Element; +use reqwest::header::CONTENT_TYPE; +use reqwest::{Method, StatusCode}; +use url::Url; -use crate::resource::Resource; -use crate::utils::{find_elem, find_elems}; use crate::calendar::remote_calendar::RemoteCalendar; use crate::calendar::SupportedComponents; -use crate::traits::CalDavSource; +use crate::resource::Resource; use crate::traits::BaseCalendar; +use crate::traits::CalDavSource; use crate::traits::DavCalendar; - +use crate::utils::{find_elem, find_elems}; static DAVCLIENT_BODY: &str = r#" @@ -49,11 +48,13 @@ static CAL_BODY: &str = r#" "#; - - -pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String, depth: u32) -> Result> { - let method = method.parse() - .expect("invalid method name"); +pub(crate) async fn sub_request( + resource: &Resource, + method: &str, + body: String, + depth: u32, +) -> Result> { + let method = method.parse().expect("invalid method name"); let res = reqwest::Client::new() .request(method, resource.url().clone()) @@ -64,7 +65,7 @@ pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String, .send() .await?; - if res.status().is_success() == false { + if !res.status().is_success() { return Err(format!("Unexpected HTTP status code {:?}", res.status()).into()); } @@ -72,12 +73,16 @@ pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String, Ok(text) } -pub(crate) async fn sub_request_and_extract_elem(resource: &Resource, body: String, items: &[&str]) -> Result> { +pub(crate) async fn sub_request_and_extract_elem( + resource: &Resource, + body: String, + items: &[&str], +) -> Result> { let text = sub_request(resource, "PROPFIND", body, 0).await?; let mut current_element: &Element = &text.parse()?; for item in items { - current_element = match find_elem(¤t_element, item) { + current_element = match find_elem(current_element, item) { Some(elem) => elem, None => return Err(format!("missing element {}", item).into()), } @@ -85,18 +90,21 @@ pub(crate) async fn sub_request_and_extract_elem(resource: &Resource, body: Stri Ok(current_element.text()) } -pub(crate) async fn sub_request_and_extract_elems(resource: &Resource, method: &str, body: String, item: &str) -> Result, Box> { +pub(crate) async fn sub_request_and_extract_elems( + resource: &Resource, + method: &str, + body: String, + item: &str, +) -> Result, Box> { let text = sub_request(resource, method, body, 1).await?; let element: &Element = &text.parse()?; - Ok(find_elems(&element, item) + Ok(find_elems(element, item) .iter() .map(|elem| (*elem).clone()) - .collect() - ) + .collect()) } - /// A CalDAV data source that fetches its data from a CalDAV server #[derive(Debug)] pub struct Client { @@ -107,7 +115,6 @@ pub struct Client { cached_replies: Mutex, } - #[derive(Debug, Default)] struct CachedReplies { principal: Option, @@ -117,10 +124,14 @@ struct CachedReplies { impl Client { /// Create a client. This does not start a connection - pub fn new, T: ToString, U: ToString>(url: S, username: T, password: U) -> Result> { + pub fn new, T: ToString, U: ToString>( + url: S, + username: T, + password: U, + ) -> Result> { let url = Url::parse(url.as_ref())?; - Ok(Self{ + Ok(Self { resource: Resource::new(url, username.to_string(), password.to_string()), cached_replies: Mutex::new(CachedReplies::default()), }) @@ -132,12 +143,17 @@ impl Client { return Ok(p.clone()); } - let href = sub_request_and_extract_elem(&self.resource, DAVCLIENT_BODY.into(), &["current-user-principal", "href"]).await?; + let href = sub_request_and_extract_elem( + &self.resource, + DAVCLIENT_BODY.into(), + &["current-user-principal", "href"], + ) + .await?; let principal_url = self.resource.combine(&href); self.cached_replies.lock().unwrap().principal = Some(principal_url.clone()); log::debug!("Principal URL is {}", href); - return Ok(principal_url); + Ok(principal_url) } /// Return the Homeset URL, or fetch it from server if not known yet @@ -147,7 +163,12 @@ impl Client { } let principal_url = self.get_principal().await?; - let href = sub_request_and_extract_elem(&principal_url, HOMESET_BODY.into(), &["calendar-home-set", "href"]).await?; + let href = sub_request_and_extract_elem( + &principal_url, + HOMESET_BODY.into(), + &["calendar-home-set", "href"], + ) + .await?; let chs_url = self.resource.combine(&href); self.cached_replies.lock().unwrap().calendar_home_set = Some(chs_url.clone()); log::debug!("Calendar home set URL is {:?}", href); @@ -158,10 +179,18 @@ impl Client { async fn populate_calendars(&self) -> Result<(), Box> { let cal_home_set = self.get_cal_home_set().await?; - let reps = sub_request_and_extract_elems(&cal_home_set, "PROPFIND", CAL_BODY.to_string(), "response").await?; + let reps = sub_request_and_extract_elems( + &cal_home_set, + "PROPFIND", + CAL_BODY.to_string(), + "response", + ) + .await?; let mut calendars = HashMap::new(); for rep in reps { - let display_name = find_elem(&rep, "displayname").map(|e| e.text()).unwrap_or("".to_string()); + 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 @@ -176,7 +205,7 @@ impl Client { break; } } - if found_calendar_type == false { + if !found_calendar_type { continue; } @@ -193,48 +222,60 @@ impl Client { None => { log::warn!("Calendar {} has no URL! Ignoring it.", display_name); continue; - }, + } Some(h) => h.text(), }; let this_calendar_url = self.resource.combine(&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 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_color = find_elem(&rep, "calendar-color") - .and_then(|col| { - col.texts().next() - .and_then(|t| csscolorparser::parse(t).ok()) - }); + let this_calendar_color = find_elem(&rep, "calendar-color").and_then(|col| { + col.texts() + .next() + .and_then(|t| csscolorparser::parse(t).ok()) + }); - let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color); + let this_calendar = RemoteCalendar::new( + display_name, + this_calendar_url, + supported_components, + this_calendar_color, + ); log::info!("Found calendar {}", this_calendar.name()); - calendars.insert(this_calendar.url().clone(), Arc::new(Mutex::new(this_calendar))); + calendars.insert( + this_calendar.url().clone(), + Arc::new(Mutex::new(this_calendar)), + ); } let mut replies = self.cached_replies.lock().unwrap(); replies.calendars = Some(calendars); Ok(()) } - } #[async_trait] impl CalDavSource for Client { - async fn get_calendars(&self) -> Result>>, Box> { + async fn get_calendars( + &self, + ) -> Result>>, Box> { self.populate_calendars().await?; match &self.cached_replies.lock().unwrap().calendars { - Some(cals) => { - return Ok(cals.clone()) - }, - None => return Err("No calendars available".into()) + Some(cals) => return Ok(cals.clone()), + None => return Err("No calendars available".into()), }; } @@ -244,14 +285,22 @@ impl CalDavSource for Client { return None; } - self.cached_replies.lock().unwrap() + self.cached_replies + .lock() + .unwrap() .calendars .as_ref() .and_then(|cals| cals.get(url)) - .map(|cal| cal.clone()) + .cloned() } - async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option) -> Result>, Box> { + async fn create_calendar( + &mut self, + url: Url, + name: String, + supported_components: SupportedComponents, + color: Option, + ) -> Result>, Box> { self.populate_calendars().await?; match self.cached_replies.lock().unwrap().calendars.as_ref() { @@ -260,7 +309,7 @@ impl CalDavSource for Client { if cals.contains_key(&url) { return Err("This calendar already exists".into()); } - }, + } } let creation_body = calendar_body(name, supported_components, color); @@ -275,21 +324,35 @@ impl CalDavSource for Client { let status = response.status(); if status != StatusCode::CREATED { - return Err(format!("Unexpected HTTP status code. Expected CREATED, got {}", status.as_u16()).into()); + return Err(format!( + "Unexpected HTTP status code. Expected CREATED, got {}", + status.as_u16() + ) + .into()); } - self.get_calendar(&url).await.ok_or(format!("Unable to insert calendar {:?}", url).into()) + self.get_calendar(&url) + .await + .ok_or(format!("Unable to insert calendar {:?}", url).into()) } } -fn calendar_body(name: String, supported_components: SupportedComponents, color: Option) -> String { +fn calendar_body( + name: String, + supported_components: SupportedComponents, + color: Option, +) -> String { let color_property = match color { None => "".to_string(), - Some(color) => format!("{}FF", color.to_hex_string().to_ascii_uppercase()), + Some(color) => format!( + "{}FF", + color.to_hex_string().to_ascii_uppercase() + ), }; // This is taken from https://tools.ietf.org/html/rfc4791#page-24 - format!(r#" + format!( + r#"