kitchen-freezer/src/provider.rs

219 lines
8.9 KiB
Rust
Raw Normal View History

2021-02-28 00:19:00 +01:00
//! This modules abstracts data sources and merges them in a single virtual one
2021-03-05 23:32:42 +01:00
use std::error::Error;
2021-03-19 00:08:16 +01:00
use std::collections::HashSet;
2021-03-05 23:32:42 +01:00
use std::marker::PhantomData;
2021-02-28 00:19:00 +01:00
2021-03-01 23:39:16 +01:00
use crate::traits::{CalDavSource, CompleteCalendar};
use crate::traits::PartialCalendar;
2021-03-22 23:42:41 +01:00
use crate::item::SyncStatus;
2021-02-28 00:19:00 +01:00
2021-02-28 18:00:37 +01:00
/// A data source that combines two `CalDavSources` (usually a server and a local cache), which is able to sync both sources.
2021-03-22 22:39:50 +01:00
pub struct Provider<L, T, R, U>
2021-02-28 00:19:00 +01:00
where
L: CalDavSource<T>,
2021-03-22 23:42:41 +01:00
T: CompleteCalendar + Sync + Send,
2021-03-22 22:39:50 +01:00
R: CalDavSource<U>,
2021-03-21 19:19:49 +01:00
U: PartialCalendar + Sync + Send,
2021-02-28 00:19:00 +01:00
{
2021-03-22 22:39:50 +01:00
/// The remote source (usually a server)
remote: R,
2021-02-28 00:19:00 +01:00
/// The local cache
local: L,
2021-03-01 23:39:16 +01:00
phantom_t: PhantomData<T>,
phantom_u: PhantomData<U>,
2021-02-28 00:19:00 +01:00
}
2021-03-22 22:39:50 +01:00
impl<L, T, R, U> Provider<L, T, R, U>
2021-02-28 00:19:00 +01:00
where
L: CalDavSource<T>,
2021-03-22 23:42:41 +01:00
T: CompleteCalendar + Sync + Send,
2021-03-22 22:39:50 +01:00
R: CalDavSource<U>,
2021-03-21 19:19:49 +01:00
U: PartialCalendar + Sync + Send,
2021-02-28 00:19:00 +01:00
{
2021-02-28 18:02:01 +01:00
/// Create a provider.
///
2021-03-22 22:39:50 +01:00
/// `remote` is usually a [`Client`](crate::client::Client), `local` is usually a [`Cache`](crate::cache::Cache).
/// However, both can be interchangeable. The only difference is that `remote` always wins in case of a sync conflict
pub fn new(remote: R, local: L) -> Self {
Self { remote, local,
2021-03-01 23:39:16 +01:00
phantom_t: PhantomData, phantom_u: PhantomData,
}
2021-02-28 00:19:00 +01:00
}
2021-03-22 23:42:41 +01:00
/// Returns the data source described as the `remote`
2021-03-22 22:39:50 +01:00
pub fn remote(&self) -> &R { &self.remote }
2021-02-28 18:02:01 +01:00
/// Returns the data source described as the `local`
2021-02-28 00:19:00 +01:00
pub fn local(&self) -> &L { &self.local }
2021-03-22 23:42:41 +01:00
/// Performs a synchronisation between `local` and `remote`.
2021-02-28 18:02:01 +01:00
///
2021-03-20 20:12:48 +01:00
/// This bidirectional sync applies additions/deletions made on a source to the other source.
2021-03-22 23:42:41 +01:00
/// In case of conflicts (the same item has been modified on both ends since the last sync, `remote` always wins)
2021-02-28 00:19:00 +01:00
pub async fn sync(&mut self) -> Result<(), Box<dyn Error>> {
log::info!("Starting a sync.");
2021-03-22 22:37:29 +01:00
2021-03-22 22:39:50 +01:00
let cals_remote = self.remote.get_calendars().await?;
for (id, cal_remote) in cals_remote {
let mut cal_remote = cal_remote.lock().unwrap();
2021-03-18 23:59:06 +01:00
2021-03-21 19:27:55 +01:00
let cal_local = match self.local.get_calendar(&id).await {
2021-02-28 00:19:00 +01:00
None => {
log::error!("TODO: implement here");
continue;
},
Some(cal) => cal,
};
2021-03-18 23:59:06 +01:00
let mut cal_local = cal_local.lock().unwrap();
2021-02-28 00:19:00 +01:00
2021-03-22 22:37:29 +01:00
// Step 1 - find the differences
2021-03-22 23:42:41 +01:00
let mut local_del = HashSet::new();
let mut remote_del = HashSet::new();
let mut local_changes = HashSet::new();
let mut remote_changes = HashSet::new();
let mut local_additions = HashSet::new();
let mut remote_additions = HashSet::new();
let remote_items = cal_remote.get_item_version_tags().await?;
let mut local_items_to_handle = cal_local.get_item_ids().await?;
for (id, remote_tag) in remote_items {
match cal_local.get_item_by_id(&id).await {
None => {
// This was created on the remote
remote_additions.insert(id);
},
Some(local_item) => {
if local_items_to_handle.remove(&id) == true {
log::error!("Inconsistent state: missing task {} from the local tasks", id);
}
match local_item.sync_status() {
SyncStatus::NotSynced => {
log::error!("ID reuse between remote and local sources ({}). Ignoring this item in the sync", id);
continue;
},
SyncStatus::Synced(local_tag) => {
if &remote_tag != local_tag {
// This has been modified on the remote
remote_changes.insert(id);
}
},
SyncStatus::LocallyModified(local_tag) => {
if &remote_tag == local_tag {
// This has been changed locally
local_changes.insert(id);
} else {
log::info!("Conflict: task {} has been modified in both sources. Using the remote version.", id);
remote_changes.insert(id);
}
},
SyncStatus::LocallyDeleted(local_tag) => {
if &remote_tag == local_tag {
// This has been locally deleted
local_del.insert(id);
} else {
log::info!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", id);
remote_changes.insert(id);
}
},
}
}
}
}
// Also iterate on the local tasks that are not on the remote
for id in local_items_to_handle {
let local_item = match cal_local.get_item_by_id(&id).await {
None => {
log::error!("Inconsistent state: missing task {} from the local tasks", id);
continue;
},
Some(item) => item,
};
match local_item.sync_status() {
SyncStatus::Synced(_) => {
// This item has been removed from the remote
remote_del.insert(id);
},
SyncStatus::NotSynced => {
// This item has just been locally created
local_additions.insert(id);
},
variant @ _=> {
log::error!("Inconsistent state: unexpected variant {:?}", variant);
}
}
}
// Step 2 - commit changes
for id_del in local_del {
if let Err(err) = cal_remote.delete_item(&id_del).await {
log::warn!("Unable to delete remote item {}: {}", id_del, err);
}
}
for id_del in remote_del {
if let Err(err) = cal_local.delete_item(&id_del).await {
log::warn!("Unable to delete local item {}: {}", id_del, err);
}
}
for id_add in remote_additions {
match cal_remote.get_item_by_id(&id_add).await {
None => {
log::error!("Inconsistency: new item {} has vanished from the remote end", id_add);
continue;
},
Some(new_item) => cal_local.add_item(new_item.clone()).await,
}
}
for id_change in remote_changes {
match cal_remote.get_item_by_id(&id_change).await {
None => {
log::error!("Inconsistency: modified item {} has vanished from the remote end", id_change);
continue;
},
Some(item) => {
if let Err(err) = cal_local.delete_item(&id_change).await {
log::error!("Unable to delete item {} from local calendar: {}", id_change, err);
}
cal_local.add_item(item.clone());
},
}
}
for id_add in local_additions {
match cal_local.get_item_by_id(&id_add).await {
None => {
log::error!("Inconsistency: created item {} has been marked for upload but is locally missing", id_add);
continue;
},
Some(item) => cal_remote.add_item(item.clone()),
};
}
for id_change in local_changes {
match cal_local.get_item_by_id(&id_change).await {
None => {
log::error!("Inconsistency: modified item {} has been marked for upload but is locally missing", id_change);
continue;
},
Some(item) => {
if let Err(err) = cal_remote.delete_item(&id_change).await {
log::error!("Unable to delete item {} from remote calendar: {}", id_change, err);
}
cal_remote.add_item(item.clone());
}
};
}
2021-03-22 22:37:29 +01:00
2021-02-28 00:19:00 +01:00
}
Ok(())
}
}