More simple clippy fixes
This commit is contained in:
parent
43aca1b492
commit
2b213ef635
2 changed files with 236 additions and 127 deletions
176
src/cache.rs
176
src/cache.rs
|
@ -1,22 +1,22 @@
|
||||||
//! This module provides a local cache for CalDAV data
|
//! 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::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::error::Error;
|
||||||
use std::ffi::OsStr;
|
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 async_trait::async_trait;
|
||||||
use csscolorparser::Color;
|
use csscolorparser::Color;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::traits::CalDavSource;
|
|
||||||
use crate::traits::BaseCalendar;
|
|
||||||
use crate::traits::CompleteCalendar;
|
|
||||||
use crate::calendar::cached_calendar::CachedCalendar;
|
use crate::calendar::cached_calendar::CachedCalendar;
|
||||||
use crate::calendar::SupportedComponents;
|
use crate::calendar::SupportedComponents;
|
||||||
|
use crate::traits::BaseCalendar;
|
||||||
|
use crate::traits::CalDavSource;
|
||||||
|
use crate::traits::CompleteCalendar;
|
||||||
|
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
use crate::mock_behaviour::MockBehaviour;
|
use crate::mock_behaviour::MockBehaviour;
|
||||||
|
@ -52,10 +52,9 @@ impl Cache {
|
||||||
self.mock_behaviour = mock_behaviour;
|
self.mock_behaviour = mock_behaviour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Get the path to the cache folder
|
/// Get the path to the cache folder
|
||||||
pub fn cache_folder() -> PathBuf {
|
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.
|
/// 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) {
|
let mut data: CachedData = match std::fs::File::open(&main_file) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!("Unable to open file {:?}: {}", main_file, err).into());
|
return Err(format!("Unable to open file {:?}: {}", main_file, err).into());
|
||||||
},
|
}
|
||||||
Ok(file) => serde_json::from_reader(file)?,
|
Ok(file) => serde_json::from_reader(file)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,25 +75,30 @@ impl Cache {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Unable to read dir: {:?}", err);
|
log::error!("Unable to read dir: {:?}", err);
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
Ok(entry) => {
|
Ok(entry) => {
|
||||||
let cal_path = entry.path();
|
let cal_path = entry.path();
|
||||||
log::debug!("Considering {:?}", cal_path);
|
log::debug!("Considering {:?}", cal_path);
|
||||||
if cal_path.extension() == Some(OsStr::new("cal")) {
|
if cal_path.extension() == Some(OsStr::new("cal")) {
|
||||||
match Self::load_calendar(&cal_path) {
|
match Self::load_calendar(&cal_path) {
|
||||||
Err(err) => {
|
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;
|
continue;
|
||||||
},
|
}
|
||||||
Ok(cal) =>
|
Ok(cal) => data
|
||||||
data.calendars.insert(cal.url().clone(), Arc::new(Mutex::new(cal))),
|
.calendars
|
||||||
|
.insert(cal.url().clone(), Arc::new(Mutex::new(cal))),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self{
|
Ok(Self {
|
||||||
backing_folder: PathBuf::from(folder),
|
backing_folder: PathBuf::from(folder),
|
||||||
data,
|
data,
|
||||||
|
|
||||||
|
@ -104,13 +108,13 @@ impl Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_calendar(path: &Path) -> Result<CachedCalendar, Box<dyn Error>> {
|
fn load_calendar(path: &Path) -> Result<CachedCalendar, Box<dyn Error>> {
|
||||||
let file = std::fs::File::open(&path)?;
|
let file = std::fs::File::open(path)?;
|
||||||
Ok(serde_json::from_reader(file)?)
|
Ok(serde_json::from_reader(file)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a cache with the default contents
|
/// Initialize a cache with the default contents
|
||||||
pub fn new(folder_path: &Path) -> Self {
|
pub fn new(folder_path: &Path) -> Self {
|
||||||
Self{
|
Self {
|
||||||
backing_folder: PathBuf::from(folder_path),
|
backing_folder: PathBuf::from(folder_path),
|
||||||
data: CachedData::default(),
|
data: CachedData::default(),
|
||||||
|
|
||||||
|
@ -143,16 +147,18 @@ impl Cache {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Compares two Caches to check they have the same current content
|
/// 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
|
/// 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"))]
|
#[cfg(any(test, feature = "integration_tests"))]
|
||||||
pub async fn has_same_observable_content_as(&self, other: &Self) -> Result<bool, Box<dyn Error>> {
|
pub async fn has_same_observable_content_as(
|
||||||
|
&self,
|
||||||
|
other: &Self,
|
||||||
|
) -> Result<bool, Box<dyn Error>> {
|
||||||
let calendars_l = self.get_calendars().await?;
|
let calendars_l = self.get_calendars().await?;
|
||||||
let calendars_r = other.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");
|
log::debug!("Different keys for calendars");
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
@ -166,11 +172,10 @@ impl Cache {
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: check calendars have the same names/ID/whatever
|
// 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");
|
log::debug!("Different calendars");
|
||||||
return Ok(false)
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
@ -179,32 +184,43 @@ impl Cache {
|
||||||
impl Drop for Cache {
|
impl Drop for Cache {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Err(err) = self.save_to_folder() {
|
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 {
|
impl Cache {
|
||||||
/// The non-async version of [`crate::traits::CalDavSource::get_calendars`]
|
/// The non-async version of [`crate::traits::CalDavSource::get_calendars`]
|
||||||
pub fn get_calendars_sync(&self) -> Result<HashMap<Url, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
|
pub fn get_calendars_sync(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<Url, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[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()))
|
.map(|(url, cal)| (url.clone(), cal.clone()))
|
||||||
.collect()
|
.collect())
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The non-async version of [`crate::traits::CalDavSource::get_calendar`]
|
/// The non-async version of [`crate::traits::CalDavSource::get_calendar`]
|
||||||
pub fn get_calendar_sync(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
|
pub fn get_calendar_sync(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
|
||||||
self.data.calendars.get(url).map(|arc| arc.clone())
|
self.data.calendars.get(url).cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CalDavSource<CachedCalendar> for Cache {
|
impl CalDavSource<CachedCalendar> for Cache {
|
||||||
async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
|
async fn get_calendars(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<Url, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
|
||||||
self.get_calendars_sync()
|
self.get_calendars_sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,21 +228,33 @@ impl CalDavSource<CachedCalendar> for Cache {
|
||||||
self.get_calendar_sync(url)
|
self.get_calendar_sync(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> {
|
async fn create_calendar(
|
||||||
|
&mut self,
|
||||||
|
url: Url,
|
||||||
|
name: String,
|
||||||
|
supported_components: SupportedComponents,
|
||||||
|
color: Option<Color>,
|
||||||
|
) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> {
|
||||||
log::debug!("Inserting local calendar {}", url);
|
log::debug!("Inserting local calendar {}", url);
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[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 new_calendar = CachedCalendar::new(name, url.clone(), supported_components, color);
|
||||||
let arc = Arc::new(Mutex::new(new_calendar));
|
let arc = Arc::new(Mutex::new(new_calendar));
|
||||||
|
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
if let Some(behaviour) = &self.mock_behaviour {
|
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()) {
|
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),
|
None => Ok(arc),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,38 +264,54 @@ impl CalDavSource<CachedCalendar> for Cache {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use url::Url;
|
|
||||||
use crate::calendar::SupportedComponents;
|
use crate::calendar::SupportedComponents;
|
||||||
use crate::item::Item;
|
use crate::item::Item;
|
||||||
use crate::task::Task;
|
use crate::task::Task;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
async fn populate_cache(cache_path: &Path) -> Cache {
|
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(
|
let _shopping_list = cache
|
||||||
Url::parse("https://caldav.com/shopping").unwrap(),
|
.create_calendar(
|
||||||
"My shopping list".to_string(),
|
Url::parse("https://caldav.com/shopping").unwrap(),
|
||||||
SupportedComponents::TODO,
|
"My shopping list".to_string(),
|
||||||
Some(csscolorparser::parse("lime").unwrap()),
|
SupportedComponents::TODO,
|
||||||
).await.unwrap();
|
Some(csscolorparser::parse("lime").unwrap()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let bucket_list = cache.create_calendar(
|
let bucket_list = cache
|
||||||
Url::parse("https://caldav.com/bucket-list").unwrap(),
|
.create_calendar(
|
||||||
"My bucket list".to_string(),
|
Url::parse("https://caldav.com/bucket-list").unwrap(),
|
||||||
SupportedComponents::TODO,
|
"My bucket list".to_string(),
|
||||||
Some(csscolorparser::parse("#ff8000").unwrap()),
|
SupportedComponents::TODO,
|
||||||
).await.unwrap();
|
Some(csscolorparser::parse("#ff8000").unwrap()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut bucket_list = bucket_list.lock().unwrap();
|
let mut bucket_list = bucket_list.lock().unwrap();
|
||||||
let cal_url = bucket_list.url().clone();
|
let cal_url = bucket_list.url().clone();
|
||||||
bucket_list.add_item(Item::Task(Task::new(
|
bucket_list
|
||||||
String::from("Attend a concert of JS Bach"), false, &cal_url
|
.add_item(Item::Task(Task::new(
|
||||||
))).await.unwrap();
|
String::from("Attend a concert of JS Bach"),
|
||||||
|
false,
|
||||||
|
&cal_url,
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
bucket_list.add_item(Item::Task(Task::new(
|
bucket_list
|
||||||
String::from("Climb the Lighthouse of Alexandria"), true, &cal_url
|
.add_item(Item::Task(Task::new(
|
||||||
))).await.unwrap();
|
String::from("Climb the Lighthouse of Alexandria"),
|
||||||
|
true,
|
||||||
|
&cal_url,
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
cache
|
cache
|
||||||
|
@ -285,7 +329,7 @@ mod tests {
|
||||||
assert_eq!(cache.backing_folder, retrieved_cache.backing_folder);
|
assert_eq!(cache.backing_folder, retrieved_cache.backing_folder);
|
||||||
let test = cache.has_same_observable_content_as(&retrieved_cache).await;
|
let test = cache.has_same_observable_content_as(&retrieved_cache).await;
|
||||||
println!("Equal? {:?}", test);
|
println!("Equal? {:?}", test);
|
||||||
assert_eq!(test.unwrap(), true);
|
assert!(test.unwrap(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -295,12 +339,14 @@ mod tests {
|
||||||
let mut cache = populate_cache(&cache_path).await;
|
let mut cache = populate_cache(&cache_path).await;
|
||||||
|
|
||||||
// We should not be able to add a second calendar with the same URL
|
// We should not be able to add a second calendar with the same URL
|
||||||
let second_addition_same_calendar = cache.create_calendar(
|
let second_addition_same_calendar = cache
|
||||||
Url::parse("https://caldav.com/shopping").unwrap(),
|
.create_calendar(
|
||||||
"My shopping list".to_string(),
|
Url::parse("https://caldav.com/shopping").unwrap(),
|
||||||
SupportedComponents::TODO,
|
"My shopping list".to_string(),
|
||||||
None,
|
SupportedComponents::TODO,
|
||||||
).await;
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
assert!(second_addition_same_calendar.is_err());
|
assert!(second_addition_same_calendar.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
187
src/client.rs
187
src/client.rs
|
@ -1,25 +1,24 @@
|
||||||
//! This module provides a client to connect to a CalDAV server
|
//! 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::collections::HashMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::error::Error;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use reqwest::{Method, StatusCode};
|
|
||||||
use reqwest::header::CONTENT_TYPE;
|
|
||||||
use minidom::Element;
|
|
||||||
use url::Url;
|
|
||||||
use csscolorparser::Color;
|
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::remote_calendar::RemoteCalendar;
|
||||||
use crate::calendar::SupportedComponents;
|
use crate::calendar::SupportedComponents;
|
||||||
use crate::traits::CalDavSource;
|
use crate::resource::Resource;
|
||||||
use crate::traits::BaseCalendar;
|
use crate::traits::BaseCalendar;
|
||||||
|
use crate::traits::CalDavSource;
|
||||||
use crate::traits::DavCalendar;
|
use crate::traits::DavCalendar;
|
||||||
|
use crate::utils::{find_elem, find_elems};
|
||||||
|
|
||||||
static DAVCLIENT_BODY: &str = r#"
|
static DAVCLIENT_BODY: &str = r#"
|
||||||
<d:propfind xmlns:d="DAV:">
|
<d:propfind xmlns:d="DAV:">
|
||||||
|
@ -49,11 +48,13 @@ static CAL_BODY: &str = r#"
|
||||||
</d:propfind>
|
</d:propfind>
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
pub(crate) async fn sub_request(
|
||||||
|
resource: &Resource,
|
||||||
pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String, depth: u32) -> Result<String, Box<dyn Error>> {
|
method: &str,
|
||||||
let method = method.parse()
|
body: String,
|
||||||
.expect("invalid method name");
|
depth: u32,
|
||||||
|
) -> Result<String, Box<dyn Error>> {
|
||||||
|
let method = method.parse().expect("invalid method name");
|
||||||
|
|
||||||
let res = reqwest::Client::new()
|
let res = reqwest::Client::new()
|
||||||
.request(method, resource.url().clone())
|
.request(method, resource.url().clone())
|
||||||
|
@ -64,7 +65,7 @@ pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String,
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if res.status().is_success() == false {
|
if !res.status().is_success() {
|
||||||
return Err(format!("Unexpected HTTP status code {:?}", res.status()).into());
|
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)
|
Ok(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn sub_request_and_extract_elem(resource: &Resource, body: String, items: &[&str]) -> Result<String, Box<dyn Error>> {
|
pub(crate) async fn sub_request_and_extract_elem(
|
||||||
|
resource: &Resource,
|
||||||
|
body: String,
|
||||||
|
items: &[&str],
|
||||||
|
) -> Result<String, Box<dyn Error>> {
|
||||||
let text = sub_request(resource, "PROPFIND", body, 0).await?;
|
let text = sub_request(resource, "PROPFIND", body, 0).await?;
|
||||||
|
|
||||||
let mut current_element: &Element = &text.parse()?;
|
let mut current_element: &Element = &text.parse()?;
|
||||||
for item in items {
|
for item in items {
|
||||||
current_element = match find_elem(¤t_element, item) {
|
current_element = match find_elem(current_element, item) {
|
||||||
Some(elem) => elem,
|
Some(elem) => elem,
|
||||||
None => return Err(format!("missing element {}", item).into()),
|
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())
|
Ok(current_element.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn sub_request_and_extract_elems(resource: &Resource, method: &str, body: String, item: &str) -> Result<Vec<Element>, Box<dyn Error>> {
|
pub(crate) async fn sub_request_and_extract_elems(
|
||||||
|
resource: &Resource,
|
||||||
|
method: &str,
|
||||||
|
body: String,
|
||||||
|
item: &str,
|
||||||
|
) -> Result<Vec<Element>, Box<dyn Error>> {
|
||||||
let text = sub_request(resource, method, body, 1).await?;
|
let text = sub_request(resource, method, body, 1).await?;
|
||||||
|
|
||||||
let element: &Element = &text.parse()?;
|
let element: &Element = &text.parse()?;
|
||||||
Ok(find_elems(&element, item)
|
Ok(find_elems(element, item)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|elem| (*elem).clone())
|
.map(|elem| (*elem).clone())
|
||||||
.collect()
|
.collect())
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A CalDAV data source that fetches its data from a CalDAV server
|
/// A CalDAV data source that fetches its data from a CalDAV server
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
|
@ -107,7 +115,6 @@ pub struct Client {
|
||||||
cached_replies: Mutex<CachedReplies>,
|
cached_replies: Mutex<CachedReplies>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct CachedReplies {
|
struct CachedReplies {
|
||||||
principal: Option<Resource>,
|
principal: Option<Resource>,
|
||||||
|
@ -117,10 +124,14 @@ struct CachedReplies {
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a client. This does not start a connection
|
/// Create a client. This does not start a connection
|
||||||
pub fn new<S: AsRef<str>, T: ToString, U: ToString>(url: S, username: T, password: U) -> Result<Self, Box<dyn Error>> {
|
pub fn new<S: AsRef<str>, T: ToString, U: ToString>(
|
||||||
|
url: S,
|
||||||
|
username: T,
|
||||||
|
password: U,
|
||||||
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
let url = Url::parse(url.as_ref())?;
|
let url = Url::parse(url.as_ref())?;
|
||||||
|
|
||||||
Ok(Self{
|
Ok(Self {
|
||||||
resource: Resource::new(url, username.to_string(), password.to_string()),
|
resource: Resource::new(url, username.to_string(), password.to_string()),
|
||||||
cached_replies: Mutex::new(CachedReplies::default()),
|
cached_replies: Mutex::new(CachedReplies::default()),
|
||||||
})
|
})
|
||||||
|
@ -132,12 +143,17 @@ impl Client {
|
||||||
return Ok(p.clone());
|
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);
|
let principal_url = self.resource.combine(&href);
|
||||||
self.cached_replies.lock().unwrap().principal = Some(principal_url.clone());
|
self.cached_replies.lock().unwrap().principal = Some(principal_url.clone());
|
||||||
log::debug!("Principal URL is {}", href);
|
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
|
/// 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 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);
|
let chs_url = self.resource.combine(&href);
|
||||||
self.cached_replies.lock().unwrap().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 {:?}", href);
|
log::debug!("Calendar home set URL is {:?}", href);
|
||||||
|
@ -158,10 +179,18 @@ impl Client {
|
||||||
async fn populate_calendars(&self) -> Result<(), Box<dyn Error>> {
|
async fn populate_calendars(&self) -> Result<(), Box<dyn Error>> {
|
||||||
let cal_home_set = self.get_cal_home_set().await?;
|
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();
|
let mut calendars = HashMap::new();
|
||||||
for rep in reps {
|
for rep in reps {
|
||||||
let display_name = find_elem(&rep, "displayname").map(|e| e.text()).unwrap_or("<no name>".to_string());
|
let display_name = find_elem(&rep, "displayname")
|
||||||
|
.map(|e| e.text())
|
||||||
|
.unwrap_or("<no name>".to_string());
|
||||||
log::debug!("Considering calendar {}", display_name);
|
log::debug!("Considering calendar {}", display_name);
|
||||||
|
|
||||||
// We filter out non-calendar items
|
// We filter out non-calendar items
|
||||||
|
@ -176,7 +205,7 @@ impl Client {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if found_calendar_type == false {
|
if !found_calendar_type {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,48 +222,60 @@ impl Client {
|
||||||
None => {
|
None => {
|
||||||
log::warn!("Calendar {} has no URL! Ignoring it.", display_name);
|
log::warn!("Calendar {} has no URL! Ignoring it.", display_name);
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
Some(h) => h.text(),
|
Some(h) => h.text(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let this_calendar_url = self.resource.combine(&calendar_href);
|
let this_calendar_url = self.resource.combine(&calendar_href);
|
||||||
|
|
||||||
let supported_components = match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) {
|
let supported_components =
|
||||||
Err(err) => {
|
match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) {
|
||||||
log::warn!("Calendar {} has invalid supported components ({})! Ignoring it.", display_name, err);
|
Err(err) => {
|
||||||
continue;
|
log::warn!(
|
||||||
},
|
"Calendar {} has invalid supported components ({})! Ignoring it.",
|
||||||
Ok(sc) => sc,
|
display_name,
|
||||||
};
|
err
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(sc) => sc,
|
||||||
|
};
|
||||||
|
|
||||||
let this_calendar_color = find_elem(&rep, "calendar-color")
|
let this_calendar_color = find_elem(&rep, "calendar-color").and_then(|col| {
|
||||||
.and_then(|col| {
|
col.texts()
|
||||||
col.texts().next()
|
.next()
|
||||||
.and_then(|t| csscolorparser::parse(t).ok())
|
.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());
|
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();
|
let mut replies = self.cached_replies.lock().unwrap();
|
||||||
replies.calendars = Some(calendars);
|
replies.calendars = Some(calendars);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CalDavSource<RemoteCalendar> for Client {
|
impl CalDavSource<RemoteCalendar> for Client {
|
||||||
async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
|
async fn get_calendars(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<Url, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
|
||||||
self.populate_calendars().await?;
|
self.populate_calendars().await?;
|
||||||
|
|
||||||
match &self.cached_replies.lock().unwrap().calendars {
|
match &self.cached_replies.lock().unwrap().calendars {
|
||||||
Some(cals) => {
|
Some(cals) => return Ok(cals.clone()),
|
||||||
return Ok(cals.clone())
|
None => return Err("No calendars available".into()),
|
||||||
},
|
|
||||||
None => return Err("No calendars available".into())
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,14 +285,22 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cached_replies.lock().unwrap()
|
self.cached_replies
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
.calendars
|
.calendars
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|cals| cals.get(url))
|
.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<Color>) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
|
async fn create_calendar(
|
||||||
|
&mut self,
|
||||||
|
url: Url,
|
||||||
|
name: String,
|
||||||
|
supported_components: SupportedComponents,
|
||||||
|
color: Option<Color>,
|
||||||
|
) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
|
||||||
self.populate_calendars().await?;
|
self.populate_calendars().await?;
|
||||||
|
|
||||||
match self.cached_replies.lock().unwrap().calendars.as_ref() {
|
match self.cached_replies.lock().unwrap().calendars.as_ref() {
|
||||||
|
@ -260,7 +309,7 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||||
if cals.contains_key(&url) {
|
if cals.contains_key(&url) {
|
||||||
return Err("This calendar already exists".into());
|
return Err("This calendar already exists".into());
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let creation_body = calendar_body(name, supported_components, color);
|
let creation_body = calendar_body(name, supported_components, color);
|
||||||
|
@ -275,21 +324,35 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if status != StatusCode::CREATED {
|
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<Color>) -> String {
|
fn calendar_body(
|
||||||
|
name: String,
|
||||||
|
supported_components: SupportedComponents,
|
||||||
|
color: Option<Color>,
|
||||||
|
) -> String {
|
||||||
let color_property = match color {
|
let color_property = match color {
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
Some(color) => format!("<D:calendar-color xmlns:D=\"http://apple.com/ns/ical/\">{}FF</D:calendar-color>", color.to_hex_string().to_ascii_uppercase()),
|
Some(color) => format!(
|
||||||
|
"<D:calendar-color xmlns:D=\"http://apple.com/ns/ical/\">{}FF</D:calendar-color>",
|
||||||
|
color.to_hex_string().to_ascii_uppercase()
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is taken from https://tools.ietf.org/html/rfc4791#page-24
|
// This is taken from https://tools.ietf.org/html/rfc4791#page-24
|
||||||
format!(r#"<?xml version="1.0" encoding="utf-8" ?>
|
format!(
|
||||||
|
r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
|
<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
|
||||||
<A:set xmlns:A="DAV:">
|
<A:set xmlns:A="DAV:">
|
||||||
<A:prop>
|
<A:prop>
|
||||||
|
|
Loading…
Add table
Reference in a new issue