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 ;
2021-03-04 23:09:00 +01:00
use std ::collections ::HashMap ;
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 ;
use crate ::traits ::CalDavSource ;
2021-03-28 01:22:24 +01:00
use crate ::traits ::BaseCalendar ;
2021-03-04 23:09:00 +01:00
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-04-04 00:35:59 +02:00
use crate ::calendar ::SupportedComponents ;
2021-02-25 00:53:50 +01:00
2021-04-13 23:32:07 +02:00
#[ cfg(feature = " local_calendar_mocks_remote_calendars " ) ]
use crate ::mock_behaviour ::MockBehaviour ;
2021-03-21 00:11:35 +01:00
const MAIN_FILE : & str = " data.json " ;
2021-02-25 00:53:50 +01:00
2021-04-13 23:29:14 +02:00
/// A CalDAV source that stores its items in a local folder.
///
2021-04-17 19:40:55 +02:00
/// It automatically updates the content of the folder when dropped (see its `Drop` implementation), but you can also manually call [`Cache::save_to_folder`]
2021-03-21 00:11:35 +01:00
#[ 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-04-04 00:35:59 +02:00
2021-04-13 23:32:07 +02:00
/// In tests, we may add forced errors to this object
2021-04-04 00:35:59 +02:00
#[ cfg(feature = " local_calendar_mocks_remote_calendars " ) ]
2021-04-13 23:32:07 +02:00
mock_behaviour : Option < Arc < Mutex < MockBehaviour > > > ,
2021-02-25 00:53:50 +01:00
}
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-25 00:53:50 +01:00
}
impl Cache {
2021-04-04 00:35:59 +02:00
/// Activate the "mocking remote source" features (i.e. tell its children calendars that they are mocked remote calendars)
#[ cfg(feature = " local_calendar_mocks_remote_calendars " ) ]
2021-04-13 23:32:07 +02:00
pub fn set_mock_behaviour ( & mut self , mock_behaviour : Option < Arc < Mutex < MockBehaviour > > > ) {
self . mock_behaviour = mock_behaviour ;
2021-04-04 00:35:59 +02:00
}
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-04-04 00:35:59 +02:00
#[ cfg(feature = " local_calendar_mocks_remote_calendars " ) ]
2021-04-13 23:32:07 +02:00
mock_behaviour : None ,
2021-02-25 00:53:50 +01:00
} )
}
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-04-04 00:35:59 +02:00
#[ cfg(feature = " local_calendar_mocks_remote_calendars " ) ]
2021-04-13 23:32:07 +02:00
mock_behaviour : None ,
2021-02-25 00:53:50 +01:00
}
}
2021-03-21 00:11:35 +01:00
/// Store the current Cache to its backing folder
2021-04-13 23:29:14 +02:00
///
/// Note that this is automatically called when `self` is `drop`ped
pub fn save_to_folder ( & self ) -> Result < ( ) , std ::io ::Error > {
2021-03-21 00:11:35 +01:00
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 ) ? ;
}
2021-04-13 23:29:14 +02:00
2021-03-21 00:11:35 +01:00
Ok ( ( ) )
2021-02-25 00:53:50 +01:00
}
2021-02-28 18:02:01 +01:00
2021-03-04 23:09:00 +01:00
/// Compares two Caches to check they have the same current content
///
2021-04-13 23:29:14 +02:00
/// This is not a complete equality test: some attributes (sync status...) may differ. This should mostly be used in tests
2021-04-17 20:20:30 +02:00
#[ cfg(any(test, feature = " integration_tests " )) ]
2021-04-04 01:02:37 +02:00
pub async fn has_same_observable_content_as ( & self , other : & Self ) -> Result < bool , Box < dyn Error > > {
2021-03-04 23:09:00 +01:00
let calendars_l = self . get_calendars ( ) . await ? ;
let calendars_r = other . get_calendars ( ) . await ? ;
2021-04-04 01:02:37 +02:00
if crate ::utils ::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 ) ;
}
2021-04-03 17:42:55 +02:00
for ( calendar_id , cal_l ) in calendars_l {
log ::debug! ( " Comparing calendars {} " , calendar_id ) ;
2021-03-18 23:59:06 +01:00
let cal_l = cal_l . lock ( ) . unwrap ( ) ;
2021-04-03 17:42:55 +02:00
let cal_r = match calendars_r . get ( & calendar_id ) {
2021-03-18 23:59:06 +01:00
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 ( ) ) ,
} ;
2021-04-04 01:02:37 +02:00
// TODO: check calendars have the same names/ID/whatever
if cal_l . has_same_observable_content_as ( & cal_r ) . await ? = = false {
log ::debug! ( " Different calendars " ) ;
return Ok ( false )
2021-03-28 01:22:24 +01:00
}
2021-04-04 01:02:37 +02:00
2021-03-28 01:22:24 +01:00
}
2021-03-04 23:09:00 +01:00
Ok ( true )
}
}
2021-04-13 23:29:14 +02:00
impl Drop for Cache {
fn drop ( & mut self ) {
if let Err ( err ) = self . save_to_folder ( ) {
2021-04-17 19:40:55 +02:00
log ::error! ( " Unable to automatically save the cache when it's no longer required: {} " , err ) ;
2021-04-13 23:29:14 +02:00
}
}
}
2021-03-05 23:32:42 +01:00
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 > > {
2021-04-13 23:32:07 +02:00
#[ cfg(feature = " local_calendar_mocks_remote_calendars " ) ]
self . mock_behaviour . as_ref ( ) . map_or ( Ok ( ( ) ) , | b | b . lock ( ) . unwrap ( ) . can_get_calendars ( ) ) ? ;
2021-03-18 23:59:06 +01:00
Ok ( self . data . calendars . iter ( )
. map ( | ( id , cal ) | ( id . clone ( ) , cal . clone ( ) ) )
. collect ( )
)
2021-02-25 00:53:50 +01:00
}
2021-03-21 19:27:55 +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-03-31 08:32:28 +02:00
2021-04-04 00:35:59 +02:00
async fn create_calendar ( & mut self , id : CalendarId , name : String , supported_components : SupportedComponents ) -> Result < Arc < Mutex < CachedCalendar > > , Box < dyn Error > > {
2021-03-31 08:32:28 +02:00
log ::debug! ( " Inserting local calendar {} " , id ) ;
2021-04-13 23:32:07 +02:00
#[ cfg(feature = " local_calendar_mocks_remote_calendars " ) ]
self . mock_behaviour . as_ref ( ) . map_or ( Ok ( ( ) ) , | b | b . lock ( ) . unwrap ( ) . can_create_calendar ( ) ) ? ;
2021-04-04 00:35:59 +02:00
let new_calendar = CachedCalendar ::new ( name , id . clone ( ) , supported_components ) ;
2021-04-03 09:24:20 +02:00
let arc = Arc ::new ( Mutex ::new ( new_calendar ) ) ;
2021-04-04 00:35:59 +02:00
#[ cfg(feature = " local_calendar_mocks_remote_calendars " ) ]
2021-04-13 23:32:07 +02:00
if let Some ( behaviour ) = & self . mock_behaviour {
arc . lock ( ) . unwrap ( ) . set_mock_behaviour ( Some ( Arc ::clone ( behaviour ) ) ) ;
} ;
2021-04-04 00:35:59 +02:00
2021-04-03 09:24:20 +02:00
match self . data . calendars . insert ( id , arc . clone ( ) ) {
2021-03-31 08:32:28 +02:00
Some ( _ ) = > Err ( " Attempt to insert calendar failed: there is alredy such a calendar. " . into ( ) ) ,
2021-04-04 00:35:59 +02:00
None = > Ok ( arc ) ,
2021-03-31 08:32:28 +02:00
}
}
2021-02-25 00:53:50 +01:00
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use url ::Url ;
use crate ::calendar ::SupportedComponents ;
2021-04-13 23:19:41 +02:00
use crate ::item ::Item ;
use crate ::task ::Task ;
2021-02-25 00:53:50 +01:00
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-04-17 19:40:55 +02:00
let _shopping_list = cache . create_calendar (
2021-04-04 00:35:59 +02:00
Url ::parse ( " https://caldav.com/shopping " ) . unwrap ( ) ,
2021-04-13 23:19:41 +02:00
" My shopping list " . to_string ( ) ,
2021-04-04 00:35:59 +02:00
SupportedComponents ::TODO ,
) . await . unwrap ( ) ;
2021-03-31 08:32:28 +02:00
2021-04-13 23:19:41 +02:00
let bucket_list = cache . create_calendar (
Url ::parse ( " https://caldav.com/bucket-list " ) . unwrap ( ) ,
" My bucket list " . to_string ( ) ,
SupportedComponents ::TODO ,
) . await . unwrap ( ) ;
{
let mut bucket_list = bucket_list . lock ( ) . unwrap ( ) ;
let cal_id = bucket_list . id ( ) . clone ( ) ;
bucket_list . add_item ( Item ::Task ( Task ::new (
String ::from ( " Attend a concert of JS Bach " ) , false , & cal_id
) ) ) . await . unwrap ( ) ;
bucket_list . add_item ( Item ::Task ( Task ::new (
String ::from ( " Climb the Lighthouse of Alexandria " ) , true , & cal_id
) ) ) . await . unwrap ( ) ;
}
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 ) ;
2021-04-04 01:02:37 +02:00
let test = cache . has_same_observable_content_as ( & retrieved_cache ) . await ;
2021-03-21 00:11:35 +01:00
println! ( " Equal? {:?} " , test ) ;
assert_eq! ( test . unwrap ( ) , true ) ;
2021-02-25 00:53:50 +01:00
}
}