More ID -> URL renaming
This commit is contained in:
parent
2f7c14d0aa
commit
0075ebfbdb
14 changed files with 244 additions and 253 deletions
|
@ -6,7 +6,7 @@ use chrono::{Utc};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use kitchen_fridge::{client::Client, traits::CalDavSource};
|
use kitchen_fridge::{client::Client, traits::CalDavSource};
|
||||||
use kitchen_fridge::calendar::{CalendarId, SupportedComponents};
|
use kitchen_fridge::calendar::SupportedComponents;
|
||||||
use kitchen_fridge::Item;
|
use kitchen_fridge::Item;
|
||||||
use kitchen_fridge::Task;
|
use kitchen_fridge::Task;
|
||||||
use kitchen_fridge::task::CompletionStatus;
|
use kitchen_fridge::task::CompletionStatus;
|
||||||
|
@ -81,27 +81,27 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider)
|
||||||
pause();
|
pause();
|
||||||
|
|
||||||
// Create a new calendar...
|
// Create a new calendar...
|
||||||
let new_calendar_id: CalendarId = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
|
let new_calendar_url: Url = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
|
||||||
let new_calendar_name = "A brave new calendar".to_string();
|
let new_calendar_name = "A brave new calendar".to_string();
|
||||||
if let Err(_err) = provider.local_mut()
|
if let Err(_err) = provider.local_mut()
|
||||||
.create_calendar(new_calendar_id.clone(), new_calendar_name.clone(), SupportedComponents::TODO, None)
|
.create_calendar(new_calendar_url.clone(), new_calendar_name.clone(), SupportedComponents::TODO, None)
|
||||||
.await {
|
.await {
|
||||||
println!("Unable to add calendar, maybe it exists already. We're not adding it after all.");
|
println!("Unable to add calendar, maybe it exists already. We're not adding it after all.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...and add a task in it
|
// ...and add a task in it
|
||||||
let new_name = "This is a new task in a new calendar";
|
let new_name = "This is a new task in a new calendar";
|
||||||
let new_task = Task::new(String::from(new_name), true, &new_calendar_id);
|
let new_task = Task::new(String::from(new_name), true, &new_calendar_url);
|
||||||
provider.local().get_calendar(&new_calendar_id).await.unwrap()
|
provider.local().get_calendar(&new_calendar_url).await.unwrap()
|
||||||
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
|
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
|
||||||
|
|
||||||
|
|
||||||
// Also create a task in a previously existing calendar
|
// Also create a task in a previously existing calendar
|
||||||
let changed_calendar_id: CalendarId = EXAMPLE_EXISTING_CALENDAR_URL.parse().unwrap();
|
let changed_calendar_url: Url = EXAMPLE_EXISTING_CALENDAR_URL.parse().unwrap();
|
||||||
let new_task_name = "This is a new task we're adding as an example, with ÜTF-8 characters";
|
let new_task_name = "This is a new task we're adding as an example, with ÜTF-8 characters";
|
||||||
let new_task = Task::new(String::from(new_task_name), false, &changed_calendar_id);
|
let new_task = Task::new(String::from(new_task_name), false, &changed_calendar_url);
|
||||||
let new_url = new_task.url().clone();
|
let new_url = new_task.url().clone();
|
||||||
provider.local().get_calendar(&changed_calendar_id).await.unwrap()
|
provider.local().get_calendar(&changed_calendar_url).await.unwrap()
|
||||||
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
|
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,20 +112,20 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider)
|
||||||
}
|
}
|
||||||
provider.local().save_to_folder().unwrap();
|
provider.local().save_to_folder().unwrap();
|
||||||
|
|
||||||
complete_item_and_sync_again(provider, &changed_calendar_id, &new_url).await;
|
complete_item_and_sync_again(provider, &changed_calendar_url, &new_url).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn complete_item_and_sync_again(
|
async fn complete_item_and_sync_again(
|
||||||
provider: &mut CalDavProvider,
|
provider: &mut CalDavProvider,
|
||||||
changed_calendar_id: &CalendarId,
|
changed_calendar_url: &Url,
|
||||||
url_to_complete: &Url)
|
url_to_complete: &Url)
|
||||||
{
|
{
|
||||||
println!("\nNow, we'll mark this last task as completed, and run the sync again.");
|
println!("\nNow, we'll mark this last task as completed, and run the sync again.");
|
||||||
pause();
|
pause();
|
||||||
|
|
||||||
let completion_status = CompletionStatus::Completed(Some(Utc::now()));
|
let completion_status = CompletionStatus::Completed(Some(Utc::now()));
|
||||||
provider.local().get_calendar(changed_calendar_id).await.unwrap()
|
provider.local().get_calendar(changed_calendar_url).await.unwrap()
|
||||||
.lock().unwrap().get_item_by_id_mut(url_to_complete).await.unwrap()
|
.lock().unwrap().get_item_by_url_mut(url_to_complete).await.unwrap()
|
||||||
.unwrap_task_mut()
|
.unwrap_task_mut()
|
||||||
.set_completion_status(completion_status);
|
.set_completion_status(completion_status);
|
||||||
|
|
||||||
|
@ -136,19 +136,19 @@ async fn complete_item_and_sync_again(
|
||||||
}
|
}
|
||||||
provider.local().save_to_folder().unwrap();
|
provider.local().save_to_folder().unwrap();
|
||||||
|
|
||||||
remove_items_and_sync_again(provider, changed_calendar_id, url_to_complete).await;
|
remove_items_and_sync_again(provider, changed_calendar_url, url_to_complete).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_items_and_sync_again(
|
async fn remove_items_and_sync_again(
|
||||||
provider: &mut CalDavProvider,
|
provider: &mut CalDavProvider,
|
||||||
changed_calendar_id: &CalendarId,
|
changed_calendar_url: &Url,
|
||||||
id_to_remove: &Url)
|
id_to_remove: &Url)
|
||||||
{
|
{
|
||||||
println!("\nNow, we'll delete this last task, and run the sync again.");
|
println!("\nNow, we'll delete this last task, and run the sync again.");
|
||||||
pause();
|
pause();
|
||||||
|
|
||||||
// Remove the task we had created
|
// Remove the task we had created
|
||||||
provider.local().get_calendar(changed_calendar_id).await.unwrap()
|
provider.local().get_calendar(changed_calendar_url).await.unwrap()
|
||||||
.lock().unwrap()
|
.lock().unwrap()
|
||||||
.mark_for_deletion(id_to_remove).await.unwrap();
|
.mark_for_deletion(id_to_remove).await.unwrap();
|
||||||
|
|
||||||
|
|
38
src/cache.rs
38
src/cache.rs
|
@ -10,12 +10,12 @@ use std::ffi::OsStr;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use csscolorparser::Color;
|
use csscolorparser::Color;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::traits::CalDavSource;
|
use crate::traits::CalDavSource;
|
||||||
use crate::traits::BaseCalendar;
|
use crate::traits::BaseCalendar;
|
||||||
use crate::traits::CompleteCalendar;
|
use crate::traits::CompleteCalendar;
|
||||||
use crate::calendar::cached_calendar::CachedCalendar;
|
use crate::calendar::cached_calendar::CachedCalendar;
|
||||||
use crate::calendar::CalendarId;
|
|
||||||
use crate::calendar::SupportedComponents;
|
use crate::calendar::SupportedComponents;
|
||||||
|
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
|
@ -41,7 +41,7 @@ pub struct Cache {
|
||||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||||
struct CachedData {
|
struct CachedData {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
calendars: HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>,
|
calendars: HashMap<Url, Arc<Mutex<CachedCalendar>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cache {
|
impl Cache {
|
||||||
|
@ -131,8 +131,8 @@ impl Cache {
|
||||||
serde_json::to_writer(file, &self.data)?;
|
serde_json::to_writer(file, &self.data)?;
|
||||||
|
|
||||||
// Save each calendar
|
// Save each calendar
|
||||||
for (cal_id, cal_mutex) in &self.data.calendars {
|
for (cal_url, cal_mutex) in &self.data.calendars {
|
||||||
let file_name = sanitize_filename::sanitize(cal_id.as_str()) + ".cal";
|
let file_name = sanitize_filename::sanitize(cal_url.as_str()) + ".cal";
|
||||||
let cal_file = folder.join(file_name);
|
let cal_file = folder.join(file_name);
|
||||||
let file = std::fs::File::create(&cal_file)?;
|
let file = std::fs::File::create(&cal_file)?;
|
||||||
let cal = cal_mutex.lock().unwrap();
|
let cal = cal_mutex.lock().unwrap();
|
||||||
|
@ -156,10 +156,10 @@ impl Cache {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (calendar_id, cal_l) in calendars_l {
|
for (calendar_url, cal_l) in calendars_l {
|
||||||
log::debug!("Comparing calendars {}", calendar_id);
|
log::debug!("Comparing calendars {}", calendar_url);
|
||||||
let cal_l = cal_l.lock().unwrap();
|
let cal_l = cal_l.lock().unwrap();
|
||||||
let cal_r = match calendars_r.get(&calendar_id) {
|
let cal_r = match calendars_r.get(&calendar_url) {
|
||||||
Some(c) => c.lock().unwrap(),
|
Some(c) => c.lock().unwrap(),
|
||||||
None => return Err("should not happen, we've just tested keys are the same".into()),
|
None => return Err("should not happen, we've just tested keys are the same".into()),
|
||||||
};
|
};
|
||||||
|
@ -185,38 +185,38 @@ impl Drop for Cache {
|
||||||
|
|
||||||
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<CalendarId, 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(|(id, cal)| (id.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, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> {
|
pub fn get_calendar_sync(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
|
||||||
self.data.calendars.get(id).map(|arc| arc.clone())
|
self.data.calendars.get(url).map(|arc| arc.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CalDavSource<CachedCalendar> for Cache {
|
impl CalDavSource<CachedCalendar> for Cache {
|
||||||
async fn get_calendars(&self) -> Result<HashMap<CalendarId, 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> {
|
async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
|
||||||
self.get_calendar_sync(id)
|
self.get_calendar_sync(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_calendar(&mut self, id: CalendarId, 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 {}", id);
|
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, id.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")]
|
||||||
|
@ -224,7 +224,7 @@ impl CalDavSource<CachedCalendar> for Cache {
|
||||||
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(id, 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),
|
||||||
}
|
}
|
||||||
|
@ -293,7 +293,7 @@ mod tests {
|
||||||
let cache_path = PathBuf::from(String::from("test_cache/sanity_tests"));
|
let cache_path = PathBuf::from(String::from("test_cache/sanity_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 id
|
// 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.create_calendar(
|
||||||
Url::parse("https://caldav.com/shopping").unwrap(),
|
Url::parse("https://caldav.com/shopping").unwrap(),
|
||||||
"My shopping list".to_string(),
|
"My shopping list".to_string(),
|
||||||
|
|
|
@ -8,7 +8,7 @@ use url::Url;
|
||||||
|
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
use crate::traits::{BaseCalendar, CompleteCalendar};
|
use crate::traits::{BaseCalendar, CompleteCalendar};
|
||||||
use crate::calendar::{CalendarId, SupportedComponents};
|
use crate::calendar::SupportedComponents;
|
||||||
use crate::Item;
|
use crate::Item;
|
||||||
|
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
|
@ -102,13 +102,13 @@ impl CachedCalendar {
|
||||||
log::debug!("Different keys for items");
|
log::debug!("Different keys for items");
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
for (id_l, item_l) in items_l {
|
for (url_l, item_l) in items_l {
|
||||||
let item_r = match items_r.get(&id_l) {
|
let item_r = match items_r.get(&url_l) {
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
None => return Err("should not happen, we've just tested keys are the same".into()),
|
None => return Err("should not happen, we've just tested keys are the same".into()),
|
||||||
};
|
};
|
||||||
if item_l.has_same_observable_content_as(&item_r) == false {
|
if item_l.has_same_observable_content_as(&item_r) == false {
|
||||||
log::debug!("Different items for id {}:", id_l);
|
log::debug!("Different items for URL {}:", url_l);
|
||||||
log::debug!("{:#?}", item_l);
|
log::debug!("{:#?}", item_l);
|
||||||
log::debug!("{:#?}", item_r);
|
log::debug!("{:#?}", item_r);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
@ -118,8 +118,8 @@ impl CachedCalendar {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The non-async version of [`Self::get_item_ids`]
|
/// The non-async version of [`Self::get_item_urls`]
|
||||||
pub fn get_item_ids_sync(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
pub fn get_item_urls_sync(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
||||||
Ok(self.items.iter()
|
Ok(self.items.iter()
|
||||||
.map(|(url, _)| url.clone())
|
.map(|(url, _)| url.clone())
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -134,14 +134,14 @@ impl CachedCalendar {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The non-async version of [`Self::get_item_by_id`]
|
/// The non-async version of [`Self::get_item_by_url`]
|
||||||
pub fn get_item_by_id_sync<'a>(&'a self, id: &Url) -> Option<&'a Item> {
|
pub fn get_item_by_url_sync<'a>(&'a self, url: &Url) -> Option<&'a Item> {
|
||||||
self.items.get(id)
|
self.items.get(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The non-async version of [`Self::get_item_by_id_mut`]
|
/// The non-async version of [`Self::get_item_by_url_mut`]
|
||||||
pub fn get_item_by_id_mut_sync<'a>(&'a mut self, id: &Url) -> Option<&'a mut Item> {
|
pub fn get_item_by_url_mut_sync<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item> {
|
||||||
self.items.get_mut(id)
|
self.items.get_mut(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The non-async version of [`Self::add_item`]
|
/// The non-async version of [`Self::add_item`]
|
||||||
|
@ -169,8 +169,8 @@ impl CachedCalendar {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The non-async version of [`Self::mark_for_deletion`]
|
/// The non-async version of [`Self::mark_for_deletion`]
|
||||||
pub fn mark_for_deletion_sync(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>> {
|
pub fn mark_for_deletion_sync(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||||
match self.items.get_mut(item_id) {
|
match self.items.get_mut(item_url) {
|
||||||
None => Err("no item for this key".into()),
|
None => Err("no item for this key".into()),
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
match item.sync_status() {
|
match item.sync_status() {
|
||||||
|
@ -188,7 +188,7 @@ impl CachedCalendar {
|
||||||
},
|
},
|
||||||
SyncStatus::NotSynced => {
|
SyncStatus::NotSynced => {
|
||||||
// This was never synced to the server, we can safely delete it as soon as now
|
// This was never synced to the server, we can safely delete it as soon as now
|
||||||
self.items.remove(item_id);
|
self.items.remove(item_url);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -197,9 +197,9 @@ impl CachedCalendar {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The non-async version of [`Self::immediately_delete_item`]
|
/// The non-async version of [`Self::immediately_delete_item`]
|
||||||
pub fn immediately_delete_item_sync(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>> {
|
pub fn immediately_delete_item_sync(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||||
match self.items.remove(item_id) {
|
match self.items.remove(item_url) {
|
||||||
None => Err(format!("Item {} is absent from this calendar", item_id).into()),
|
None => Err(format!("Item {} is absent from this calendar", item_url).into()),
|
||||||
Some(_) => Ok(())
|
Some(_) => Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,7 @@ impl BaseCalendar for CachedCalendar {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CompleteCalendar for CachedCalendar {
|
impl CompleteCalendar for CachedCalendar {
|
||||||
fn new(name: String, url: CalendarId, supported_components: SupportedComponents, color: Option<Color>) -> Self {
|
fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option<Color>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name, url, supported_components, color,
|
name, url, supported_components, color,
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
|
@ -245,28 +245,28 @@ impl CompleteCalendar for CachedCalendar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_item_ids(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
||||||
self.get_item_ids_sync()
|
self.get_item_urls_sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>> {
|
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>> {
|
||||||
self.get_items_sync()
|
self.get_items_sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_item_by_id<'a>(&'a self, id: &Url) -> Option<&'a Item> {
|
async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item> {
|
||||||
self.get_item_by_id_sync(id)
|
self.get_item_by_url_sync(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_item_by_id_mut<'a>(&'a mut self, id: &Url) -> Option<&'a mut Item> {
|
async fn get_item_by_url_mut<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item> {
|
||||||
self.get_item_by_id_mut_sync(id)
|
self.get_item_by_url_mut_sync(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mark_for_deletion(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>> {
|
async fn mark_for_deletion(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||||
self.mark_for_deletion_sync(item_id)
|
self.mark_for_deletion_sync(item_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn immediately_delete_item(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>> {
|
async fn immediately_delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||||
self.immediately_delete_item_sync(item_id)
|
self.immediately_delete_item_sync(item_url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,30 +294,30 @@ impl DavCalendar for CachedCalendar {
|
||||||
|
|
||||||
let mut result = HashMap::new();
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
for (id, item) in self.items.iter() {
|
for (url, item) in self.items.iter() {
|
||||||
let vt = match item.sync_status() {
|
let vt = match item.sync_status() {
|
||||||
SyncStatus::Synced(vt) => vt.clone(),
|
SyncStatus::Synced(vt) => vt.clone(),
|
||||||
_ => {
|
_ => {
|
||||||
panic!("Mock calendars must contain only SyncStatus::Synced. Got {:?}", item);
|
panic!("Mock calendars must contain only SyncStatus::Synced. Got {:?}", item);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
result.insert(id.clone(), vt);
|
result.insert(url.clone(), vt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_item_by_id(&self, id: &Url) -> Result<Option<Item>, Box<dyn Error>> {
|
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, 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_item_by_id())?;
|
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_url())?;
|
||||||
|
|
||||||
Ok(self.items.get(id).cloned())
|
Ok(self.items.get(url).cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_item(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>> {
|
async fn delete_item(&mut self, item_url: &Url) -> Result<(), 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_delete_item())?;
|
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_delete_item())?;
|
||||||
|
|
||||||
self.immediately_delete_item(item_id).await
|
self.immediately_delete_item(item_url).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,3 @@ impl Default for SearchFilter {
|
||||||
SearchFilter::All
|
SearchFilter::All
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub type CalendarId = url::Url;
|
|
||||||
|
|
|
@ -138,7 +138,7 @@ impl DavCalendar for RemoteCalendar {
|
||||||
for response in responses {
|
for response in responses {
|
||||||
let item_url = crate::utils::find_elem(&response, "href")
|
let item_url = crate::utils::find_elem(&response, "href")
|
||||||
.map(|elem| self.resource.combine(&elem.text()));
|
.map(|elem| self.resource.combine(&elem.text()));
|
||||||
let item_id = match item_url {
|
let item_url = match item_url {
|
||||||
None => {
|
None => {
|
||||||
log::warn!("Unable to extract HREF");
|
log::warn!("Unable to extract HREF");
|
||||||
continue;
|
continue;
|
||||||
|
@ -150,7 +150,7 @@ impl DavCalendar for RemoteCalendar {
|
||||||
|
|
||||||
let version_tag = match crate::utils::find_elem(&response, "getetag") {
|
let version_tag = match crate::utils::find_elem(&response, "getetag") {
|
||||||
None => {
|
None => {
|
||||||
log::warn!("Unable to extract ETAG for item {}, ignoring it", item_id);
|
log::warn!("Unable to extract ETAG for item {}, ignoring it", item_url);
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Some(etag) => {
|
Some(etag) => {
|
||||||
|
@ -158,7 +158,7 @@ impl DavCalendar for RemoteCalendar {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
items.insert(item_id.clone(), version_tag);
|
items.insert(item_url.clone(), version_tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: the mutex cannot be locked during this whole async function, but it can safely be re-entrant (this will just waste an unnecessary request)
|
// Note: the mutex cannot be locked during this whole async function, but it can safely be re-entrant (this will just waste an unnecessary request)
|
||||||
|
@ -166,9 +166,9 @@ impl DavCalendar for RemoteCalendar {
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_item_by_id(&self, id: &Url) -> Result<Option<Item>, Box<dyn Error>> {
|
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>> {
|
||||||
let res = reqwest::Client::new()
|
let res = reqwest::Client::new()
|
||||||
.get(id.clone())
|
.get(url.clone())
|
||||||
.header(CONTENT_TYPE, "text/calendar")
|
.header(CONTENT_TYPE, "text/calendar")
|
||||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||||
.send()
|
.send()
|
||||||
|
@ -182,18 +182,18 @@ impl DavCalendar for RemoteCalendar {
|
||||||
|
|
||||||
// This is supposed to be cached
|
// This is supposed to be cached
|
||||||
let version_tags = self.get_item_version_tags().await?;
|
let version_tags = self.get_item_version_tags().await?;
|
||||||
let vt = match version_tags.get(id) {
|
let vt = match version_tags.get(url) {
|
||||||
None => return Err(format!("Inconsistent data: {} has no version tag", id).into()),
|
None => return Err(format!("Inconsistent data: {} has no version tag", url).into()),
|
||||||
Some(vt) => vt,
|
Some(vt) => vt,
|
||||||
};
|
};
|
||||||
|
|
||||||
let item = crate::ical::parse(&text, id.clone(), SyncStatus::Synced(vt.clone()))?;
|
let item = crate::ical::parse(&text, url.clone(), SyncStatus::Synced(vt.clone()))?;
|
||||||
Ok(Some(item))
|
Ok(Some(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_item(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>> {
|
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||||
let del_response = reqwest::Client::new()
|
let del_response = reqwest::Client::new()
|
||||||
.delete(item_id.clone())
|
.delete(item_url.clone())
|
||||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -15,7 +15,6 @@ use csscolorparser::Color;
|
||||||
use crate::resource::Resource;
|
use crate::resource::Resource;
|
||||||
use crate::utils::{find_elem, find_elems};
|
use crate::utils::{find_elem, find_elems};
|
||||||
use crate::calendar::remote_calendar::RemoteCalendar;
|
use crate::calendar::remote_calendar::RemoteCalendar;
|
||||||
use crate::calendar::CalendarId;
|
|
||||||
use crate::calendar::SupportedComponents;
|
use crate::calendar::SupportedComponents;
|
||||||
use crate::traits::CalDavSource;
|
use crate::traits::CalDavSource;
|
||||||
use crate::traits::BaseCalendar;
|
use crate::traits::BaseCalendar;
|
||||||
|
@ -113,7 +112,7 @@ pub struct Client {
|
||||||
struct CachedReplies {
|
struct CachedReplies {
|
||||||
principal: Option<Resource>,
|
principal: Option<Resource>,
|
||||||
calendar_home_set: Option<Resource>,
|
calendar_home_set: Option<Resource>,
|
||||||
calendars: Option<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>>,
|
calendars: Option<HashMap<Url, Arc<Mutex<RemoteCalendar>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
|
@ -228,7 +227,7 @@ impl Client {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CalDavSource<RemoteCalendar> for Client {
|
impl CalDavSource<RemoteCalendar> for Client {
|
||||||
async fn get_calendars(&self) -> Result<HashMap<CalendarId, 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 {
|
||||||
|
@ -239,7 +238,7 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<RemoteCalendar>>> {
|
async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<RemoteCalendar>>> {
|
||||||
if let Err(err) = self.populate_calendars().await {
|
if let Err(err) = self.populate_calendars().await {
|
||||||
log::warn!("Unable to fetch calendars: {}", err);
|
log::warn!("Unable to fetch calendars: {}", err);
|
||||||
return None;
|
return None;
|
||||||
|
@ -248,17 +247,17 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||||
self.cached_replies.lock().unwrap()
|
self.cached_replies.lock().unwrap()
|
||||||
.calendars
|
.calendars
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|cals| cals.get(id))
|
.and_then(|cals| cals.get(url))
|
||||||
.map(|cal| cal.clone())
|
.map(|cal| cal.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_calendar(&mut self, id: CalendarId, 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() {
|
||||||
None => return Err("No calendars have been fetched".into()),
|
None => return Err("No calendars have been fetched".into()),
|
||||||
Some(cals) => {
|
Some(cals) => {
|
||||||
if cals.contains_key(&id) {
|
if cals.contains_key(&url) {
|
||||||
return Err("This calendar already exists".into());
|
return Err("This calendar already exists".into());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -267,7 +266,7 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||||
let creation_body = calendar_body(name, supported_components);
|
let creation_body = calendar_body(name, supported_components);
|
||||||
|
|
||||||
let response = reqwest::Client::new()
|
let response = reqwest::Client::new()
|
||||||
.request(Method::from_bytes(b"MKCALENDAR").unwrap(), id.clone())
|
.request(Method::from_bytes(b"MKCALENDAR").unwrap(), url.clone())
|
||||||
.header(CONTENT_TYPE, "application/xml")
|
.header(CONTENT_TYPE, "application/xml")
|
||||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||||
.body(creation_body)
|
.body(creation_body)
|
||||||
|
@ -279,7 +278,7 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||||
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(&id).await.ok_or(format!("Unable to insert calendar {:?}", id).into())
|
self.get_calendar(&url).await.ok_or(format!("Unable to insert calendar {:?}", url).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,12 +130,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_task(completed: bool) -> (String, String, String) {
|
fn build_task(completed: bool) -> (String, String, String) {
|
||||||
let cal_id = "http://my.calend.ar/id".parse().unwrap();
|
let cal_url = "http://my.calend.ar/id".parse().unwrap();
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let s_now = format_date_time(&now);
|
let s_now = format_date_time(&now);
|
||||||
|
|
||||||
let task = Item::Task(Task::new(
|
let task = Item::Task(Task::new(
|
||||||
String::from("This is a task with ÜTF-8 characters"), completed, &cal_id
|
String::from("This is a task with ÜTF-8 characters"), completed, &cal_url
|
||||||
));
|
));
|
||||||
|
|
||||||
let ical = build_from(&task).unwrap();
|
let ical = build_from(&task).unwrap();
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub struct MockBehaviour {
|
||||||
|
|
||||||
// From the DavCalendar trait
|
// From the DavCalendar trait
|
||||||
pub get_item_version_tags_behaviour: (u32, u32),
|
pub get_item_version_tags_behaviour: (u32, u32),
|
||||||
pub get_item_by_id_behaviour: (u32, u32),
|
pub get_item_by_url_behaviour: (u32, u32),
|
||||||
pub delete_item_behaviour: (u32, u32),
|
pub delete_item_behaviour: (u32, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ impl MockBehaviour {
|
||||||
add_item_behaviour: (0, n_fails),
|
add_item_behaviour: (0, n_fails),
|
||||||
update_item_behaviour: (0, n_fails),
|
update_item_behaviour: (0, n_fails),
|
||||||
get_item_version_tags_behaviour: (0, n_fails),
|
get_item_version_tags_behaviour: (0, n_fails),
|
||||||
get_item_by_id_behaviour: (0, n_fails),
|
get_item_by_url_behaviour: (0, n_fails),
|
||||||
delete_item_behaviour: (0, n_fails),
|
delete_item_behaviour: (0, n_fails),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,9 +84,9 @@ impl MockBehaviour {
|
||||||
if self.is_suspended { return Ok(()) }
|
if self.is_suspended { return Ok(()) }
|
||||||
decrement(&mut self.get_item_version_tags_behaviour, "get_item_version_tags")
|
decrement(&mut self.get_item_version_tags_behaviour, "get_item_version_tags")
|
||||||
}
|
}
|
||||||
pub fn can_get_item_by_id(&mut self) -> Result<(), Box<dyn Error>> {
|
pub fn can_get_item_by_url(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
if self.is_suspended { return Ok(()) }
|
if self.is_suspended { return Ok(()) }
|
||||||
decrement(&mut self.get_item_by_id_behaviour, "get_item_by_id")
|
decrement(&mut self.get_item_by_url_behaviour, "get_item_by_url")
|
||||||
}
|
}
|
||||||
pub fn can_delete_item(&mut self) -> Result<(), Box<dyn Error>> {
|
pub fn can_delete_item(&mut self) -> Result<(), Box<dyn Error>> {
|
||||||
if self.is_suspended { return Ok(()) }
|
if self.is_suspended { return Ok(()) }
|
||||||
|
|
|
@ -11,7 +11,6 @@ use url::Url;
|
||||||
use crate::traits::{BaseCalendar, CalDavSource, DavCalendar};
|
use crate::traits::{BaseCalendar, CalDavSource, DavCalendar};
|
||||||
use crate::traits::CompleteCalendar;
|
use crate::traits::CompleteCalendar;
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
use crate::calendar::CalendarId;
|
|
||||||
|
|
||||||
pub mod sync_progress;
|
pub mod sync_progress;
|
||||||
use sync_progress::SyncProgress;
|
use sync_progress::SyncProgress;
|
||||||
|
@ -103,39 +102,39 @@ where
|
||||||
|
|
||||||
// Sync every remote calendar
|
// Sync every remote calendar
|
||||||
let cals_remote = self.remote.get_calendars().await?;
|
let cals_remote = self.remote.get_calendars().await?;
|
||||||
for (cal_id, cal_remote) in cals_remote {
|
for (cal_url, cal_remote) in cals_remote {
|
||||||
let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_id, cal_remote.clone()).await {
|
let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_url, cal_remote.clone()).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
progress.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_id, err));
|
progress.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_url, err));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Ok(arc) => arc,
|
Ok(arc) => arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, progress).await {
|
if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, progress).await {
|
||||||
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err));
|
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
handled_calendars.insert(cal_id);
|
handled_calendars.insert(cal_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync every local calendar that would not be in the remote yet
|
// Sync every local calendar that would not be in the remote yet
|
||||||
let cals_local = self.local.get_calendars().await?;
|
let cals_local = self.local.get_calendars().await?;
|
||||||
for (cal_id, cal_local) in cals_local {
|
for (cal_url, cal_local) in cals_local {
|
||||||
if handled_calendars.contains(&cal_id) {
|
if handled_calendars.contains(&cal_url) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_id, cal_local.clone()).await {
|
let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_url, cal_local.clone()).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
progress.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_id, err));
|
progress.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_url, err));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Ok(arc) => arc,
|
Ok(arc) => arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, progress).await {
|
if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, progress).await {
|
||||||
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err));
|
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,11 +145,11 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn get_or_insert_local_counterpart_calendar(&mut self, cal_id: &CalendarId, needle: Arc<Mutex<U>>) -> Result<Arc<Mutex<T>>, Box<dyn Error>> {
|
async fn get_or_insert_local_counterpart_calendar(&mut self, cal_url: &Url, needle: Arc<Mutex<U>>) -> Result<Arc<Mutex<T>>, Box<dyn Error>> {
|
||||||
get_or_insert_counterpart_calendar("local", &mut self.local, cal_id, needle).await
|
get_or_insert_counterpart_calendar("local", &mut self.local, cal_url, needle).await
|
||||||
}
|
}
|
||||||
async fn get_or_insert_remote_counterpart_calendar(&mut self, cal_id: &CalendarId, needle: Arc<Mutex<T>>) -> Result<Arc<Mutex<U>>, Box<dyn Error>> {
|
async fn get_or_insert_remote_counterpart_calendar(&mut self, cal_url: &Url, needle: Arc<Mutex<T>>) -> Result<Arc<Mutex<U>>, Box<dyn Error>> {
|
||||||
get_or_insert_counterpart_calendar("remote", &mut self.remote, cal_id, needle).await
|
get_or_insert_counterpart_calendar("remote", &mut self.remote, cal_url, needle).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -180,52 +179,52 @@ where
|
||||||
details: format!("{} remote items", remote_items.len()),
|
details: format!("{} remote items", remote_items.len()),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut local_items_to_handle = cal_local.get_item_ids().await?;
|
let mut local_items_to_handle = cal_local.get_item_urls().await?;
|
||||||
for (id, remote_tag) in remote_items {
|
for (url, remote_tag) in remote_items {
|
||||||
progress.trace(&format!("***** Considering remote item {}...", id));
|
progress.trace(&format!("***** Considering remote item {}...", url));
|
||||||
match cal_local.get_item_by_id(&id).await {
|
match cal_local.get_item_by_url(&url).await {
|
||||||
None => {
|
None => {
|
||||||
// This was created on the remote
|
// This was created on the remote
|
||||||
progress.debug(&format!("* {} is a remote addition", id));
|
progress.debug(&format!("* {} is a remote addition", url));
|
||||||
remote_additions.insert(id);
|
remote_additions.insert(url);
|
||||||
},
|
},
|
||||||
Some(local_item) => {
|
Some(local_item) => {
|
||||||
if local_items_to_handle.remove(&id) == false {
|
if local_items_to_handle.remove(&url) == false {
|
||||||
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", id));
|
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url));
|
||||||
}
|
}
|
||||||
|
|
||||||
match local_item.sync_status() {
|
match local_item.sync_status() {
|
||||||
SyncStatus::NotSynced => {
|
SyncStatus::NotSynced => {
|
||||||
progress.error(&format!("ID reuse between remote and local sources ({}). Ignoring this item in the sync", id));
|
progress.error(&format!("URL reuse between remote and local sources ({}). Ignoring this item in the sync", url));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
SyncStatus::Synced(local_tag) => {
|
SyncStatus::Synced(local_tag) => {
|
||||||
if &remote_tag != local_tag {
|
if &remote_tag != local_tag {
|
||||||
// This has been modified on the remote
|
// This has been modified on the remote
|
||||||
progress.debug(&format!("* {} is a remote change", id));
|
progress.debug(&format!("* {} is a remote change", url));
|
||||||
remote_changes.insert(id);
|
remote_changes.insert(url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SyncStatus::LocallyModified(local_tag) => {
|
SyncStatus::LocallyModified(local_tag) => {
|
||||||
if &remote_tag == local_tag {
|
if &remote_tag == local_tag {
|
||||||
// This has been changed locally
|
// This has been changed locally
|
||||||
progress.debug(&format!("* {} is a local change", id));
|
progress.debug(&format!("* {} is a local change", url));
|
||||||
local_changes.insert(id);
|
local_changes.insert(url);
|
||||||
} else {
|
} else {
|
||||||
progress.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", id));
|
progress.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", url));
|
||||||
progress.debug(&format!("* {} is considered a remote change", id));
|
progress.debug(&format!("* {} is considered a remote change", url));
|
||||||
remote_changes.insert(id);
|
remote_changes.insert(url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SyncStatus::LocallyDeleted(local_tag) => {
|
SyncStatus::LocallyDeleted(local_tag) => {
|
||||||
if &remote_tag == local_tag {
|
if &remote_tag == local_tag {
|
||||||
// This has been locally deleted
|
// This has been locally deleted
|
||||||
progress.debug(&format!("* {} is a local deletion", id));
|
progress.debug(&format!("* {} is a local deletion", url));
|
||||||
local_del.insert(id);
|
local_del.insert(url);
|
||||||
} else {
|
} else {
|
||||||
progress.info(&format!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", id));
|
progress.info(&format!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", url));
|
||||||
progress.debug(&format!("* {} is a considered a remote change", id));
|
progress.debug(&format!("* {} is a considered a remote change", url));
|
||||||
remote_changes.insert(id);
|
remote_changes.insert(url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -234,11 +233,11 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also iterate on the local tasks that are not on the remote
|
// Also iterate on the local tasks that are not on the remote
|
||||||
for id in local_items_to_handle {
|
for url in local_items_to_handle {
|
||||||
progress.trace(&format!("##### Considering local item {}...", id));
|
progress.trace(&format!("##### Considering local item {}...", url));
|
||||||
let local_item = match cal_local.get_item_by_id(&id).await {
|
let local_item = match cal_local.get_item_by_url(&url).await {
|
||||||
None => {
|
None => {
|
||||||
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", id));
|
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Some(item) => item,
|
Some(item) => item,
|
||||||
|
@ -247,22 +246,22 @@ where
|
||||||
match local_item.sync_status() {
|
match local_item.sync_status() {
|
||||||
SyncStatus::Synced(_) => {
|
SyncStatus::Synced(_) => {
|
||||||
// This item has been removed from the remote
|
// This item has been removed from the remote
|
||||||
progress.debug(&format!("# {} is a deletion from the server", id));
|
progress.debug(&format!("# {} is a deletion from the server", url));
|
||||||
remote_del.insert(id);
|
remote_del.insert(url);
|
||||||
},
|
},
|
||||||
SyncStatus::NotSynced => {
|
SyncStatus::NotSynced => {
|
||||||
// This item has just been locally created
|
// This item has just been locally created
|
||||||
progress.debug(&format!("# {} has been locally created", id));
|
progress.debug(&format!("# {} has been locally created", url));
|
||||||
local_additions.insert(id);
|
local_additions.insert(url);
|
||||||
},
|
},
|
||||||
SyncStatus::LocallyDeleted(_) => {
|
SyncStatus::LocallyDeleted(_) => {
|
||||||
// This item has been deleted from both sources
|
// This item has been deleted from both sources
|
||||||
progress.debug(&format!("# {} has been deleted from both sources", id));
|
progress.debug(&format!("# {} has been deleted from both sources", url));
|
||||||
remote_del.insert(id);
|
remote_del.insert(url);
|
||||||
},
|
},
|
||||||
SyncStatus::LocallyModified(_) => {
|
SyncStatus::LocallyModified(_) => {
|
||||||
progress.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", id));
|
progress.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", url));
|
||||||
remote_del.insert(id);
|
remote_del.insert(url);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,80 +269,80 @@ where
|
||||||
|
|
||||||
// Step 2 - commit changes
|
// Step 2 - commit changes
|
||||||
progress.trace("Committing changes...");
|
progress.trace("Committing changes...");
|
||||||
for id_del in local_del {
|
for url_del in local_del {
|
||||||
progress.debug(&format!("> Pushing local deletion {} to the server", id_del));
|
progress.debug(&format!("> Pushing local deletion {} to the server", url_del));
|
||||||
progress.feedback(SyncEvent::InProgress{
|
progress.feedback(SyncEvent::InProgress{
|
||||||
calendar: cal_name.clone(),
|
calendar: cal_name.clone(),
|
||||||
details: Self::item_name(&cal_local, &id_del).await,
|
details: Self::item_name(&cal_local, &url_del).await,
|
||||||
});
|
});
|
||||||
match cal_remote.delete_item(&id_del).await {
|
match cal_remote.delete_item(&url_del).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
progress.warn(&format!("Unable to delete remote item {}: {}", id_del, err));
|
progress.warn(&format!("Unable to delete remote item {}: {}", url_del, err));
|
||||||
},
|
},
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
// Change the local copy from "marked to deletion" to "actually deleted"
|
// Change the local copy from "marked to deletion" to "actually deleted"
|
||||||
if let Err(err) = cal_local.immediately_delete_item(&id_del).await {
|
if let Err(err) = cal_local.immediately_delete_item(&url_del).await {
|
||||||
progress.error(&format!("Unable to permanently delete local item {}: {}", id_del, err));
|
progress.error(&format!("Unable to permanently delete local item {}: {}", url_del, err));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for id_del in remote_del {
|
for url_del in remote_del {
|
||||||
progress.debug(&format!("> Applying remote deletion {} locally", id_del));
|
progress.debug(&format!("> Applying remote deletion {} locally", url_del));
|
||||||
progress.feedback(SyncEvent::InProgress{
|
progress.feedback(SyncEvent::InProgress{
|
||||||
calendar: cal_name.clone(),
|
calendar: cal_name.clone(),
|
||||||
details: Self::item_name(&cal_local, &id_del).await,
|
details: Self::item_name(&cal_local, &url_del).await,
|
||||||
});
|
});
|
||||||
if let Err(err) = cal_local.immediately_delete_item(&id_del).await {
|
if let Err(err) = cal_local.immediately_delete_item(&url_del).await {
|
||||||
progress.warn(&format!("Unable to delete local item {}: {}", id_del, err));
|
progress.warn(&format!("Unable to delete local item {}: {}", url_del, err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for id_add in remote_additions {
|
for url_add in remote_additions {
|
||||||
progress.debug(&format!("> Applying remote addition {} locally", id_add));
|
progress.debug(&format!("> Applying remote addition {} locally", url_add));
|
||||||
progress.feedback(SyncEvent::InProgress{
|
progress.feedback(SyncEvent::InProgress{
|
||||||
calendar: cal_name.clone(),
|
calendar: cal_name.clone(),
|
||||||
details: Self::item_name(&cal_local, &id_add).await,
|
details: Self::item_name(&cal_local, &url_add).await,
|
||||||
});
|
});
|
||||||
match cal_remote.get_item_by_id(&id_add).await {
|
match cal_remote.get_item_by_url(&url_add).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it.", id_add, err));
|
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it.", url_add, err));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Ok(item) => match item {
|
Ok(item) => match item {
|
||||||
None => {
|
None => {
|
||||||
progress.error(&format!("Inconsistency: new item {} has vanished from the remote end", id_add));
|
progress.error(&format!("Inconsistency: new item {} has vanished from the remote end", url_add));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Some(new_item) => {
|
Some(new_item) => {
|
||||||
if let Err(err) = cal_local.add_item(new_item.clone()).await {
|
if let Err(err) = cal_local.add_item(new_item.clone()).await {
|
||||||
progress.error(&format!("Not able to add item {} to local calendar: {}", id_add, err));
|
progress.error(&format!("Not able to add item {} to local calendar: {}", url_add, err));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for id_change in remote_changes {
|
for url_change in remote_changes {
|
||||||
progress.debug(&format!("> Applying remote change {} locally", id_change));
|
progress.debug(&format!("> Applying remote change {} locally", url_change));
|
||||||
progress.feedback(SyncEvent::InProgress{
|
progress.feedback(SyncEvent::InProgress{
|
||||||
calendar: cal_name.clone(),
|
calendar: cal_name.clone(),
|
||||||
details: Self::item_name(&cal_local, &id_change).await,
|
details: Self::item_name(&cal_local, &url_change).await,
|
||||||
});
|
});
|
||||||
match cal_remote.get_item_by_id(&id_change).await {
|
match cal_remote.get_item_by_url(&url_change).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it", id_change, err));
|
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it", url_change, err));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Ok(item) => match item {
|
Ok(item) => match item {
|
||||||
None => {
|
None => {
|
||||||
progress.error(&format!("Inconsistency: modified item {} has vanished from the remote end", id_change));
|
progress.error(&format!("Inconsistency: modified item {} has vanished from the remote end", url_change));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
if let Err(err) = cal_local.update_item(item.clone()).await {
|
if let Err(err) = cal_local.update_item(item.clone()).await {
|
||||||
progress.error(&format!("Unable to update item {} in local calendar: {}", id_change, err));
|
progress.error(&format!("Unable to update item {} in local calendar: {}", url_change, err));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -351,20 +350,20 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for id_add in local_additions {
|
for url_add in local_additions {
|
||||||
progress.debug(&format!("> Pushing local addition {} to the server", id_add));
|
progress.debug(&format!("> Pushing local addition {} to the server", url_add));
|
||||||
progress.feedback(SyncEvent::InProgress{
|
progress.feedback(SyncEvent::InProgress{
|
||||||
calendar: cal_name.clone(),
|
calendar: cal_name.clone(),
|
||||||
details: Self::item_name(&cal_local, &id_add).await,
|
details: Self::item_name(&cal_local, &url_add).await,
|
||||||
});
|
});
|
||||||
match cal_local.get_item_by_id_mut(&id_add).await {
|
match cal_local.get_item_by_url_mut(&url_add).await {
|
||||||
None => {
|
None => {
|
||||||
progress.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", id_add));
|
progress.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", url_add));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
match cal_remote.add_item(item.clone()).await {
|
match cal_remote.add_item(item.clone()).await {
|
||||||
Err(err) => progress.error(&format!("Unable to add item {} to remote calendar: {}", id_add, err)),
|
Err(err) => progress.error(&format!("Unable to add item {} to remote calendar: {}", url_add, err)),
|
||||||
Ok(new_ss) => {
|
Ok(new_ss) => {
|
||||||
// Update local sync status
|
// Update local sync status
|
||||||
item.set_sync_status(new_ss);
|
item.set_sync_status(new_ss);
|
||||||
|
@ -374,20 +373,20 @@ where
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for id_change in local_changes {
|
for url_change in local_changes {
|
||||||
progress.debug(&format!("> Pushing local change {} to the server", id_change));
|
progress.debug(&format!("> Pushing local change {} to the server", url_change));
|
||||||
progress.feedback(SyncEvent::InProgress{
|
progress.feedback(SyncEvent::InProgress{
|
||||||
calendar: cal_name.clone(),
|
calendar: cal_name.clone(),
|
||||||
details: Self::item_name(&cal_local, &id_change).await,
|
details: Self::item_name(&cal_local, &url_change).await,
|
||||||
});
|
});
|
||||||
match cal_local.get_item_by_id_mut(&id_change).await {
|
match cal_local.get_item_by_url_mut(&url_change).await {
|
||||||
None => {
|
None => {
|
||||||
progress.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", id_change));
|
progress.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", url_change));
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
Some(item) => {
|
Some(item) => {
|
||||||
match cal_remote.update_item(item.clone()).await {
|
match cal_remote.update_item(item.clone()).await {
|
||||||
Err(err) => progress.error(&format!("Unable to update item {} in remote calendar: {}", id_change, err)),
|
Err(err) => progress.error(&format!("Unable to update item {} in remote calendar: {}", url_change, err)),
|
||||||
Ok(new_ss) => {
|
Ok(new_ss) => {
|
||||||
// Update local sync status
|
// Update local sync status
|
||||||
item.set_sync_status(new_ss);
|
item.set_sync_status(new_ss);
|
||||||
|
@ -401,14 +400,14 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn item_name(cal: &T, id: &Url) -> String {
|
async fn item_name(cal: &T, url: &Url) -> String {
|
||||||
cal.get_item_by_id(id).await.map(|item| item.name()).unwrap_or_default().to_string()
|
cal.get_item_by_url(url).await.map(|item| item.name()).unwrap_or_default().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn get_or_insert_counterpart_calendar<H, N, I>(haystack_descr: &str, haystack: &mut H, cal_id: &CalendarId, needle: Arc<Mutex<N>>)
|
async fn get_or_insert_counterpart_calendar<H, N, I>(haystack_descr: &str, haystack: &mut H, cal_url: &Url, needle: Arc<Mutex<N>>)
|
||||||
-> Result<Arc<Mutex<I>>, Box<dyn Error>>
|
-> Result<Arc<Mutex<I>>, Box<dyn Error>>
|
||||||
where
|
where
|
||||||
H: CalDavSource<I>,
|
H: CalDavSource<I>,
|
||||||
|
@ -416,18 +415,18 @@ where
|
||||||
N: BaseCalendar,
|
N: BaseCalendar,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
if let Some(cal) = haystack.get_calendar(&cal_id).await {
|
if let Some(cal) = haystack.get_calendar(&cal_url).await {
|
||||||
break Ok(cal);
|
break Ok(cal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This calendar does not exist locally yet, let's add it
|
// This calendar does not exist locally yet, let's add it
|
||||||
log::debug!("Adding a {} calendar {}", haystack_descr, cal_id);
|
log::debug!("Adding a {} calendar {}", haystack_descr, cal_url);
|
||||||
let src = needle.lock().unwrap();
|
let src = needle.lock().unwrap();
|
||||||
let name = src.name().to_string();
|
let name = src.name().to_string();
|
||||||
let supported_comps = src.supported_components();
|
let supported_comps = src.supported_components();
|
||||||
let color = src.color();
|
let color = src.color();
|
||||||
if let Err(err) = haystack.create_calendar(
|
if let Err(err) = haystack.create_calendar(
|
||||||
cal_id.clone(),
|
cal_url.clone(),
|
||||||
name,
|
name,
|
||||||
supported_comps,
|
supported_comps,
|
||||||
color.cloned(),
|
color.cloned(),
|
||||||
|
|
|
@ -7,7 +7,6 @@ use ical::property::Property;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
use crate::calendar::CalendarId;
|
|
||||||
use crate::utils::random_url;
|
use crate::utils::random_url;
|
||||||
|
|
||||||
/// RFC5545 defines the completion as several optional fields, yet some combinations make no sense.
|
/// RFC5545 defines the completion as several optional fields, yet some combinations make no sense.
|
||||||
|
@ -67,7 +66,7 @@ pub struct Task {
|
||||||
impl Task {
|
impl Task {
|
||||||
/// Create a brand new Task that is not on a server yet.
|
/// Create a brand new Task that is not on a server yet.
|
||||||
/// This will pick a new (random) task ID.
|
/// This will pick a new (random) task ID.
|
||||||
pub fn new(name: String, completed: bool, parent_calendar_url: &CalendarId) -> Self {
|
pub fn new(name: String, completed: bool, parent_calendar_url: &Url) -> Self {
|
||||||
let new_url = random_url(parent_calendar_url);
|
let new_url = random_url(parent_calendar_url);
|
||||||
let new_sync_status = SyncStatus::NotSynced;
|
let new_sync_status = SyncStatus::NotSynced;
|
||||||
let new_uid = Uuid::new_v4().to_hyphenated().to_string();
|
let new_uid = Uuid::new_v4().to_hyphenated().to_string();
|
||||||
|
|
|
@ -11,7 +11,6 @@ use url::Url;
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
use crate::item::Item;
|
use crate::item::Item;
|
||||||
use crate::item::VersionTag;
|
use crate::item::VersionTag;
|
||||||
use crate::calendar::CalendarId;
|
|
||||||
use crate::calendar::SupportedComponents;
|
use crate::calendar::SupportedComponents;
|
||||||
use crate::resource::Resource;
|
use crate::resource::Resource;
|
||||||
|
|
||||||
|
@ -22,11 +21,11 @@ use crate::resource::Resource;
|
||||||
pub trait CalDavSource<T: BaseCalendar> {
|
pub trait CalDavSource<T: BaseCalendar> {
|
||||||
/// Returns the current calendars that this source contains
|
/// 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)
|
/// 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<CalendarId, Arc<Mutex<T>>>, Box<dyn Error>>;
|
async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<T>>>, Box<dyn Error>>;
|
||||||
/// Returns the calendar matching the ID
|
/// Returns the calendar matching the URL
|
||||||
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<T>>>;
|
async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<T>>>;
|
||||||
/// Create a calendar if it did not exist, and return it
|
/// Create a calendar if it did not exist, and return it
|
||||||
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>)
|
async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>)
|
||||||
-> Result<Arc<Mutex<T>>, Box<dyn Error>>;
|
-> Result<Arc<Mutex<T>>, Box<dyn Error>>;
|
||||||
|
|
||||||
// Removing a calendar is not supported yet
|
// Removing a calendar is not supported yet
|
||||||
|
@ -78,20 +77,20 @@ pub trait DavCalendar : BaseCalendar {
|
||||||
/// Create a new calendar
|
/// Create a new calendar
|
||||||
fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self;
|
fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self;
|
||||||
|
|
||||||
/// Get the IDs and the version tags of every item in this calendar
|
/// Get the URLs and the version tags of every item in this calendar
|
||||||
async fn get_item_version_tags(&self) -> Result<HashMap<Url, VersionTag>, Box<dyn Error>>;
|
async fn get_item_version_tags(&self) -> Result<HashMap<Url, VersionTag>, Box<dyn Error>>;
|
||||||
|
|
||||||
/// Returns a particular item
|
/// Returns a particular item
|
||||||
async fn get_item_by_id(&self, id: &Url) -> Result<Option<Item>, Box<dyn Error>>;
|
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>>;
|
||||||
|
|
||||||
/// Delete an item
|
/// Delete an item
|
||||||
async fn delete_item(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>>;
|
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>>;
|
||||||
|
|
||||||
/// Get the IDs of all current items in this calendar
|
/// Get the URLs of all current items in this calendar
|
||||||
async fn get_item_ids(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
||||||
let items = self.get_item_version_tags().await?;
|
let items = self.get_item_version_tags().await?;
|
||||||
Ok(items.iter()
|
Ok(items.iter()
|
||||||
.map(|(id, _tag)| id.clone())
|
.map(|(url, _tag)| url.clone())
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,19 +107,19 @@ pub trait DavCalendar : BaseCalendar {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait CompleteCalendar : BaseCalendar {
|
pub trait CompleteCalendar : BaseCalendar {
|
||||||
/// Create a new calendar
|
/// Create a new calendar
|
||||||
fn new(name: String, id: CalendarId, supported_components: SupportedComponents, color: Option<Color>) -> Self;
|
fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option<Color>) -> Self;
|
||||||
|
|
||||||
/// Get the IDs of all current items in this calendar
|
/// Get the URLs of all current items in this calendar
|
||||||
async fn get_item_ids(&self) -> Result<HashSet<Url>, Box<dyn Error>>;
|
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>>;
|
||||||
|
|
||||||
/// Returns all items that this calendar contains
|
/// Returns all items that this calendar contains
|
||||||
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>>;
|
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>>;
|
||||||
|
|
||||||
/// Returns a particular item
|
/// Returns a particular item
|
||||||
async fn get_item_by_id<'a>(&'a self, id: &Url) -> Option<&'a Item>;
|
async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item>;
|
||||||
|
|
||||||
/// Returns a particular item
|
/// Returns a particular item
|
||||||
async fn get_item_by_id_mut<'a>(&'a mut self, id: &Url) -> Option<&'a mut Item>;
|
async fn get_item_by_url_mut<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item>;
|
||||||
|
|
||||||
/// Mark an item for deletion.
|
/// Mark an item for deletion.
|
||||||
/// This is required so that the upcoming sync will know it should also also delete this task from the server
|
/// This is required so that the upcoming sync will know it should also also delete this task from the server
|
||||||
|
|
|
@ -10,7 +10,6 @@ use url::Url;
|
||||||
|
|
||||||
use crate::traits::CompleteCalendar;
|
use crate::traits::CompleteCalendar;
|
||||||
use crate::traits::DavCalendar;
|
use crate::traits::DavCalendar;
|
||||||
use crate::calendar::CalendarId;
|
|
||||||
use crate::Item;
|
use crate::Item;
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
|
|
||||||
|
@ -63,12 +62,12 @@ pub fn print_xml(element: &Element) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A debug utility that pretty-prints calendars
|
/// A debug utility that pretty-prints calendars
|
||||||
pub async fn print_calendar_list<C>(cals: &HashMap<CalendarId, Arc<Mutex<C>>>)
|
pub async fn print_calendar_list<C>(cals: &HashMap<Url, Arc<Mutex<C>>>)
|
||||||
where
|
where
|
||||||
C: CompleteCalendar,
|
C: CompleteCalendar,
|
||||||
{
|
{
|
||||||
for (id, cal) in cals {
|
for (url, cal) in cals {
|
||||||
println!("CAL {} ({})", cal.lock().unwrap().name(), id);
|
println!("CAL {} ({})", cal.lock().unwrap().name(), url);
|
||||||
match cal.lock().unwrap().get_items().await {
|
match cal.lock().unwrap().get_items().await {
|
||||||
Err(_err) => continue,
|
Err(_err) => continue,
|
||||||
Ok(map) => {
|
Ok(map) => {
|
||||||
|
@ -81,17 +80,17 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A debug utility that pretty-prints calendars
|
/// A debug utility that pretty-prints calendars
|
||||||
pub async fn print_dav_calendar_list<C>(cals: &HashMap<CalendarId, Arc<Mutex<C>>>)
|
pub async fn print_dav_calendar_list<C>(cals: &HashMap<Url, Arc<Mutex<C>>>)
|
||||||
where
|
where
|
||||||
C: DavCalendar,
|
C: DavCalendar,
|
||||||
{
|
{
|
||||||
for (id, cal) in cals {
|
for (url, cal) in cals {
|
||||||
println!("CAL {} ({})", cal.lock().unwrap().name(), id);
|
println!("CAL {} ({})", cal.lock().unwrap().name(), url);
|
||||||
match cal.lock().unwrap().get_item_version_tags().await {
|
match cal.lock().unwrap().get_item_version_tags().await {
|
||||||
Err(_err) => continue,
|
Err(_err) => continue,
|
||||||
Ok(map) => {
|
Ok(map) => {
|
||||||
for (id, version_tag) in map {
|
for (url, version_tag) in map {
|
||||||
println!(" * {} (version {:?})", id, version_tag);
|
println!(" * {} (version {:?})", url, version_tag);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ use url::Url;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
use kitchen_fridge::calendar::CalendarId;
|
|
||||||
use kitchen_fridge::calendar::SupportedComponents;
|
use kitchen_fridge::calendar::SupportedComponents;
|
||||||
use kitchen_fridge::traits::CalDavSource;
|
use kitchen_fridge::traits::CalDavSource;
|
||||||
use kitchen_fridge::traits::BaseCalendar;
|
use kitchen_fridge::traits::BaseCalendar;
|
||||||
|
@ -45,7 +44,7 @@ pub enum LocatedState {
|
||||||
pub struct ItemState {
|
pub struct ItemState {
|
||||||
// TODO: if/when this crate supports Events as well, we could add such events here
|
// TODO: if/when this crate supports Events as well, we could add such events here
|
||||||
/// The calendar it is in
|
/// The calendar it is in
|
||||||
calendar: CalendarId,
|
calendar: Url,
|
||||||
/// Its name
|
/// Its name
|
||||||
name: String,
|
name: String,
|
||||||
/// Its completion status
|
/// Its completion status
|
||||||
|
@ -55,10 +54,10 @@ pub struct ItemState {
|
||||||
pub enum ChangeToApply {
|
pub enum ChangeToApply {
|
||||||
Rename(String),
|
Rename(String),
|
||||||
SetCompletion(bool),
|
SetCompletion(bool),
|
||||||
Create(CalendarId, Item),
|
Create(Url, Item),
|
||||||
/// "remove" means "mark for deletion" in the local calendar, or "immediately delete" on the remote calendar
|
/// "remove" means "mark for deletion" in the local calendar, or "immediately delete" on the remote calendar
|
||||||
Remove,
|
Remove,
|
||||||
// ChangeCalendar(CalendarId) is useless, as long as changing a calendar is implemented as "delete in one calendar and re-create it in another one"
|
// ChangeCalendar(Url) is useless, as long as changing a calendar is implemented as "delete in one calendar and re-create it in another one"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,9 +87,9 @@ pub struct ItemScenario {
|
||||||
pub fn scenarii_basic() -> Vec<ItemScenario> {
|
pub fn scenarii_basic() -> Vec<ItemScenario> {
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
let first_cal = CalendarId::from("https://some.calend.ar/calendar-1/".parse().unwrap());
|
let first_cal = Url::from("https://some.calend.ar/calendar-1/".parse().unwrap());
|
||||||
let second_cal = CalendarId::from("https://some.calend.ar/calendar-2/".parse().unwrap());
|
let second_cal = Url::from("https://some.calend.ar/calendar-2/".parse().unwrap());
|
||||||
let third_cal = CalendarId::from("https://some.calend.ar/calendar-3/".parse().unwrap());
|
let third_cal = Url::from("https://some.calend.ar/calendar-3/".parse().unwrap());
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
ItemScenario {
|
ItemScenario {
|
||||||
|
@ -419,8 +418,8 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
||||||
pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
|
pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
let cal1 = CalendarId::from("https://some.calend.ar/first/".parse().unwrap());
|
let cal1 = Url::from("https://some.calend.ar/first/".parse().unwrap());
|
||||||
let cal2 = CalendarId::from("https://some.calend.ar/second/".parse().unwrap());
|
let cal2 = Url::from("https://some.calend.ar/second/".parse().unwrap());
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
ItemScenario {
|
ItemScenario {
|
||||||
|
@ -483,8 +482,8 @@ pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
|
||||||
pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
|
pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
let cal3 = CalendarId::from("https://some.calend.ar/third/".parse().unwrap());
|
let cal3 = Url::from("https://some.calend.ar/third/".parse().unwrap());
|
||||||
let cal4 = CalendarId::from("https://some.calend.ar/fourth/".parse().unwrap());
|
let cal4 = Url::from("https://some.calend.ar/fourth/".parse().unwrap());
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
ItemScenario {
|
ItemScenario {
|
||||||
|
@ -548,7 +547,7 @@ pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
|
||||||
pub fn scenarii_transient_task() -> Vec<ItemScenario> {
|
pub fn scenarii_transient_task() -> Vec<ItemScenario> {
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
let cal = CalendarId::from("https://some.calend.ar/transient/".parse().unwrap());
|
let cal = Url::from("https://some.calend.ar/transient/".parse().unwrap());
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
ItemScenario {
|
ItemScenario {
|
||||||
|
@ -668,37 +667,37 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc<M
|
||||||
async fn apply_changes_on_provider(provider: &mut Provider<Cache, CachedCalendar, Cache, CachedCalendar>, scenarii: &[ItemScenario]) {
|
async fn apply_changes_on_provider(provider: &mut Provider<Cache, CachedCalendar, Cache, CachedCalendar>, scenarii: &[ItemScenario]) {
|
||||||
// Apply changes to each item
|
// Apply changes to each item
|
||||||
for item in scenarii {
|
for item in scenarii {
|
||||||
let initial_calendar_id = match &item.initial_state {
|
let initial_calendar_url = match &item.initial_state {
|
||||||
LocatedState::None => None,
|
LocatedState::None => None,
|
||||||
LocatedState::Local(state) => Some(state.calendar.clone()),
|
LocatedState::Local(state) => Some(state.calendar.clone()),
|
||||||
LocatedState::Remote(state) => Some(state.calendar.clone()),
|
LocatedState::Remote(state) => Some(state.calendar.clone()),
|
||||||
LocatedState::BothSynced(state) => Some(state.calendar.clone()),
|
LocatedState::BothSynced(state) => Some(state.calendar.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut calendar_id = initial_calendar_id.clone();
|
let mut calendar_url = initial_calendar_url.clone();
|
||||||
for local_change in &item.local_changes_to_apply {
|
for local_change in &item.local_changes_to_apply {
|
||||||
calendar_id = Some(apply_change(provider.local(), calendar_id, &item.url, local_change, false).await);
|
calendar_url = Some(apply_change(provider.local(), calendar_url, &item.url, local_change, false).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut calendar_id = initial_calendar_id;
|
let mut calendar_url = initial_calendar_url;
|
||||||
for remote_change in &item.remote_changes_to_apply {
|
for remote_change in &item.remote_changes_to_apply {
|
||||||
calendar_id = Some(apply_change(provider.remote(), calendar_id, &item.url, remote_change, true).await);
|
calendar_url = Some(apply_change(provider.remote(), calendar_url, &item.url, remote_change, true).await);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId)
|
async fn get_or_insert_calendar(source: &mut Cache, url: &Url)
|
||||||
-> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>>
|
-> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>>
|
||||||
{
|
{
|
||||||
match source.get_calendar(id).await {
|
match source.get_calendar(url).await {
|
||||||
Some(cal) => Ok(cal),
|
Some(cal) => Ok(cal),
|
||||||
None => {
|
None => {
|
||||||
let new_name = format!("Test calendar for ID {}", id);
|
let new_name = format!("Test calendar for URL {}", url);
|
||||||
let supported_components = SupportedComponents::TODO;
|
let supported_components = SupportedComponents::TODO;
|
||||||
let color = csscolorparser::parse("#ff8000"); // TODO: we should rather have specific colors, depending on the calendars
|
let color = csscolorparser::parse("#ff8000"); // TODO: we should rather have specific colors, depending on the calendars
|
||||||
|
|
||||||
source.create_calendar(
|
source.create_calendar(
|
||||||
id.clone(),
|
url.clone(),
|
||||||
new_name.to_string(),
|
new_name.to_string(),
|
||||||
supported_components,
|
supported_components,
|
||||||
None,
|
None,
|
||||||
|
@ -707,13 +706,13 @@ async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a single change on a given source, and returns the calendar ID that was modified
|
/// Apply a single change on a given source, and returns the calendar URL that was modified
|
||||||
async fn apply_change<S, C>(source: &S, calendar_id: Option<CalendarId>, item_url: &Url, change: &ChangeToApply, is_remote: bool) -> CalendarId
|
async fn apply_change<S, C>(source: &S, calendar_url: Option<Url>, item_url: &Url, change: &ChangeToApply, is_remote: bool) -> Url
|
||||||
where
|
where
|
||||||
S: CalDavSource<C>,
|
S: CalDavSource<C>,
|
||||||
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
||||||
{
|
{
|
||||||
match calendar_id {
|
match calendar_url {
|
||||||
Some(cal) => {
|
Some(cal) => {
|
||||||
apply_changes_on_an_existing_item(source, &cal, item_url, change, is_remote).await;
|
apply_changes_on_an_existing_item(source, &cal, item_url, change, is_remote).await;
|
||||||
cal
|
cal
|
||||||
|
@ -724,14 +723,14 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn apply_changes_on_an_existing_item<S, C>(source: &S, calendar_id: &CalendarId, item_url: &Url, change: &ChangeToApply, is_remote: bool)
|
async fn apply_changes_on_an_existing_item<S, C>(source: &S, calendar_url: &Url, item_url: &Url, change: &ChangeToApply, is_remote: bool)
|
||||||
where
|
where
|
||||||
S: CalDavSource<C>,
|
S: CalDavSource<C>,
|
||||||
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
||||||
{
|
{
|
||||||
let cal = source.get_calendar(calendar_id).await.unwrap();
|
let cal = source.get_calendar(calendar_url).await.unwrap();
|
||||||
let mut cal = cal.lock().unwrap();
|
let mut cal = cal.lock().unwrap();
|
||||||
let task = cal.get_item_by_id_mut(item_url).await.unwrap().unwrap_task_mut();
|
let task = cal.get_item_by_url_mut(item_url).await.unwrap().unwrap_task_mut();
|
||||||
|
|
||||||
match change {
|
match change {
|
||||||
ChangeToApply::Rename(new_name) => {
|
ChangeToApply::Rename(new_name) => {
|
||||||
|
@ -758,14 +757,14 @@ where
|
||||||
true => cal.delete_item(item_url).await.unwrap(),
|
true => cal.delete_item(item_url).await.unwrap(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
ChangeToApply::Create(_calendar_id, _item) => {
|
ChangeToApply::Create(_calendar_url, _item) => {
|
||||||
panic!("This function only handles already existing items");
|
panic!("This function only handles already existing items");
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an item, and returns the calendar ID it was inserted in
|
/// Create an item, and returns the URL of the calendar it was inserted in
|
||||||
async fn create_test_item<S, C>(source: &S, change: &ChangeToApply) -> CalendarId
|
async fn create_test_item<S, C>(source: &S, change: &ChangeToApply) -> Url
|
||||||
where
|
where
|
||||||
S: CalDavSource<C>,
|
S: CalDavSource<C>,
|
||||||
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
||||||
|
@ -776,10 +775,10 @@ where
|
||||||
ChangeToApply::Remove => {
|
ChangeToApply::Remove => {
|
||||||
panic!("This function only creates items that do not exist yet");
|
panic!("This function only creates items that do not exist yet");
|
||||||
}
|
}
|
||||||
ChangeToApply::Create(calendar_id, item) => {
|
ChangeToApply::Create(calendar_url, item) => {
|
||||||
let cal = source.get_calendar(calendar_id).await.unwrap();
|
let cal = source.get_calendar(calendar_url).await.unwrap();
|
||||||
cal.lock().unwrap().add_item(item.clone()).await.unwrap();
|
cal.lock().unwrap().add_item(item.clone()).await.unwrap();
|
||||||
calendar_id.clone()
|
calendar_url.clone()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ impl TestFlavour {
|
||||||
Self {
|
Self {
|
||||||
scenarii: scenarii::scenarii_basic(),
|
scenarii: scenarii::scenarii_basic(),
|
||||||
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
|
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
|
||||||
get_item_by_id_behaviour: (3,2),
|
get_item_by_url_behaviour: (3,2),
|
||||||
..MockBehaviour::default()
|
..MockBehaviour::default()
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@ impl TestFlavour {
|
||||||
scenarii: scenarii::scenarii_basic(),
|
scenarii: scenarii::scenarii_basic(),
|
||||||
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
|
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
|
||||||
add_item_behaviour: (2,3),
|
add_item_behaviour: (2,3),
|
||||||
get_item_by_id_behaviour: (1,4),
|
get_item_by_url_behaviour: (1,4),
|
||||||
..MockBehaviour::default()
|
..MockBehaviour::default()
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
@ -183,7 +183,7 @@ impl TestFlavour {
|
||||||
delete_item_behaviour: (1,1),
|
delete_item_behaviour: (1,1),
|
||||||
create_calendar_behaviour: (1,4),
|
create_calendar_behaviour: (1,4),
|
||||||
get_item_version_tags_behaviour: (3,1),
|
get_item_version_tags_behaviour: (3,1),
|
||||||
get_item_by_id_behaviour: (0,41),
|
get_item_by_url_behaviour: (0,41),
|
||||||
..MockBehaviour::default()
|
..MockBehaviour::default()
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue