kitchen-freezer/src/cache.rs

236 lines
7.7 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;
2021-03-05 23:32:42 +01:00
use std::collections::HashSet;
use std::hash::Hash;
2021-03-18 23:59:06 +01:00
use std::sync::{Arc, Mutex};
2021-03-21 00:11:35 +01:00
use std::ffi::OsStr;
2021-02-25 00:53:50 +01:00
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
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-03-05 23:32:42 +01:00
use crate::calendar::CalendarId;
2021-02-25 00:53:50 +01:00
2021-03-21 00:11:35 +01:00
const MAIN_FILE: &str = "data.json";
2021-02-25 00:53:50 +01:00
2021-03-21 00:11:35 +01:00
/// A CalDAV source that stores its item in a local folder
#[derive(Debug)]
2021-02-25 00:53:50 +01:00
pub struct Cache {
2021-03-21 00:11:35 +01:00
backing_folder: PathBuf,
2021-02-25 00:53:50 +01:00
data: CachedData,
}
2021-03-21 00:11:35 +01:00
#[derive(Default, Debug, Serialize, Deserialize)]
2021-02-25 00:53:50 +01:00
struct CachedData {
2021-03-21 00:11:35 +01:00
#[serde(skip)]
2021-03-18 23:59:06 +01:00
calendars: HashMap<CalendarId, Arc<Mutex<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-03-21 00:11:35 +01:00
/// Get the path to the cache folder
pub fn cache_folder() -> PathBuf {
return PathBuf::from(String::from("~/.config/my-tasks/cache/"))
2021-02-25 00:53:50 +01:00
}
2021-03-21 00:11:35 +01:00
/// Initialize a cache from the content of a valid backing folder if it exists.
2021-02-28 18:02:01 +01:00
/// Returns an error otherwise
2021-03-21 00:11:35 +01:00
pub fn from_folder(folder: &Path) -> Result<Self, Box<dyn Error>> {
// Load shared data...
let main_file = folder.join(MAIN_FILE);
let mut data: CachedData = match std::fs::File::open(&main_file) {
2021-02-28 18:02:01 +01:00
Err(err) => {
2021-03-21 00:11:35 +01:00
return Err(format!("Unable to open file {:?}: {}", main_file, err).into());
2021-02-25 00:53:50 +01:00
},
Ok(file) => serde_json::from_reader(file)?,
};
2021-03-21 00:11:35 +01:00
// ...and every calendar
for entry in std::fs::read_dir(folder)? {
match entry {
Err(err) => {
log::error!("Unable to read dir: {:?}", err);
continue;
},
Ok(entry) => {
let cal_path = entry.path();
log::debug!("Considering {:?}", cal_path);
if cal_path.extension() == Some(OsStr::new("cal")) {
match Self::load_calendar(&cal_path) {
Err(err) => {
log::error!("Unable to load calendar {:?} from cache: {:?}", cal_path, err);
continue;
},
Ok(cal) =>
data.calendars.insert(cal.id().clone(), Arc::new(Mutex::new(cal))),
};
}
},
}
}
2021-02-25 00:53:50 +01:00
Ok(Self{
2021-03-21 00:11:35 +01:00
backing_folder: PathBuf::from(folder),
2021-02-25 00:53:50 +01:00
data,
})
}
2021-03-21 00:11:35 +01:00
fn load_calendar(path: &Path) -> Result<CachedCalendar, Box<dyn Error>> {
let file = std::fs::File::open(&path)?;
Ok(serde_json::from_reader(file)?)
}
2021-02-25 00:53:50 +01:00
/// Initialize a cache with the default contents
2021-03-21 00:11:35 +01:00
pub fn new(folder_path: &Path) -> Self {
2021-02-25 00:53:50 +01:00
Self{
2021-03-21 00:11:35 +01:00
backing_folder: PathBuf::from(folder_path),
2021-02-25 00:53:50 +01:00
data: CachedData::default(),
}
}
2021-03-21 00:11:35 +01:00
/// Store the current Cache to its backing folder
fn save_to_folder(&mut self) -> Result<(), std::io::Error> {
let folder = &self.backing_folder;
std::fs::create_dir_all(folder)?;
// Save the general data
let main_file_path = folder.join(MAIN_FILE);
let file = std::fs::File::create(&main_file_path)?;
serde_json::to_writer(file, &self.data)?;
// Save each calendar
for (cal_id, cal_mutex) in &self.data.calendars {
let file_name = sanitize_filename::sanitize(cal_id.as_str()) + ".cal";
let cal_file = folder.join(file_name);
let file = std::fs::File::create(&cal_file)?;
let cal = cal_mutex.lock().unwrap();
serde_json::to_writer(file, &*cal)?;
}
Ok(())
2021-02-25 00:53:50 +01:00
}
2021-02-28 18:02:01 +01:00
2021-03-18 23:59:06 +01:00
pub fn add_calendar(&mut self, calendar: Arc<Mutex<CachedCalendar>>) {
let id = calendar.lock().unwrap().id().clone();
self.data.calendars.insert(id, calendar);
2021-02-25 00:53:50 +01:00
}
/// 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?;
2021-03-05 23:32:42 +01:00
if keys_are_the_same(&calendars_l, &calendars_r) == false {
2021-03-21 00:11:35 +01:00
log::debug!("Different keys for calendars");
2021-03-05 23:32:42 +01:00
return Ok(false);
}
for (id, cal_l) in calendars_l {
2021-03-18 23:59:06 +01:00
let cal_l = cal_l.lock().unwrap();
let cal_r = match calendars_r.get(&id) {
Some(c) => c.lock().unwrap(),
2021-03-05 23:32:42 +01:00
None => return Err("should not happen, we've just tested keys are the same".into()),
};
let items_l = cal_l.get_items();
let items_r = cal_r.get_items();
if keys_are_the_same(&items_l, &items_r) == false {
2021-03-21 00:11:35 +01:00
log::debug!("Different keys for items");
return Ok(false);
}
for (id_l, item_l) in items_l {
2021-03-05 23:32:42 +01:00
let item_r = match items_r.get(&id_l) {
Some(c) => c,
None => return Err("should not happen, we've just tested keys are the same".into()),
};
if &item_l != item_r {
2021-03-21 00:11:35 +01:00
log::debug!("Different items");
return Ok(false);
}
}
}
Ok(true)
}
}
fn keys_are_the_same<T, U, V>(left: &HashMap<T, U>, right: &HashMap<T, V>) -> bool
where
2021-03-05 23:32:42 +01:00
T: Hash + Eq + Clone,
{
2021-03-05 23:32:42 +01:00
if left.len() != right.len() {
return false;
}
let keys_l: HashSet<T> = left.keys().cloned().collect();
let keys_r: HashSet<T> = right.keys().cloned().collect();
keys_l == keys_r
2021-02-25 00:53:50 +01:00
}
#[async_trait]
2021-03-01 23:39:16 +01:00
impl CalDavSource<CachedCalendar> for Cache {
2021-03-18 23:59:06 +01:00
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
Ok(self.data.calendars.iter()
.map(|(id, cal)| (id.clone(), cal.clone()))
.collect()
)
2021-02-25 00:53:50 +01:00
}
2021-03-18 23:59:06 +01:00
async fn get_calendar(&self, id: CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> {
self.data.calendars.get(&id).map(|arc| arc.clone())
2021-02-26 17:55:23 +01:00
}
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;
2021-03-21 00:11:35 +01:00
#[tokio::test]
async fn serde_cache() {
let _ = env_logger::builder().is_test(true).try_init();
let cache_path = PathBuf::from(String::from("test_cache/"));
2021-02-25 00:53:50 +01:00
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);
2021-03-18 23:59:06 +01:00
cache.add_calendar(Arc::new(Mutex::new(cal1)));
2021-02-25 00:53:50 +01:00
2021-03-21 00:11:35 +01:00
cache.save_to_folder().unwrap();
let retrieved_cache = Cache::from_folder(&cache_path).unwrap();
assert_eq!(cache.backing_folder, retrieved_cache.backing_folder);
assert_eq!(cache.data.last_sync, retrieved_cache.data.last_sync);
let test = cache.has_same_contents_than(&retrieved_cache).await;
println!("Equal? {:?}", test);
assert_eq!(test.unwrap(), true);
2021-02-25 00:53:50 +01:00
}
}