Cache saves to a folder
This commit is contained in:
parent
d6ee642dd9
commit
d53ec193d8
5 changed files with 106 additions and 134 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -441,6 +441,7 @@ dependencies = [
|
|||
"log",
|
||||
"minidom",
|
||||
"reqwest",
|
||||
"sanitize-filename",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
@ -736,6 +737,16 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
|
||||
[[package]]
|
||||
name = "sanitize-filename"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.19"
|
||||
|
|
|
@ -19,3 +19,4 @@ serde_json = "1.0"
|
|||
async-trait = "0.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
sanitize-filename = "0.3"
|
||||
|
|
118
src/cache.rs
118
src/cache.rs
|
@ -7,6 +7,7 @@ use std::collections::HashMap;
|
|||
use std::collections::HashSet;
|
||||
use std::hash::Hash;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use async_trait::async_trait;
|
||||
|
@ -19,66 +20,102 @@ use crate::traits::CompleteCalendar;
|
|||
use crate::calendar::cached_calendar::CachedCalendar;
|
||||
use crate::calendar::CalendarId;
|
||||
|
||||
const MAIN_FILE: &str = "data.json";
|
||||
|
||||
/// A CalDAV source that stores its item in a local file
|
||||
#[derive(Debug, PartialEq)]
|
||||
/// A CalDAV source that stores its item in a local folder
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
backing_file: PathBuf,
|
||||
backing_folder: PathBuf,
|
||||
data: CachedData,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
struct CachedData {
|
||||
#[serde(skip)]
|
||||
calendars: HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>,
|
||||
last_sync: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
/// Get the path to the cache file
|
||||
pub fn cache_file() -> PathBuf {
|
||||
return PathBuf::from(String::from("~/.config/my-tasks/cache.json"))
|
||||
/// Get the path to the cache folder
|
||||
pub fn cache_folder() -> PathBuf {
|
||||
return PathBuf::from(String::from("~/.config/my-tasks/cache/"))
|
||||
}
|
||||
|
||||
/// Initialize a cache from the content of a valid backing file if it exists.
|
||||
/// Initialize a cache from the content of a valid backing folder if it exists.
|
||||
/// Returns an error otherwise
|
||||
pub fn from_file(path: &Path) -> Result<Self, Box<dyn Error>> {
|
||||
let data = match std::fs::File::open(path) {
|
||||
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) {
|
||||
Err(err) => {
|
||||
return Err(format!("Unable to open file {:?}: {}", path, err).into());
|
||||
return Err(format!("Unable to open file {:?}: {}", main_file, err).into());
|
||||
},
|
||||
Ok(file) => serde_json::from_reader(file)?,
|
||||
};
|
||||
|
||||
// ...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))),
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self{
|
||||
backing_file: PathBuf::from(path),
|
||||
backing_folder: PathBuf::from(folder),
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_calendar(path: &Path) -> Result<CachedCalendar, Box<dyn Error>> {
|
||||
let file = std::fs::File::open(&path)?;
|
||||
Ok(serde_json::from_reader(file)?)
|
||||
}
|
||||
|
||||
/// Initialize a cache with the default contents
|
||||
pub fn new(path: &Path) -> Self {
|
||||
pub fn new(folder_path: &Path) -> Self {
|
||||
Self{
|
||||
backing_file: PathBuf::from(path),
|
||||
backing_folder: PathBuf::from(folder_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,
|
||||
};
|
||||
/// 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)?;
|
||||
|
||||
if let Err(err) = serde_json::to_writer(file, &self.data) {
|
||||
log::warn!("Unable to serialize: {}", err);
|
||||
return;
|
||||
};
|
||||
// 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(())
|
||||
}
|
||||
|
||||
|
||||
|
@ -95,6 +132,7 @@ impl Cache {
|
|||
let calendars_r = other.get_calendars().await?;
|
||||
|
||||
if keys_are_the_same(&calendars_l, &calendars_r) == false {
|
||||
log::debug!("Different keys for calendars");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
@ -109,6 +147,7 @@ impl Cache {
|
|||
let items_r = cal_r.get_items();
|
||||
|
||||
if keys_are_the_same(&items_l, &items_r) == false {
|
||||
log::debug!("Different keys for items");
|
||||
return Ok(false);
|
||||
}
|
||||
for (id_l, item_l) in items_l {
|
||||
|
@ -116,8 +155,8 @@ impl Cache {
|
|||
Some(c) => c,
|
||||
None => return Err("should not happen, we've just tested keys are the same".into()),
|
||||
};
|
||||
//println!(" items {} {}", item_r.name(), item_l.name());
|
||||
if &item_l != item_r {
|
||||
log::debug!("Different items");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
@ -170,9 +209,11 @@ mod tests {
|
|||
use url::Url;
|
||||
use crate::calendar::SupportedComponents;
|
||||
|
||||
#[test]
|
||||
fn serde_cache() {
|
||||
let cache_path = PathBuf::from(String::from("cache.json"));
|
||||
#[tokio::test]
|
||||
async fn serde_cache() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
||||
let cache_path = PathBuf::from(String::from("test_cache/"));
|
||||
|
||||
let mut cache = Cache::new(&cache_path);
|
||||
|
||||
|
@ -181,9 +222,14 @@ mod tests {
|
|||
SupportedComponents::TODO);
|
||||
cache.add_calendar(Arc::new(Mutex::new(cal1)));
|
||||
|
||||
cache.save_to_file();
|
||||
|
||||
let retrieved_cache = Cache::from_file(&cache_path).unwrap();
|
||||
assert_eq!(cache, retrieved_cache);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,27 +45,6 @@ static CAL_BODY: &str = r#"
|
|||
</d:propfind>
|
||||
"#;
|
||||
|
||||
static TASKS_BODY: &str = r#"
|
||||
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop xmlns:D="DAV:">
|
||||
<D:getetag/>
|
||||
<C:calendar-data/>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VTODO">
|
||||
<C:prop-filter name="COMPLETED">
|
||||
<C:is-not-defined/>
|
||||
</C:prop-filter>
|
||||
<C:prop-filter name="STATUS">
|
||||
<C:text-match
|
||||
negate-condition="yes">CANCELLED</C:text-match>
|
||||
</C:prop-filter>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"#;
|
||||
|
||||
|
||||
/// A CalDAV source that fetches its data from a CalDAV server
|
||||
|
@ -226,79 +205,6 @@ impl Client {
|
|||
|
||||
#[async_trait]
|
||||
impl CalDavSource<RemoteCalendar> for Client {
|
||||
/// Return the list of calendars, or fetch from server if not known yet
|
||||
/*
|
||||
async fn get_calendars(&self) -> Result<&HashMap<CalendarId, RemoteCalendar>, Box<dyn Error>> {
|
||||
let mut replies = self.cached_replies.lock().unwrap();
|
||||
|
||||
if let Some(c) = &replies.calendars {
|
||||
return Ok(c);
|
||||
}
|
||||
let cal_home_set = self.get_cal_home_set().await?;
|
||||
|
||||
let text = self.sub_request(&cal_home_set, CAL_BODY.into(), 1).await?;
|
||||
|
||||
let root: Element = text.parse().unwrap();
|
||||
let reps = find_elems(&root, "response");
|
||||
let mut calendars = HashMap::new();
|
||||
for rep in reps {
|
||||
let display_name = find_elem(rep, "displayname").map(|e| e.text()).unwrap_or("<no name>".to_string());
|
||||
log::debug!("Considering calendar {}", display_name);
|
||||
|
||||
// We filter out non-calendar items
|
||||
let resource_types = match find_elem(rep, "resourcetype") {
|
||||
None => continue,
|
||||
Some(rt) => rt,
|
||||
};
|
||||
let mut found_calendar_type = false;
|
||||
for resource_type in resource_types.children() {
|
||||
if resource_type.name() == "calendar" {
|
||||
found_calendar_type = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if found_calendar_type == false {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We filter out the root calendar collection, that has an empty supported-calendar-component-set
|
||||
let el_supported_comps = match find_elem(rep, "supported-calendar-component-set") {
|
||||
None => continue,
|
||||
Some(comps) => comps,
|
||||
};
|
||||
if el_supported_comps.children().count() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let calendar_href = match find_elem(rep, "href") {
|
||||
None => {
|
||||
log::warn!("Calendar {} has no URL! Ignoring it.", display_name);
|
||||
continue;
|
||||
},
|
||||
Some(h) => h.text(),
|
||||
};
|
||||
|
||||
let mut this_calendar_url = self.url.clone();
|
||||
this_calendar_url.set_path(&calendar_href);
|
||||
|
||||
let supported_components = match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) {
|
||||
Err(err) => {
|
||||
log::warn!("Calendar {} has invalid supported components ({})! Ignoring it.", display_name, err);
|
||||
continue;
|
||||
},
|
||||
Ok(sc) => sc,
|
||||
};
|
||||
let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components);
|
||||
log::info!("Found calendar {}", this_calendar.name());
|
||||
calendars.insert(this_calendar.id().clone(), this_calendar);
|
||||
}
|
||||
|
||||
replies.calendars = Some(calendars);
|
||||
Ok(&calendars)
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
|
||||
self.populate_calendars().await?;
|
||||
|
||||
|
@ -318,6 +224,5 @@ impl CalDavSource<RemoteCalendar> for Client {
|
|||
.and_then(|cals| cals.get(&id))
|
||||
.map(|cal| cal.clone())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
13
src/utils.rs
13
src/utils.rs
|
@ -7,6 +7,7 @@ use minidom::Element;
|
|||
|
||||
use crate::traits::CompleteCalendar;
|
||||
use crate::calendar::CalendarId;
|
||||
use crate::Item;
|
||||
|
||||
/// Walks an XML tree and returns every element that has the given name
|
||||
pub fn find_elems<S: AsRef<str>>(root: &Element, searched_name: S) -> Vec<&Element> {
|
||||
|
@ -64,9 +65,17 @@ where
|
|||
for (id, cal) in cals {
|
||||
println!("CAL {}", id);
|
||||
for (_, item) in cal.lock().unwrap().get_items() {
|
||||
let task = item.unwrap_task();
|
||||
print_task(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_task(item: &Item) {
|
||||
match item {
|
||||
Item::Task(task) => {
|
||||
let completion = if task.completed() {"✓"} else {" "};
|
||||
println!(" {} {}\t{}", completion, task.name(), task.id());
|
||||
}
|
||||
},
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue