From 060f33b4bd16fd32f4ebeaa2e73cd5ea5b0d7267 Mon Sep 17 00:00:00 2001 From: daladim Date: Thu, 18 Mar 2021 23:59:06 +0100 Subject: [PATCH] Turning calendars into Arc --- src/cache.rs | 37 ++++---- src/calendar/mod.rs | 1 + src/calendar/remote_calendar.rs | 38 +++++++- src/client.rs | 161 +++++++++++++++++++++++--------- src/provider.rs | 17 ++-- src/traits.rs | 12 +-- src/utils.rs | 8 +- tests/sync.rs | 16 ++-- 8 files changed, 199 insertions(+), 91 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 40a3d21..06d5c65 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -6,6 +6,7 @@ use std::error::Error; use std::collections::HashMap; use std::collections::HashSet; use std::hash::Hash; +use std::sync::{Arc, Mutex}; use serde::{Deserialize, Serialize}; use async_trait::async_trait; @@ -28,7 +29,7 @@ pub struct Cache { #[derive(Default, Debug, PartialEq, Serialize, Deserialize)] struct CachedData { - calendars: HashMap, + calendars: HashMap>>, last_sync: Option>, } @@ -81,8 +82,9 @@ impl Cache { } - pub fn add_calendar(&mut self, calendar: CachedCalendar) { - self.data.calendars.insert(calendar.id().clone(), calendar); + pub fn add_calendar(&mut self, calendar: Arc>) { + let id = calendar.lock().unwrap().id().clone(); + self.data.calendars.insert(id, calendar); } /// Compares two Caches to check they have the same current content @@ -97,8 +99,9 @@ impl Cache { } for (id, cal_l) in calendars_l { - let cal_r = match calendars_r.get(id) { - Some(c) => c, + let cal_l = cal_l.lock().unwrap(); + let cal_r = match calendars_r.get(&id) { + Some(c) => c.lock().unwrap(), None => return Err("should not happen, we've just tested keys are the same".into()), }; @@ -138,23 +141,15 @@ where #[async_trait] impl CalDavSource for Cache { - async fn get_calendars(&self) -> Result<&HashMap, Box> { - Ok(&self.data.calendars) + async fn get_calendars(&self) -> Result>>, Box> { + Ok(self.data.calendars.iter() + .map(|(id, cal)| (id.clone(), cal.clone())) + .collect() + ) } - async fn get_calendars_mut(&mut self) -> Result, Box> { - let mut hm = HashMap::new(); - for (id, val) in self.data.calendars.iter_mut() { - hm.insert(id.clone(), val); - } - Ok(hm) - } - - async fn get_calendar(&self, id: CalendarId) -> Option<&CachedCalendar> { - self.data.calendars.get(&id) - } - async fn get_calendar_mut(&mut self, id: CalendarId) -> Option<&mut CachedCalendar> { - self.data.calendars.get_mut(&id) + async fn get_calendar(&self, id: CalendarId) -> Option>> { + self.data.calendars.get(&id).map(|arc| arc.clone()) } } @@ -184,7 +179,7 @@ mod tests { let cal1 = CachedCalendar::new("shopping list".to_string(), Url::parse("https://caldav.com/shopping").unwrap(), SupportedComponents::TODO); - cache.add_calendar(cal1); + cache.add_calendar(Arc::new(Mutex::new(cal1))); cache.save_to_file(); diff --git a/src/calendar/mod.rs b/src/calendar/mod.rs index 68fa9d0..0585466 100644 --- a/src/calendar/mod.rs +++ b/src/calendar/mod.rs @@ -1,4 +1,5 @@ pub mod cached_calendar; +pub mod remote_calendar; use std::convert::TryFrom; use std::error::Error; diff --git a/src/calendar/remote_calendar.rs b/src/calendar/remote_calendar.rs index 2459a45..8dbb851 100644 --- a/src/calendar/remote_calendar.rs +++ b/src/calendar/remote_calendar.rs @@ -1,21 +1,39 @@ +use std::collections::{HashMap, HashSet}; +use std::error::Error; + +use url::Url; +use chrono::{DateTime, Utc}; + use crate::traits::PartialCalendar; +use crate::calendar::SupportedComponents; +use crate::calendar::CalendarId; +use crate::item::ItemId; +use crate::item::Item; /// A CalDAV calendar created by a [`Client`](crate::client::Client). +#[derive(Clone)] pub struct RemoteCalendar { name: String, url: Url, supported_components: SupportedComponents } -impl PartialCalendar for RemoteCalendar { - fn name(&self) -> &str { - &self.name +impl RemoteCalendar { + pub fn new(name: String, url: Url, supported_components: SupportedComponents) -> Self { + Self { + name, url, supported_components + } } +} +impl PartialCalendar for RemoteCalendar { + fn name(&self) -> &str { &self.name } + fn id(&self) -> &CalendarId { &self.url } fn supported_components(&self) -> crate::calendar::SupportedComponents { self.supported_components } + /// Returns the items that have been last-modified after `since` fn get_items_modified_since(&self, since: Option>, filter: Option) -> HashMap { @@ -23,16 +41,28 @@ impl PartialCalendar for RemoteCalendar { HashMap::new() } + /// Get the IDs of all current items in this calendar + fn get_item_ids(&mut self) -> HashSet { + log::error!("Not implemented"); + HashSet::new() + } + + /// Returns a particular item fn get_item_by_id_mut(&mut self, id: &ItemId) -> Option<&mut Item> { log::error!("Not implemented"); None } + /// Add an item into this calendar fn add_item(&mut self, item: Item) { log::error!("Not implemented"); } - fn delete_item(&mut self, item_id: &ItemId) { + /// Remove an item from this calendar + fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box> { log::error!("Not implemented"); + Ok(()) } + } + diff --git a/src/client.rs b/src/client.rs index 46a4546..21cf1a2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,15 +3,18 @@ use std::error::Error; use std::convert::TryFrom; use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use async_trait::async_trait; use reqwest::Method; use reqwest::header::CONTENT_TYPE; use minidom::Element; use url::Url; use crate::utils::{find_elem, find_elems}; -use crate::calendar::cached_calendar::CachedCalendar; +use crate::calendar::remote_calendar::RemoteCalendar; use crate::calendar::CalendarId; +use crate::traits::CalDavSource; use crate::traits::PartialCalendar; @@ -71,9 +74,17 @@ pub struct Client { username: String, password: String, + /// The interior mutable part of a Client. + /// This data may be retrieved once and then cached + cached_replies: Mutex, +} + + +#[derive(Default)] +struct CachedReplies { principal: Option, calendar_home_set: Option, - calendars: Option>, + calendars: Option>>>, } impl Client { @@ -85,9 +96,7 @@ impl Client { url, username: username.to_string(), password: password.to_string(), - principal: None, - calendar_home_set: None, - calendars: None, + cached_replies: Mutex::new(CachedReplies::default()), }) } @@ -111,33 +120,30 @@ impl Client { let text = self.sub_request(url, body, 0).await?; let mut current_element: &Element = &text.parse().unwrap(); - items.iter() - .map(|item| { + for item in items { current_element = find_elem(¤t_element, item).unwrap(); - }) - .collect::<()>(); - + } Ok(current_element.text()) } /// Return the Principal URL, or fetch it from server if not known yet - async fn get_principal(&mut self) -> Result> { - if let Some(p) = &self.principal { + async fn get_principal(&self) -> Result> { + if let Some(p) = &self.cached_replies.lock().unwrap().principal { return Ok(p.clone()); } let href = self.sub_request_and_process(&self.url, DAVCLIENT_BODY.into(), &["current-user-principal", "href"]).await?; let mut principal_url = self.url.clone(); principal_url.set_path(&href); - self.principal = Some(principal_url.clone()); + self.cached_replies.lock().unwrap().principal = Some(principal_url.clone()); log::debug!("Principal URL is {}", href); return Ok(principal_url); } /// Return the Homeset URL, or fetch it from server if not known yet - async fn get_cal_home_set(&mut self) -> Result> { - if let Some(h) = &self.calendar_home_set { + async fn get_cal_home_set(&self) -> Result> { + if let Some(h) = &self.cached_replies.lock().unwrap().calendar_home_set { return Ok(h.clone()); } let principal_url = self.get_principal().await?; @@ -145,16 +151,88 @@ impl Client { let href = self.sub_request_and_process(&principal_url, HOMESET_BODY.into(), &["calendar-home-set", "href"]).await?; let mut chs_url = self.url.clone(); chs_url.set_path(&href); - self.calendar_home_set = Some(chs_url.clone()); + self.cached_replies.lock().unwrap().calendar_home_set = Some(chs_url.clone()); log::debug!("Calendar home set URL is {:?}", chs_url.path()); Ok(chs_url) } + async fn populate_calendars(&self) -> Result<(), Box> { + 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(), 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 { /// Return the list of calendars, or fetch from server if not known yet - pub async fn get_calendars(&mut self) -> Result, Box> { - if let Some(c) = &self.calendars { - return Ok(c.clone()); + /* + 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?; @@ -210,37 +288,36 @@ impl Client { }, Ok(sc) => sc, }; - let this_calendar = CachedCalendar::new(display_name, this_calendar_url, supported_components); + 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); } - self.calendars = Some(calendars.clone()); - Ok(calendars) + replies.calendars = Some(calendars); + Ok(&calendars) + } + */ + + + 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()) + }; } - pub async fn get_tasks(&mut self, calendar: &CalendarId) -> Result<(), Box> { - let method = Method::from_bytes(b"REPORT") - .expect("cannot create REPORT method."); - let res = reqwest::Client::new() - .request(method, calendar.as_str()) - .header("Depth", 1) - .header(CONTENT_TYPE, "application/xml") - .basic_auth(self.username.clone(), Some(self.password.clone())) - .body(TASKS_BODY) - .send() - .await?; - let text = res.text().await?; - - let el: Element = text.parse().unwrap(); - let responses = find_elems(&el, "response"); - - for _response in responses { - println!("(a response)\n"); + async fn get_calendar(&self, id: CalendarId) -> Option>> { + self.cached_replies.lock().unwrap() + .calendars + .as_ref() + .and_then(|cals| cals.get(&id)) + .map(|cal| cal.clone()) } - Ok(()) - } } diff --git a/src/provider.rs b/src/provider.rs index de804fa..7a047db 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -63,16 +63,19 @@ where pub async fn sync(&mut self) -> Result<(), Box> { let last_sync = self.local.get_last_sync(); log::info!("Starting a sync. Last sync was at {:?}", last_sync); - let cals_server = self.server.get_calendars_mut().await?; + let cals_server = self.server.get_calendars().await?; - for (id, cal_server) in cals_server { - let cal_local = match self.local.get_calendar_mut(id).await { + for (id, mut cal_server) in cals_server { + let mut cal_server = cal_server.lock().unwrap(); + + let cal_local = match self.local.get_calendar(id).await { None => { log::error!("TODO: implement here"); continue; }, Some(cal) => cal, }; + let mut cal_local = cal_local.lock().unwrap(); // Pull remote changes from the server let mut tasks_id_to_remove_from_local = match last_sync { @@ -93,7 +96,7 @@ where tasks_to_add_to_local.push((*new_item).clone()); } // Even in case of conflicts, "the server always wins", so it is safe to remove tasks from the local cache as soon as now - remove_from_calendar(&tasks_id_to_remove_from_local, cal_local); + remove_from_calendar(&tasks_id_to_remove_from_local, &mut *cal_local); @@ -121,9 +124,9 @@ where tasks_to_add_to_server.push((*new_item).clone()); } - remove_from_calendar(&tasks_id_to_remove_from_server, cal_server); - move_to_calendar(&mut tasks_to_add_to_local, cal_local); - move_to_calendar(&mut tasks_to_add_to_server, cal_server); + remove_from_calendar(&tasks_id_to_remove_from_server, &mut *cal_server); + move_to_calendar(&mut tasks_to_add_to_local, &mut *cal_local); + move_to_calendar(&mut tasks_to_add_to_server, &mut *cal_server); } self.local.update_last_sync(None); diff --git a/src/traits.rs b/src/traits.rs index 1a256de..a31e1c6 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,5 +1,6 @@ use std::error::Error; use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -12,16 +13,9 @@ use crate::calendar::CalendarId; pub trait CalDavSource { /// Returns the current calendars that this source contains /// This function may trigger an update (that can be a long process, or that can even fail, e.g. in case of a remote server) - async fn get_calendars(&self) -> Result<&HashMap, Box>; - /// Returns the current calendars that this source contains - /// This function may trigger an update (that can be a long process, or that can even fail, e.g. in case of a remote server) - async fn get_calendars_mut(&mut self) -> Result, Box>; - + async fn get_calendars(&self) -> Result>>, Box>; /// Returns the calendar matching the ID - async fn get_calendar(&self, id: CalendarId) -> Option<&T>; - /// Returns the calendar matching the ID - async fn get_calendar_mut(&mut self, id: CalendarId) -> Option<&mut T>; - + async fn get_calendar(&self, id: CalendarId) -> Option>>; } pub trait SyncSlave { diff --git a/src/utils.rs b/src/utils.rs index bb12600..b5c900c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,7 @@ ///! Some utility functions use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use minidom::Element; @@ -56,10 +57,13 @@ pub fn print_xml(element: &Element) { } /// A debug utility that pretty-prints calendars -pub fn print_calendar_list(cals: &HashMap) { +pub fn print_calendar_list(cals: &HashMap>>) +where + C: CompleteCalendar, +{ for (id, cal) in cals { println!("CAL {}", id); - for (_, item) in cal.get_items() { + 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()); diff --git a/tests/sync.rs b/tests/sync.rs index 35a02fd..a9040c4 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use std::sync::{Arc, Mutex}; use chrono::{Utc, TimeZone}; use url::Url; @@ -20,10 +21,11 @@ async fn test_regular_sync() { let mut provider = populate_test_provider().await; provider.sync().await.unwrap(); + let cals_server = provider.server().get_calendars().await.unwrap(); + my_tasks::utils::print_calendar_list(&cals_server); let cals_local = provider.local().get_calendars().await.unwrap(); - my_tasks::utils::print_calendar_list(cals_local); - my_tasks::utils::print_calendar_list(cals_server); + my_tasks::utils::print_calendar_list(&cals_local); assert!(provider.server().has_same_contents_than(provider.local()).await.unwrap()); @@ -94,12 +96,13 @@ async fn populate_test_provider() -> Provider Provider