kitchen-freezer/src/cache.rs

194 lines
5.6 KiB
Rust
Raw Normal View History

2021-02-25 00:53:50 +01:00
//! 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::hash::Hash;
2021-02-25 00:53:50 +01:00
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
2021-02-26 17:55:23 +01:00
use url::Url;
2021-02-28 18:00:37 +01:00
use chrono::{DateTime, Utc};
2021-02-25 00:53:50 +01:00
use crate::traits::CalDavSource;
2021-02-28 18:00:37 +01:00
use crate::traits::SyncSlave;
2021-03-01 23:39:16 +01:00
use crate::traits::PartialCalendar;
use crate::traits::CompleteCalendar;
2021-03-01 23:39:16 +01:00
use crate::calendar::cached_calendar::CachedCalendar;
2021-02-25 00:53:50 +01:00
2021-02-28 18:02:01 +01:00
/// A CalDAV source that stores its item in a local file
2021-02-25 00:53:50 +01:00
#[derive(Debug, PartialEq)]
pub struct Cache {
backing_file: PathBuf,
data: CachedData,
}
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
struct CachedData {
2021-03-01 23:39:16 +01:00
calendars: Vec<CachedCalendar>,
2021-02-28 18:00:37 +01:00
last_sync: Option<DateTime<Utc>>,
2021-02-25 00:53:50 +01:00
}
impl Cache {
2021-02-28 18:02:01 +01:00
/// Get the path to the cache file
2021-02-25 00:53:50 +01:00
pub fn cache_file() -> PathBuf {
return PathBuf::from(String::from("~/.config/my-tasks/cache.json"))
}
2021-02-28 18:02:01 +01:00
/// Initialize a cache from the content of a valid backing file if it exists.
/// Returns an error otherwise
2021-02-25 00:53:50 +01:00
pub fn from_file(path: &Path) -> Result<Self, Box<dyn Error>> {
let data = match std::fs::File::open(path) {
2021-02-28 18:02:01 +01:00
Err(err) => {
return Err(format!("Unable to open file {:?}: {}", path, err).into());
2021-02-25 00:53:50 +01:00
},
Ok(file) => serde_json::from_reader(file)?,
};
Ok(Self{
backing_file: PathBuf::from(path),
data,
})
}
/// Initialize a cache with the default contents
pub fn new(path: &Path) -> Self {
Self{
backing_file: PathBuf::from(path),
data: CachedData::default(),
}
}
/// Store the current Cache to its backing file
fn save_to_file(&mut self) {
// Save the contents to the file
let path = &self.backing_file;
let file = match std::fs::File::create(path) {
Err(err) => {
log::warn!("Unable to save file {:?}: {}", path, err);
return;
},
Ok(f) => f,
};
if let Err(err) = serde_json::to_writer(file, &self.data) {
log::warn!("Unable to serialize: {}", err);
return;
};
}
2021-02-28 18:02:01 +01:00
2021-03-01 23:39:16 +01:00
pub fn add_calendar(&mut self, calendar: CachedCalendar) {
2021-02-25 00:53:50 +01:00
self.data.calendars.push(calendar);
}
/// Compares two Caches to check they have the same current content
///
/// This is not a complete equality test: some attributes (last sync date, deleted items...) may differ
pub async fn has_same_contents_than(&self, other: &Self) -> Result<bool, Box<dyn Error>> {
let calendars_l = self.get_calendars().await?;
let calendars_r = other.get_calendars().await?;
for cal_l in calendars_l {
for cal_r in calendars_r {
if cal_l.url() == cal_r.url() {
let items_l = cal_l.get_items();
let items_r = cal_r.get_items();
if keys_are_the_same(&items_l, &items_r) == false {
return Ok(false);
}
for (id_l, item_l) in items_l {
let item_r = items_r.get(&id_l).unwrap();
//println!(" items {} {}", item_r.name(), item_l.name());
if &item_l != item_r {
return Ok(false);
}
}
break;
}
}
}
// TODO: also check there is no missing calendar in one side or the other
// TODO: also check there is no missing task in either side of each calendar
Ok(true)
}
}
fn keys_are_the_same<T, U, V>(left: &HashMap<T, U>, right: &HashMap<T, V>) -> bool
where
T: Hash + Eq,
{
left.len() == right.len()
&& left.keys().all(|k| right.contains_key(k))
2021-02-25 00:53:50 +01:00
}
#[async_trait]
2021-03-01 23:39:16 +01:00
impl CalDavSource<CachedCalendar> for Cache {
async fn get_calendars(&self) -> Result<&Vec<CachedCalendar>, Box<dyn Error>> {
2021-02-25 00:53:50 +01:00
Ok(&self.data.calendars)
}
2021-03-01 23:39:16 +01:00
async fn get_calendars_mut(&mut self) -> Result<Vec<&mut CachedCalendar>, Box<dyn Error>> {
2021-02-25 00:53:50 +01:00
Ok(
self.data.calendars.iter_mut()
.collect()
)
}
2021-02-26 17:55:23 +01:00
2021-03-01 23:39:16 +01:00
async fn get_calendar(&self, url: Url) -> Option<&CachedCalendar> {
2021-02-26 17:55:23 +01:00
for cal in &self.data.calendars {
if cal.url() == &url {
return Some(cal);
2021-03-01 23:39:16 +01:00
}
2021-02-26 17:55:23 +01:00
}
return None;
}
2021-03-01 23:39:16 +01:00
async fn get_calendar_mut(&mut self, url: Url) -> Option<&mut CachedCalendar> {
2021-02-26 17:55:23 +01:00
for cal in &mut self.data.calendars {
if cal.url() == &url {
return Some(cal);
}
}
return None;
}
2021-02-25 00:53:50 +01:00
}
2021-02-28 18:00:37 +01:00
impl SyncSlave for Cache {
fn get_last_sync(&self) -> Option<DateTime<Utc>> {
self.data.last_sync
}
2021-02-25 00:53:50 +01:00
2021-02-28 18:00:37 +01:00
fn update_last_sync(&mut self, timepoint: Option<DateTime<Utc>>) {
self.data.last_sync = Some(timepoint.unwrap_or_else(|| Utc::now()));
}
}
2021-02-25 00:53:50 +01:00
#[cfg(test)]
mod tests {
use super::*;
use url::Url;
use crate::calendar::SupportedComponents;
#[test]
fn serde_cache() {
let cache_path = PathBuf::from(String::from("cache.json"));
let mut cache = Cache::new(&cache_path);
2021-03-01 23:39:16 +01:00
let cal1 = CachedCalendar::new("shopping list".to_string(),
2021-02-25 00:53:50 +01:00
Url::parse("https://caldav.com/shopping").unwrap(),
SupportedComponents::TODO);
cache.add_calendar(cal1);
cache.save_to_file();
let retrieved_cache = Cache::from_file(&cache_path).unwrap();
assert_eq!(cache, retrieved_cache);
}
}