37 Commits
Author SHA1 Message Date
daladim 96eec57951 Added a github actions file 2021-11-10 22:52:47 +01:00
daladim 0f55850b6d [cleanup] Deprecated settings.rs for config.rs 2021-11-10 22:47:33 +01:00
daladim 413b2b285e [cleanup] 2021-11-10 22:45:46 +01:00
daladim 582c187e04 Merge branch 'ical_unknown_fields' 2021-11-10 22:22:57 +01:00
daladim 9dcf7e0499 Fixed tests to support unknown iCal fields 2021-11-10 22:22:24 +01:00
daladim b0db19b586 Forked ical-rs to support serde 2021-11-10 22:16:40 +01:00
daladim 75fe00983d Added a parsing/building round-trip test 2021-11-10 22:16:40 +01:00
daladim 39867a9f15 PRODID is parsed, stored and generated 2021-11-10 22:16:40 +01:00
daladim f7acadc3e2 Unhandled lines from iCal files are stored 2021-11-10 22:16:40 +01:00
daladim 49bdb3d199 Macro for code de-duplication 2021-11-05 23:37:25 +01:00
daladim d6e93d846e [minor] Turned a if list into a match 2021-11-04 18:55:12 +01:00
daladim 40503f46d5 typo 2021-11-04 18:54:53 +01:00
daladim 7d09d72175 SyncEvent is more usable 2021-11-04 09:01:25 +01:00
daladim d8c89ec727 [doc] 2021-11-03 21:58:00 +01:00
daladim 9bd45d5a07 Merge branch 'feedback' 2021-10-13 08:19:41 +02:00
daladim b404fc68e8 Sending sync progress 2021-10-09 00:25:23 +02:00
daladim 7fb98a471b Feedback infrastructure 2021-10-09 00:23:50 +02:00
daladim 04c8b3a2ee Renamed SyncResult -> SyncProgress 2021-10-08 23:26:48 +02:00
daladim f7ec5d29be [doc] 2021-10-08 23:03:43 +02:00
daladim 38fd04c3a4 Moved SyncResult 2021-10-08 23:03:24 +02:00
daladim 2e4be4a754 License, readme and metadata 2021-09-29 23:47:00 +02:00
daladim 4c54515b79 More logs 2021-09-29 08:38:27 +02:00
daladim e563cb3b2f Comparison functions are no longer provided 2021-08-12 20:05:58 +02:00
daladim 92b3089e5e [typo] 2021-08-12 19:06:17 +02:00
daladim db662d2f4a Non-async versions of deletion functions 2021-08-12 19:06:17 +02:00
daladim 9234d57cdc Non-async versions of add_item/update_item 2021-08-12 19:06:17 +02:00
daladim b6e8ce2421 [minor] Removed useless asyncs 2021-08-12 19:06:17 +02:00
daladim 6d984cdc7f [doc] 2021-08-12 19:06:17 +02:00
daladim 78bb78f61f [fixup] Doc and usability 2021-07-08 16:05:15 +02:00
daladim 17716575d8 Added support for calendar colors 2021-07-08 16:01:54 +02:00
daladim 2b339a7aff Added sorting utilities 2021-07-05 11:28:06 +02:00
daladim fbd076875a [doc] 2021-07-05 11:08:18 +02:00
daladim 72e05dc6c9 [deps] Update 2021-07-05 11:07:49 +02:00
daladim e6dda89d01 pub use Cache 2021-07-05 11:06:55 +02:00
daladim bf68d81ec8 More derive(Debug) 2021-07-05 11:06:55 +02:00
daladim 58cfb42f64 Non-async version of some functions + renamed a function 2021-07-05 11:06:55 +02:00
daladim ab8f6a9413 Moved a file 2021-07-05 11:06:55 +02:00
28 changed files with 863 additions and 449 deletions
+24
View File
@@ -0,0 +1,24 @@
name: Rust
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run regular tests
run: cargo test --verbose
- name: Run specific integration tests
run: cargo test --verbose --features=integration_tests
Generated
+163 -24
View File
@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.15"
@@ -103,6 +105,16 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "csscolorparser"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2fb3bd93ef32553e3d5b9f8020028f41ac64ff8a230033d5d548b8222d21fbe"
dependencies = [
"phf",
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.28"
@@ -158,43 +170,55 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.12"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846"
checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.12"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65"
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1"
[[package]]
name = "futures-sink"
version = "0.3.12"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6"
checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282"
[[package]]
name = "futures-task"
version = "0.3.12"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86"
checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae"
[[package]]
name = "futures-util"
version = "0.3.12"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b"
checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967"
dependencies = [
"autocfg",
"futures-core",
"futures-task",
"pin-project-lite",
"pin-utils",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.2"
@@ -203,7 +227,7 @@ checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.10.2+wasi-snapshot-preview1",
]
[[package]]
@@ -319,10 +343,10 @@ dependencies = [
[[package]]
name = "ical"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9f7215ad0d77e69644570dee000c7678a47ba7441062c1b5f918adde0d73cf"
version = "0.6.0"
source = "git+https://github.com/daladim/ical-rs.git?branch=ical_serde#f3a182eee5f1f6acf44fa1512839602d7105a899"
dependencies = [
"serde",
"thiserror",
]
@@ -381,11 +405,13 @@ dependencies = [
"async-trait",
"bitflags",
"chrono",
"csscolorparser",
"env_logger",
"ical",
"ics",
"log",
"minidom",
"once_cell",
"reqwest",
"sanitize-filename",
"serde",
@@ -524,9 +550,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.5.2"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "openssl"
@@ -567,6 +593,50 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_shared",
"proc-macro-hack",
]
[[package]]
name = "phf_generator"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [
"phf_shared",
"rand 0.7.3",
]
[[package]]
name = "phf_macros"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.0.5"
@@ -611,6 +681,12 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.24"
@@ -638,6 +714,20 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc 0.2.0",
"rand_pcg",
]
[[package]]
name = "rand"
version = "0.8.3"
@@ -645,9 +735,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
"rand_chacha 0.3.0",
"rand_core 0.6.2",
"rand_hc 0.3.0",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]]
@@ -657,7 +757,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.2",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]]
@@ -666,7 +775,16 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
dependencies = [
"getrandom",
"getrandom 0.2.2",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
@@ -675,7 +793,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
dependencies = [
"rand_core",
"rand_core 0.6.2",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
@@ -840,6 +967,12 @@ dependencies = [
"serde",
]
[[package]]
name = "siphasher"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27"
[[package]]
name = "slab"
version = "0.4.2"
@@ -876,7 +1009,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if",
"libc",
"rand",
"rand 0.8.3",
"redox_syscall",
"remove_dir_all",
"winapi",
@@ -1081,7 +1214,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
"getrandom 0.2.2",
]
[[package]]
@@ -1100,6 +1233,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
+11 -1
View File
@@ -3,6 +3,11 @@ name = "kitchen-fridge"
version = "0.1.0"
authors = ["daladim"]
edition = "2018"
description = "A CalDAV (ical file management over WebDAV) library"
repository = "https://github.com/daladim/kitchen-fridge"
license = "MIT"
keywords = ["CalDAV", "client", "WebDAV", "todo", "RFC4791"]
categories = ["network-programming", "web-programming::http-client"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -23,6 +28,11 @@ serde_json = "1.0"
async-trait = "0.1"
uuid = { version = "0.8", features = ["v4"] }
sanitize-filename = "0.3"
ical = "0.7"
ical = { version = "0.6", features = ["serde-derive"] }
ics = "0.5"
chrono = { version = "0.4", features = ["serde"] }
csscolorparser = { version = "0.5", features = ["serde"] }
once_cell = "1.8"
[patch.crates-io]
ical = { git = "https://github.com/daladim/ical-rs.git", branch = "ical_serde" }
+7
View File
@@ -0,0 +1,7 @@
Copyright © 2021 kitchen-fridge contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
# kitchen-fridge
kitchen-fridge is a CalDAV (iCal file transfer over WebDAV) Rust library.
Its [documentation](https://docs.rs/kitchen-fridge/) is available on docs.rs.
+14 -6
View File
@@ -1,3 +1,5 @@
//! This is an example of how kitchen-fridge can be used
use std::path::Path;
use chrono::{Utc};
@@ -12,16 +14,22 @@ use kitchen_fridge::cache::Cache;
use kitchen_fridge::CalDavProvider;
use kitchen_fridge::traits::BaseCalendar;
use kitchen_fridge::traits::CompleteCalendar;
use kitchen_fridge::settings::URL;
use kitchen_fridge::settings::USERNAME;
use kitchen_fridge::settings::PASSWORD;
use kitchen_fridge::settings::EXAMPLE_CREATED_CALENDAR_URL;
use kitchen_fridge::settings::EXAMPLE_EXISTING_CALENDAR_URL;
use kitchen_fridge::utils::pause;
const CACHE_FOLDER: &str = "test_cache/provider_sync";
// TODO: change these values with yours
pub const URL: &str = "https://my.server.com/remote.php/dav/files/john";
pub const USERNAME: &str = "username";
pub const PASSWORD: &str = "secret_password";
pub const EXAMPLE_TASK_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/6121A0BE-C2E0-4F16-A3FA-658E54E7062A/74439558-CDFF-426C-92CD-ECDDACE971B0.ics";
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/";
pub const EXAMPLE_CREATED_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_that_we_have_created/";
#[tokio::main]
async fn main() {
env_logger::init();
@@ -69,7 +77,7 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider)
let new_calendar_id: CalendarId = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
let new_calendar_name = "A brave new calendar".to_string();
if let Err(_err) = provider.local_mut()
.create_calendar(new_calendar_id.clone(), new_calendar_name.clone(), SupportedComponents::TODO)
.create_calendar(new_calendar_id.clone(), new_calendar_name.clone(), SupportedComponents::TODO, None)
.await {
println!("Unable to add calendar, maybe it exists already. We're not adding it after all.");
}
+23 -8
View File
@@ -9,6 +9,7 @@ use std::ffi::OsStr;
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
use csscolorparser::Color;
use crate::traits::CalDavSource;
use crate::traits::BaseCalendar;
@@ -25,6 +26,8 @@ const MAIN_FILE: &str = "data.json";
/// A CalDAV source that stores its items in a local folder.
///
/// It automatically updates the content of the folder when dropped (see its `Drop` implementation), but you can also manually call [`Cache::save_to_folder`]
///
/// Most of its methods are part of the `CalDavSource` trait implementation
#[derive(Debug)]
pub struct Cache {
backing_folder: PathBuf,
@@ -180,10 +183,8 @@ impl Drop for Cache {
}
}
#[async_trait]
impl CalDavSource<CachedCalendar> for Cache {
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
impl Cache {
pub fn get_calendars_sync(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?;
@@ -193,16 +194,27 @@ impl CalDavSource<CachedCalendar> for Cache {
)
}
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> {
pub fn get_calendar_sync(&self, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> {
self.data.calendars.get(id).map(|arc| arc.clone())
}
}
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> {
#[async_trait]
impl CalDavSource<CachedCalendar> for Cache {
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
self.get_calendars_sync()
}
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> {
self.get_calendar_sync(id)
}
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> {
log::debug!("Inserting local calendar {}", id);
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?;
let new_calendar = CachedCalendar::new(name, id.clone(), supported_components);
let new_calendar = CachedCalendar::new(name, id.clone(), supported_components, color);
let arc = Arc::new(Mutex::new(new_calendar));
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
@@ -233,12 +245,14 @@ mod tests {
Url::parse("https://caldav.com/shopping").unwrap(),
"My shopping list".to_string(),
SupportedComponents::TODO,
Some(csscolorparser::parse("lime").unwrap()),
).await.unwrap();
let bucket_list = cache.create_calendar(
Url::parse("https://caldav.com/bucket-list").unwrap(),
"My bucket list".to_string(),
SupportedComponents::TODO,
Some(csscolorparser::parse("#ff8000").unwrap()),
).await.unwrap();
{
@@ -277,11 +291,12 @@ mod tests {
let cache_path = PathBuf::from(String::from("test_cache/sanity_tests"));
let mut cache = populate_cache(&cache_path).await;
// We should not be able to add twice the same calendar
// We should not be able to add a second calendar with the same id
let second_addition_same_calendar = cache.create_calendar(
Url::parse("https://caldav.com/shopping").unwrap(),
"My shopping list".to_string(),
SupportedComponents::TODO,
None,
).await;
assert!(second_addition_same_calendar.is_err());
}
+118 -67
View File
@@ -3,6 +3,7 @@ use std::error::Error;
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
use csscolorparser::Color;
use crate::item::SyncStatus;
use crate::traits::{BaseCalendar, CompleteCalendar};
@@ -17,11 +18,14 @@ use crate::mock_behaviour::MockBehaviour;
/// A calendar used by the [`cache`](crate::cache) module
///
/// Most of its methods are part of traits implementations
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CachedCalendar {
name: String,
id: CalendarId,
supported_components: SupportedComponents,
color: Option<Color>,
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
#[serde(skip)]
mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>,
@@ -38,27 +42,27 @@ impl CachedCalendar {
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
async fn add_item_maybe_mocked(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
fn add_item_maybe_mocked(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
if self.mock_behaviour.is_some() {
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_add_item())?;
self.add_or_update_item_force_synced(item).await
self.add_or_update_item_force_synced(item)
} else {
self.regular_add_or_update_item(item).await
self.regular_add_or_update_item(item)
}
}
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
async fn update_item_maybe_mocked(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
fn update_item_maybe_mocked(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
if self.mock_behaviour.is_some() {
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_update_item())?;
self.add_or_update_item_force_synced(item).await
self.add_or_update_item_force_synced(item)
} else {
self.regular_add_or_update_item(item).await
self.regular_add_or_update_item(item)
}
}
/// Add or update an item
async fn regular_add_or_update_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
fn regular_add_or_update_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
let ss_clone = item.sync_status().clone();
log::debug!("Adding or updating an item with {:?}", ss_clone);
self.items.insert(item.id().clone(), item);
@@ -67,7 +71,7 @@ impl CachedCalendar {
/// Add or update an item, but force a "synced" SyncStatus. This is the normal behaviour that would happen on a server
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
async fn add_or_update_item_force_synced(&mut self, mut item: Item) -> Result<SyncStatus, Box<dyn Error>> {
fn add_or_update_item_force_synced(&mut self, mut item: Item) -> Result<SyncStatus, Box<dyn Error>> {
log::debug!("Adding or updating an item, but forces a synced SyncStatus");
match item.sync_status() {
SyncStatus::Synced(_) => (),
@@ -83,7 +87,9 @@ impl CachedCalendar {
pub async fn has_same_observable_content_as(&self, other: &CachedCalendar) -> Result<bool, Box<dyn Error>> {
if self.name != other.name
|| self.id != other.id
|| self.supported_components != other.supported_components {
|| self.supported_components != other.supported_components
|| self.color != other.color
{
log::debug!("Calendar properties mismatch");
return Ok(false);
}
@@ -111,80 +117,59 @@ impl CachedCalendar {
Ok(true)
}
}
#[async_trait]
impl BaseCalendar for CachedCalendar {
fn name(&self) -> &str {
&self.name
}
fn id(&self) -> &CalendarId {
&self.id
}
fn supported_components(&self) -> SupportedComponents {
self.supported_components
}
async fn add_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
if self.items.contains_key(item.id()) {
return Err(format!("Item {:?} cannot be added, it exists already", item.id()).into());
}
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))]
return self.regular_add_or_update_item(item).await;
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
return self.add_item_maybe_mocked(item).await;
}
async fn update_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
if self.items.contains_key(item.id()) == false {
return Err(format!("Item {:?} cannot be updated, it does not already exist", item.id()).into());
}
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))]
return self.regular_add_or_update_item(item).await;
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
return self.update_item_maybe_mocked(item).await;
}
}
#[async_trait]
impl CompleteCalendar for CachedCalendar {
fn new(name: String, id: CalendarId, supported_components: SupportedComponents) -> Self {
Self {
name, id, supported_components,
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
mock_behaviour: None,
items: HashMap::new(),
}
}
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> {
/// The non-async version of [`Self::get_item_ids`]
pub fn get_item_ids_sync(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> {
Ok(self.items.iter()
.map(|(id, _)| id.clone())
.collect()
)
}
async fn get_items(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>> {
/// The non-async version of [`Self::get_items`]
pub fn get_items_sync(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>> {
Ok(self.items.iter()
.map(|(id, item)| (id.clone(), item))
.collect()
)
}
async fn get_item_by_id_ref<'a>(&'a self, id: &ItemId) -> Option<&'a Item> {
/// The non-async version of [`Self::get_item_by_id`]
pub fn get_item_by_id_sync<'a>(&'a self, id: &ItemId) -> Option<&'a Item> {
self.items.get(id)
}
async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> {
/// The non-async version of [`Self::get_item_by_id_mut`]
pub fn get_item_by_id_mut_sync<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> {
self.items.get_mut(id)
}
async fn mark_for_deletion(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
/// The non-async version of [`Self::add_item`]
pub fn add_item_sync(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
if self.items.contains_key(item.id()) {
return Err(format!("Item {:?} cannot be added, it exists already", item.id()).into());
}
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))]
return self.regular_add_or_update_item(item);
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
return self.add_item_maybe_mocked(item);
}
/// The non-async version of [`Self::update_item`]
pub fn update_item_sync(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
if self.items.contains_key(item.id()) == false {
return Err(format!("Item {:?} cannot be updated, it does not already exist", item.id()).into());
}
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))]
return self.regular_add_or_update_item(item);
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
return self.update_item_maybe_mocked(item);
}
/// The non-async version of [`Self::mark_for_deletion`]
pub fn mark_for_deletion_sync(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
match self.items.get_mut(item_id) {
None => Err("no item for this key".into()),
Some(item) => {
@@ -211,12 +196,78 @@ impl CompleteCalendar for CachedCalendar {
}
}
async fn immediately_delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
/// The non-async version of [`Self::immediately_delete_item`]
pub fn immediately_delete_item_sync(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
match self.items.remove(item_id) {
None => Err(format!("Item {} is absent from this calendar", item_id).into()),
Some(_) => Ok(())
}
}
}
#[async_trait]
impl BaseCalendar for CachedCalendar {
fn name(&self) -> &str {
&self.name
}
fn id(&self) -> &CalendarId {
&self.id
}
fn supported_components(&self) -> SupportedComponents {
self.supported_components
}
fn color(&self) -> Option<&Color> {
self.color.as_ref()
}
async fn add_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
self.add_item_sync(item)
}
async fn update_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
self.update_item_sync(item)
}
}
#[async_trait]
impl CompleteCalendar for CachedCalendar {
fn new(name: String, id: CalendarId, supported_components: SupportedComponents, color: Option<Color>) -> Self {
Self {
name, id, supported_components, color,
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
mock_behaviour: None,
items: HashMap::new(),
}
}
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> {
self.get_item_ids_sync()
}
async fn get_items(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>> {
self.get_items_sync()
}
async fn get_item_by_id<'a>(&'a self, id: &ItemId) -> Option<&'a Item> {
self.get_item_by_id_sync(id)
}
async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> {
self.get_item_by_id_mut_sync(id)
}
async fn mark_for_deletion(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
self.mark_for_deletion_sync(item_id)
}
async fn immediately_delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
self.immediately_delete_item_sync(item_id)
}
}
@@ -231,8 +282,8 @@ use crate::{item::VersionTag,
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
#[async_trait]
impl DavCalendar for CachedCalendar {
fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self {
crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components)
fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self {
crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components, color)
}
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> {
+8 -2
View File
@@ -4,6 +4,7 @@ use std::sync::Mutex;
use async_trait::async_trait;
use reqwest::{header::CONTENT_TYPE, header::CONTENT_LENGTH};
use csscolorparser::Color;
use crate::traits::BaseCalendar;
use crate::traits::DavCalendar;
@@ -32,10 +33,12 @@ static TASKS_BODY: &str = r#"
/// A CalDAV calendar created by a [`Client`](crate::client::Client).
#[derive(Debug)]
pub struct RemoteCalendar {
name: String,
resource: Resource,
supported_components: SupportedComponents,
color: Option<Color>,
cached_version_tags: Mutex<Option<HashMap<ItemId, VersionTag>>>,
}
@@ -47,6 +50,9 @@ impl BaseCalendar for RemoteCalendar {
fn supported_components(&self) -> crate::calendar::SupportedComponents {
self.supported_components
}
fn color(&self) -> Option<&Color> {
self.color.as_ref()
}
async fn add_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
let ical_text = crate::ical::build_from(&item)?;
@@ -113,9 +119,9 @@ impl BaseCalendar for RemoteCalendar {
#[async_trait]
impl DavCalendar for RemoteCalendar {
fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self {
fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self {
Self {
name, resource, supported_components,
name, resource, supported_components, color,
cached_version_tags: Mutex::new(None),
}
}
+13 -3
View File
@@ -10,6 +10,7 @@ use reqwest::{Method, StatusCode};
use reqwest::header::CONTENT_TYPE;
use minidom::Element;
use url::Url;
use csscolorparser::Color;
use crate::resource::Resource;
use crate::utils::{find_elem, find_elems};
@@ -42,6 +43,7 @@ static CAL_BODY: &str = r#"
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
<d:prop>
<d:displayname />
<E:calendar-color xmlns:E="http://apple.com/ns/ical/"/>
<d:resourcetype />
<c:supported-calendar-component-set />
</d:prop>
@@ -97,6 +99,7 @@ pub(crate) async fn sub_request_and_extract_elems(resource: &Resource, method: &
/// A CalDAV data source that fetches its data from a CalDAV server
#[derive(Debug)]
pub struct Client {
resource: Resource,
@@ -106,7 +109,7 @@ pub struct Client {
}
#[derive(Default)]
#[derive(Debug, Default)]
struct CachedReplies {
principal: Option<Resource>,
calendar_home_set: Option<Resource>,
@@ -204,7 +207,14 @@ impl Client {
},
Ok(sc) => sc,
};
let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components);
let this_calendar_color = find_elem(&rep, "calendar-color")
.and_then(|col| {
col.texts().next()
.and_then(|t| csscolorparser::parse(t).ok())
});
let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color);
log::info!("Found calendar {}", this_calendar.name());
calendars.insert(this_calendar.id().clone(), Arc::new(Mutex::new(this_calendar)));
}
@@ -242,7 +252,7 @@ impl CalDavSource<RemoteCalendar> for Client {
.map(|cal| cal.clone())
}
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
self.populate_calendars().await?;
match self.cached_replies.lock().unwrap().calendars.as_ref() {
+11
View File
@@ -0,0 +1,11 @@
//! Support for compile-time configuration options
use once_cell::sync::Lazy;
/// Part of the ProdID string that describes the organization (example of a ProdID string: `-//ABC Corporation//My Product//EN`)
/// You can override it at compile-time with the `KITCHEN_FRIDGE_ICAL_ORG_NAME` environment variable, or keep the default value
pub static ORG_NAME: Lazy<String> = Lazy::new(|| option_env!("KITCHEN_FRIDGE_ICAL_ORG_NAME").unwrap_or("My organization").to_string() );
/// Part of the ProdID string that describes the product name (example of a ProdID string: `-//ABC Corporation//My Product//EN`)
/// You can override it at compile-time with the `KITCHEN_FRIDGE_ICAL_PRODUCT_NAME` environment variable, or keep the default value
pub static PRODUCT_NAME: Lazy<String> = Lazy::new(|| option_env!("KITCHEN_FRIDGE_ICAL_PRODUCT_NAME").unwrap_or("KitchenFridge").to_string() );
+4
View File
@@ -32,6 +32,10 @@ impl Event {
&self.name
}
pub fn ical_prod_id(&self) -> &str {
unimplemented!()
}
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
unimplemented!()
}
+40 -18
View File
@@ -5,33 +5,38 @@ use std::error::Error;
use chrono::{DateTime, Utc};
use ics::properties::{Completed, Created, LastModified, PercentComplete, Status, Summary};
use ics::{ICalendar, ToDo};
use ics::components::Parameter as IcsParameter;
use ics::components::Property as IcsProperty;
use ical::property::Property as IcalProperty;
use crate::Task;
use crate::item::Item;
use crate::task::CompletionStatus;
use crate::settings::{ORG_NAME, PRODUCT_NAME};
fn ical_product_id() -> String {
format!("-//{}//{}//EN", ORG_NAME, PRODUCT_NAME)
}
/// Create an iCal item from a `crate::item::Item`
pub fn build_from(item: &Item) -> Result<String, Box<dyn Error>> {
let s_last_modified = format_date_time(item.last_modified());
match item {
Item::Task(t) => build_from_task(t),
_ => unimplemented!(),
}
}
pub fn build_from_task(task: &Task) -> Result<String, Box<dyn Error>> {
let s_last_modified = format_date_time(task.last_modified());
let mut todo = ToDo::new(
item.uid(),
task.uid(),
s_last_modified.clone(),
);
item.creation_date().map(|dt|
task.creation_date().map(|dt|
todo.push(Created::new(format_date_time(dt)))
);
todo.push(LastModified::new(s_last_modified));
todo.push(Summary::new(item.name()));
todo.push(Summary::new(task.name()));
match item {
Item::Task(t) => {
match t.completion_status() {
match task.completion_status() {
CompletionStatus::Uncompleted => {
todo.push(Status::needs_action());
},
@@ -43,13 +48,14 @@ pub fn build_from(item: &Item) -> Result<String, Box<dyn Error>> {
todo.push(Status::completed());
}
}
},
_ => {
unimplemented!()
},
// Also add fields that we have not handled
for ical_property in task.extra_parameters() {
let ics_property = ical_to_ics_property(ical_property.clone());
todo.push(ics_property);
}
let mut calendar = ICalendar::new("2.0", ical_product_id());
let mut calendar = ICalendar::new("2.0", task.ical_prod_id());
calendar.add_todo(todo);
Ok(calendar.to_string())
@@ -60,10 +66,26 @@ fn format_date_time(dt: &DateTime<Utc>) -> String {
}
fn ical_to_ics_property(prop: IcalProperty) -> IcsProperty<'static> {
let mut ics_prop = match prop.value {
Some(value) => IcsProperty::new(prop.name, value),
None => IcsProperty::new(prop.name, ""),
};
prop.params.map(|v| {
for (key, vec_values) in v {
let values = vec_values.join(";");
ics_prop.add(IcsParameter::new(key, values));
}
});
ics_prop
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Task;
use crate::config::{ORG_NAME, PRODUCT_NAME};
#[test]
fn test_ical_from_completed_task() {
@@ -82,7 +104,7 @@ mod tests {
COMPLETED:{}\r\n\
STATUS:COMPLETED\r\n\
END:VTODO\r\n\
END:VCALENDAR\r\n", ORG_NAME, PRODUCT_NAME, uid, s_now, s_now, s_now, s_now);
END:VCALENDAR\r\n", *ORG_NAME, *PRODUCT_NAME, uid, s_now, s_now, s_now, s_now);
assert_eq!(ical, expected_ical);
}
@@ -102,7 +124,7 @@ mod tests {
SUMMARY:This is a task with ÜTF-8 characters\r\n\
STATUS:NEEDS-ACTION\r\n\
END:VTODO\r\n\
END:VCALENDAR\r\n", ORG_NAME, PRODUCT_NAME, uid, s_now, s_now, s_now);
END:VCALENDAR\r\n", *ORG_NAME, *PRODUCT_NAME, uid, s_now, s_now, s_now);
assert_eq!(ical, expected_ical);
}
+48
View File
@@ -6,3 +6,51 @@ mod parser;
pub use parser::parse;
mod builder;
pub use builder::build_from;
use crate::config::{ORG_NAME, PRODUCT_NAME};
pub fn default_prod_id() -> String {
format!("-//{}//{}//EN", *ORG_NAME, *PRODUCT_NAME)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use crate::item::SyncStatus;
#[test]
fn test_ical_round_trip_serde() {
let ical_with_unknown_fields = std::fs::read_to_string("tests/assets/ical_with_unknown_fields.ics").unwrap();
let item_id = "http://item.id".parse().unwrap();
let sync_status = SyncStatus::NotSynced;
let deserialized = parse(&ical_with_unknown_fields, item_id, sync_status).unwrap();
let serialized = build_from(&deserialized).unwrap();
assert_same_fields(&ical_with_unknown_fields, &serialized);
}
/// Assert the properties are present (possibly in another order)
/// RFC5545 "imposes no ordering of properties within an iCalendar object."
fn assert_same_fields(left: &str, right: &str) {
let left_parts: HashSet<&str> = left.split("\r\n").collect();
let right_parts: HashSet<&str> = right.split("\r\n").collect();
// Let's be more explicit than assert_eq!(left_parts, right_parts);
if left_parts != right_parts {
println!("Only in left:");
for item in left_parts.difference(&right_parts) {
println!(" * {}", item);
}
println!("Only in right:");
for item in right_parts.difference(&left_parts) {
println!(" * {}", item);
}
assert_eq!(left_parts, right_parts);
}
}
}
+42 -24
View File
@@ -17,13 +17,17 @@ use crate::Event;
pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> {
let mut reader = ical::IcalParser::new(content.as_bytes());
let parsed_item = match reader.next() {
None => return Err(format!("Invalid uCal data to parse for item {}", item_id).into()),
None => return Err(format!("Invalid iCal data to parse for item {}", item_id).into()),
Some(item) => match item {
Err(err) => return Err(format!("Unable to parse uCal data for item {}: {}", item_id, err).into()),
Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_id, err).into()),
Ok(item) => item,
}
};
let ical_prod_id = extract_ical_prod_id(&parsed_item)
.map(|s| s.to_string())
.unwrap_or_else(|| super::default_prod_id());
let item = match assert_single_type(&parsed_item)? {
CurrentType::Event(_) => {
Item::Event(Event::new())
@@ -36,11 +40,29 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
let mut last_modified = None;
let mut completion_date = None;
let mut creation_date = None;
let mut extra_parameters = Vec::new();
for prop in &todo.properties {
if prop.name == "SUMMARY" {
name = prop.value.clone();
}
if prop.name == "STATUS" {
match prop.name.as_str() {
"SUMMARY" => { name = prop.value.clone() },
"UID" => { uid = prop.value.clone() },
"DTSTAMP" => {
// The property can be specified once, but is not mandatory
// "This property specifies the date and time that the information associated with
// the calendar component was last revised in the calendar store."
last_modified = parse_date_time_from_property(&prop.value)
},
"COMPLETED" => {
// The property can be specified once, but is not mandatory
// "This property defines the date and time that a to-do was
// actually completed."
completion_date = parse_date_time_from_property(&prop.value)
},
"CREATED" => {
// The property can be specified once, but is not mandatory
creation_date = parse_date_time_from_property(&prop.value)
},
"STATUS" => {
// Possible values:
// "NEEDS-ACTION" ;Indicates to-do needs action.
// "COMPLETED" ;Indicates to-do completed.
@@ -50,24 +72,10 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
completed = true;
}
}
if prop.name == "UID" {
uid = prop.value.clone();
_ => {
// This field is not supported. Let's store it anyway, so that we are able to re-create an identical iCal file
extra_parameters.push(prop.clone());
}
if prop.name == "DTSTAMP" {
// The property can be specified once, but is not mandatory
// "This property specifies the date and time that the information associated with
// the calendar component was last revised in the calendar store."
last_modified = parse_date_time_from_property(&prop.value)
}
if prop.name == "COMPLETED" {
// The property can be specified once, but is not mandatory
// "This property defines the date and time that a to-do was
// actually completed."
completion_date = parse_date_time_from_property(&prop.value)
}
if prop.name == "CREATED" {
// The property can be specified once, but is not mandatory
creation_date = parse_date_time_from_property(&prop.value)
}
}
let name = match name {
@@ -92,7 +100,7 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
true => CompletionStatus::Completed(completion_date),
};
Item::Task(Task::new_with_parameters(name, uid, item_id, completion_status, sync_status, creation_date, last_modified))
Item::Task(Task::new_with_parameters(name, uid, item_id, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters))
},
};
@@ -122,6 +130,16 @@ fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>
}
fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> {
for prop in &item.properties {
if &prop.name == "PRODID" {
return prop.value.as_ref().map(|s| s.as_str())
}
}
None
}
enum CurrentType<'a> {
Event(&'a IcalEvent),
Todo(&'a IcalTodo),
+20 -41
View File
@@ -19,48 +19,27 @@ pub enum Item {
Task(crate::task::Task),
}
/// Returns `task.$property_name` or `event.$property_name`, depending on whether self is a Task or an Event
macro_rules! synthetise_common_getter {
($property_name:ident, $return_type:ty) => {
pub fn $property_name(&self) -> $return_type {
match self {
Item::Event(e) => e.$property_name(),
Item::Task(t) => t.$property_name(),
}
}
}
}
impl Item {
pub fn id(&self) -> &ItemId {
match self {
Item::Event(e) => e.id(),
Item::Task(t) => t.id(),
}
}
synthetise_common_getter!(id, &ItemId);
synthetise_common_getter!(uid, &str);
synthetise_common_getter!(name, &str);
synthetise_common_getter!(creation_date, Option<&DateTime<Utc>>);
synthetise_common_getter!(last_modified, &DateTime<Utc>);
synthetise_common_getter!(sync_status, &SyncStatus);
synthetise_common_getter!(ical_prod_id, &str);
pub fn uid(&self) -> &str {
match self {
Item::Event(e) => e.uid(),
Item::Task(t) => t.uid(),
}
}
pub fn name(&self) -> &str {
match self {
Item::Event(e) => e.name(),
Item::Task(t) => t.name(),
}
}
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
match self {
Item::Event(e) => e.creation_date(),
Item::Task(t) => t.creation_date(),
}
}
pub fn last_modified(&self) -> &DateTime<Utc> {
match self {
Item::Event(e) => e.last_modified(),
Item::Task(t) => t.last_modified(),
}
}
pub fn sync_status(&self) -> &SyncStatus {
match self {
Item::Event(e) => e.sync_status(),
Item::Task(t) => t.sync_status(),
}
}
pub fn set_sync_status(&mut self, new_status: SyncStatus) {
match self {
Item::Event(e) => e.set_sync_status(new_status),
@@ -206,7 +185,7 @@ impl VersionTag {
/// Desribes whether this item has been synced already, or modified since the last time it was synced
/// Describes whether this item has been synced already, or modified since the last time it was synced
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum SyncStatus {
/// This item has ben locally created, and never synced yet
+4 -2
View File
@@ -18,7 +18,8 @@
//!
//! ## Examples
//!
//! See example usage in the `examples/` folder, that you can run using `cargo run --example <example-name>`
//! See example usage in the `examples/` folder, that you can run using `cargo run --example <example-name>`. \
//! You can also have a look at `tasklist`, a GUI app that uses `kitchen-fridge` under the hood.
pub mod traits;
@@ -35,9 +36,10 @@ pub mod mock_behaviour;
pub mod client;
pub use client::Client;
pub mod cache;
pub use cache::Cache;
pub mod ical;
pub mod settings;
pub mod config;
pub mod utils;
pub mod resource;
+127 -90
View File
@@ -1,4 +1,6 @@
//! This modules abstracts data sources and merges them in a single virtual one
//!
//! It is also responsible for syncing them together
use std::error::Error;
use std::collections::HashSet;
@@ -7,44 +9,18 @@ use std::sync::{Arc, Mutex};
use crate::traits::{BaseCalendar, CalDavSource, DavCalendar};
use crate::traits::CompleteCalendar;
use crate::item::SyncStatus;
use crate::item::{ItemId, SyncStatus};
use crate::calendar::CalendarId;
/// A counter of errors that happen during a sync
struct SyncResult {
n_errors: u32,
}
impl SyncResult {
pub fn new() -> Self {
Self { n_errors: 0 }
}
pub fn is_success(&self) -> bool {
self.n_errors == 0
}
pub fn error(&mut self, text: &str) {
log::error!("{}", text);
self.n_errors += 1;
}
pub fn warn(&mut self, text: &str) {
log::warn!("{}", text);
self.n_errors += 1;
}
pub fn info(&mut self, text: &str) {
log::info!("{}", text);
}
pub fn debug(&mut self, text: &str) {
log::debug!("{}", text);
}
pub fn trace(&mut self, text: &str) {
log::trace!("{}", text);
}
}
pub mod sync_progress;
use sync_progress::SyncProgress;
use sync_progress::{FeedbackSender, SyncEvent};
/// A data source that combines two `CalDavSource`s, which is able to sync both sources.
///
/// Usually, you will only need to use a provider between a server and a local cache, that is to say a [`CalDavProvider`](crate::CalDavProvider), i.e. a `Provider<Cache, CachedCalendar, Client, RemoteCalendar>`. \
/// However, providers can be used for integration tests, where the remote source is mocked by a `Cache`.
#[derive(Debug)]
pub struct Provider<L, T, R, U>
where
L: CalDavSource<T>,
@@ -84,28 +60,43 @@ where
pub fn local_mut(&mut self) -> &mut L { &mut self.local }
/// Returns the data source described as `remote`.
///
/// Apart in tests, there are very few (if any) reasons to access `remote` directly.
/// Apart from tests, there are very few (if any) reasons to access `remote` directly.
/// Usually, you should rather use the `local` source, which (usually) is a much faster local cache.
/// To be sure `local` accurately mirrors the `remote` source, you can run [`Provider::sync`]
pub fn remote(&self) -> &R { &self.remote }
/// Performs a synchronisation between `local` and `remote`.
/// Performs a synchronisation between `local` and `remote`, and provide feeedback to the user about the progress.
///
/// This bidirectional sync applies additions/deletions made on a source to the other source.
/// In case of conflicts (the same item has been modified on both ends since the last sync, `remote` always wins)
/// In case of conflicts (the same item has been modified on both ends since the last sync, `remote` always wins).
///
/// It returns whether the sync was totally successful (details about errors are logged using the `log::*` macros).
/// In case errors happened, the sync might have been partially executed, and you can safely run this function again, since it has been designed to gracefully recover from errors.
pub async fn sync(&mut self) -> bool {
let mut result = SyncResult::new();
if let Err(err) = self.run_sync(&mut result).await {
result.error(&format!("Sync terminated because of an error: {}", err));
}
result.is_success()
/// In case errors happened, the sync might have been partially executed but your data will never be correupted (either locally nor in the server).
/// Simply run this function again, it will re-start a sync, picking up where it failed.
pub async fn sync_with_feedback(&mut self, feedback_sender: FeedbackSender) -> bool {
let mut progress = SyncProgress::new_with_feedback_channel(feedback_sender);
self.run_sync(&mut progress).await
}
async fn run_sync(&mut self, result: &mut SyncResult) -> Result<(), Box<dyn Error>> {
result.info("Starting a sync.");
/// Performs a synchronisation between `local` and `remote`, without giving any feedback.
///
/// See [`Self::sync_with_feedback`]
pub async fn sync(&mut self) -> bool {
let mut progress = SyncProgress::new();
self.run_sync(&mut progress).await
}
async fn run_sync(&mut self, progress: &mut SyncProgress) -> bool {
if let Err(err) = self.run_sync_inner(progress).await {
progress.error(&format!("Sync terminated because of an error: {}", err));
}
progress.feedback(SyncEvent::Finished{ success: progress.is_success() });
progress.is_success()
}
async fn run_sync_inner(&mut self, progress: &mut SyncProgress) -> Result<(), Box<dyn Error>> {
progress.info("Starting a sync.");
progress.feedback(SyncEvent::Started);
let mut handled_calendars = HashSet::new();
@@ -114,14 +105,14 @@ where
for (cal_id, cal_remote) in cals_remote {
let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_id, cal_remote.clone()).await {
Err(err) => {
result.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_id, err));
progress.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_id, err));
continue;
},
Ok(arc) => arc,
};
if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, result).await {
result.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err));
if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, progress).await {
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err));
continue;
}
handled_calendars.insert(cal_id);
@@ -136,18 +127,20 @@ where
let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_id, cal_local.clone()).await {
Err(err) => {
result.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_id, err));
progress.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_id, err));
continue;
},
Ok(arc) => arc,
};
if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, result).await {
result.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err));
if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, progress).await {
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err));
continue;
}
}
progress.info("Sync ended");
Ok(())
}
@@ -160,12 +153,19 @@ where
}
async fn sync_calendar_pair(cal_local: Arc<Mutex<T>>, cal_remote: Arc<Mutex<U>>, result: &mut SyncResult) -> Result<(), Box<dyn Error>> {
async fn sync_calendar_pair(cal_local: Arc<Mutex<T>>, cal_remote: Arc<Mutex<U>>, progress: &mut SyncProgress) -> Result<(), Box<dyn Error>> {
let mut cal_remote = cal_remote.lock().unwrap();
let mut cal_local = cal_local.lock().unwrap();
let cal_name = cal_local.name().to_string();
progress.info(&format!("Syncing calendar {}", cal_name));
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(),
details: "started".to_string()
});
// Step 1 - find the differences
result.debug("Finding the differences to sync...");
progress.debug("Finding the differences to sync...");
let mut local_del = HashSet::new();
let mut remote_del = HashSet::new();
let mut local_changes = HashSet::new();
@@ -174,51 +174,56 @@ where
let mut remote_additions = HashSet::new();
let remote_items = cal_remote.get_item_version_tags().await?;
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(),
details: format!("{} remote items", remote_items.len()),
});
let mut local_items_to_handle = cal_local.get_item_ids().await?;
for (id, remote_tag) in remote_items {
result.trace(&format!("***** Considering remote item {}...", id));
match cal_local.get_item_by_id_ref(&id).await {
progress.trace(&format!("***** Considering remote item {}...", id));
match cal_local.get_item_by_id(&id).await {
None => {
// This was created on the remote
result.debug(&format!("* {} is a remote addition", id));
progress.debug(&format!("* {} is a remote addition", id));
remote_additions.insert(id);
},
Some(local_item) => {
if local_items_to_handle.remove(&id) == false {
result.error(&format!("Inconsistent state: missing task {} from the local tasks", id));
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", id));
}
match local_item.sync_status() {
SyncStatus::NotSynced => {
result.error(&format!("ID reuse between remote and local sources ({}). Ignoring this item in the sync", id));
progress.error(&format!("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
result.debug(&format!("* {} is a remote change", id));
progress.debug(&format!("* {} is a remote change", id));
remote_changes.insert(id);
}
},
SyncStatus::LocallyModified(local_tag) => {
if &remote_tag == local_tag {
// This has been changed locally
result.debug(&format!("* {} is a local change", id));
progress.debug(&format!("* {} is a local change", id));
local_changes.insert(id);
} else {
result.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", id));
result.debug(&format!("* {} is considered a remote change", id));
progress.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", id));
progress.debug(&format!("* {} is considered a remote change", id));
remote_changes.insert(id);
}
},
SyncStatus::LocallyDeleted(local_tag) => {
if &remote_tag == local_tag {
// This has been locally deleted
result.debug(&format!("* {} is a local deletion", id));
progress.debug(&format!("* {} is a local deletion", id));
local_del.insert(id);
} else {
result.info(&format!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", id));
result.debug(&format!("* {} is a considered a remote change", id));
progress.info(&format!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", id));
progress.debug(&format!("* {} is a considered a remote change", id));
remote_changes.insert(id);
}
},
@@ -229,10 +234,10 @@ where
// Also iterate on the local tasks that are not on the remote
for id in local_items_to_handle {
result.trace(&format!("##### Considering local item {}...", id));
let local_item = match cal_local.get_item_by_id_ref(&id).await {
progress.trace(&format!("##### Considering local item {}...", id));
let local_item = match cal_local.get_item_by_id(&id).await {
None => {
result.error(&format!("Inconsistent state: missing task {} from the local tasks", id));
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", id));
continue;
},
Some(item) => item,
@@ -241,21 +246,21 @@ where
match local_item.sync_status() {
SyncStatus::Synced(_) => {
// This item has been removed from the remote
result.debug(&format!("# {} is a deletion from the server", id));
progress.debug(&format!("# {} is a deletion from the server", id));
remote_del.insert(id);
},
SyncStatus::NotSynced => {
// This item has just been locally created
result.debug(&format!("# {} has been locally created", id));
progress.debug(&format!("# {} has been locally created", id));
local_additions.insert(id);
},
SyncStatus::LocallyDeleted(_) => {
// This item has been deleted from both sources
result.debug(&format!("# {} has been deleted from both sources", id));
progress.debug(&format!("# {} has been deleted from both sources", id));
remote_del.insert(id);
},
SyncStatus::LocallyModified(_) => {
result.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", id));
progress.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", id));
remote_del.insert(id);
},
}
@@ -263,44 +268,56 @@ where
// Step 2 - commit changes
result.trace("Committing changes...");
progress.trace("Committing changes...");
for id_del in local_del {
result.debug(&format!("> Pushing local deletion {} to the server", id_del));
progress.debug(&format!("> Pushing local deletion {} to the server", id_del));
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_del).await,
});
match cal_remote.delete_item(&id_del).await {
Err(err) => {
result.warn(&format!("Unable to delete remote item {}: {}", id_del, err));
progress.warn(&format!("Unable to delete remote item {}: {}", id_del, err));
},
Ok(()) => {
// Change the local copy from "marked to deletion" to "actually deleted"
if let Err(err) = cal_local.immediately_delete_item(&id_del).await {
result.error(&format!("Unable to permanently delete local item {}: {}", id_del, err));
progress.error(&format!("Unable to permanently delete local item {}: {}", id_del, err));
}
},
}
}
for id_del in remote_del {
result.debug(&format!("> Applying remote deletion {} locally", id_del));
progress.debug(&format!("> Applying remote deletion {} locally", id_del));
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_del).await,
});
if let Err(err) = cal_local.immediately_delete_item(&id_del).await {
result.warn(&format!("Unable to delete local item {}: {}", id_del, err));
progress.warn(&format!("Unable to delete local item {}: {}", id_del, err));
}
}
for id_add in remote_additions {
result.debug(&format!("> Applying remote addition {} locally", id_add));
progress.debug(&format!("> Applying remote addition {} locally", id_add));
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_add).await,
});
match cal_remote.get_item_by_id(&id_add).await {
Err(err) => {
result.warn(&format!("Unable to get remote item {}: {}. Skipping it.", id_add, err));
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it.", id_add, err));
continue;
},
Ok(item) => match item {
None => {
result.error(&format!("Inconsistency: new item {} has vanished from the remote end", id_add));
progress.error(&format!("Inconsistency: new item {} has vanished from the remote end", id_add));
continue;
},
Some(new_item) => {
if let Err(err) = cal_local.add_item(new_item.clone()).await {
result.error(&format!("Not able to add item {} to local calendar: {}", id_add, err));
progress.error(&format!("Not able to add item {} to local calendar: {}", id_add, err));
}
},
},
@@ -308,20 +325,24 @@ where
}
for id_change in remote_changes {
result.debug(&format!("> Applying remote change {} locally", id_change));
progress.debug(&format!("> Applying remote change {} locally", id_change));
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_change).await,
});
match cal_remote.get_item_by_id(&id_change).await {
Err(err) => {
result.warn(&format!("Unable to get remote item {}: {}. Skipping it", id_change, err));
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it", id_change, err));
continue;
},
Ok(item) => match item {
None => {
result.error(&format!("Inconsistency: modified item {} has vanished from the remote end", id_change));
progress.error(&format!("Inconsistency: modified item {} has vanished from the remote end", id_change));
continue;
},
Some(item) => {
if let Err(err) = cal_local.update_item(item.clone()).await {
result.error(&format!("Unable to update item {} in local calendar: {}", id_change, err));
progress.error(&format!("Unable to update item {} in local calendar: {}", id_change, err));
}
},
}
@@ -330,15 +351,19 @@ where
for id_add in local_additions {
result.debug(&format!("> Pushing local addition {} to the server", id_add));
progress.debug(&format!("> Pushing local addition {} to the server", id_add));
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_add).await,
});
match cal_local.get_item_by_id_mut(&id_add).await {
None => {
result.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", id_add));
progress.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", id_add));
continue;
},
Some(item) => {
match cal_remote.add_item(item.clone()).await {
Err(err) => result.error(&format!("Unable to add item {} to remote calendar: {}", id_add, err)),
Err(err) => progress.error(&format!("Unable to add item {} to remote calendar: {}", id_add, err)),
Ok(new_ss) => {
// Update local sync status
item.set_sync_status(new_ss);
@@ -349,15 +374,19 @@ where
}
for id_change in local_changes {
result.debug(&format!("> Pushing local change {} to the server", id_change));
progress.debug(&format!("> Pushing local change {} to the server", id_change));
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(),
details: Self::item_name(&cal_local, &id_change).await,
});
match cal_local.get_item_by_id_mut(&id_change).await {
None => {
result.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", id_change));
progress.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", id_change));
continue;
},
Some(item) => {
match cal_remote.update_item(item.clone()).await {
Err(err) => result.error(&format!("Unable to update item {} in remote calendar: {}", id_change, err)),
Err(err) => progress.error(&format!("Unable to update item {} in remote calendar: {}", id_change, err)),
Ok(new_ss) => {
// Update local sync status
item.set_sync_status(new_ss);
@@ -369,6 +398,12 @@ where
Ok(())
}
async fn item_name(cal: &T, id: &ItemId) -> String {
cal.get_item_by_id(id).await.map(|item| item.name()).unwrap_or_default().to_string()
}
}
@@ -389,10 +424,12 @@ where
let src = needle.lock().unwrap();
let name = src.name().to_string();
let supported_comps = src.supported_components();
let color = src.color();
if let Err(err) = haystack.create_calendar(
cal_id.clone(),
name,
supported_comps,
color.cloned(),
).await{
return Err(err);
}
+101
View File
@@ -0,0 +1,101 @@
//! Utilities to track the progression of a sync
use std::fmt::{Display, Error, Formatter};
/// An event that happens during a sync
#[derive(Clone, Debug)]
pub enum SyncEvent {
/// Sync has not started
NotStarted,
/// Sync has just started but no calendar is handled yet
Started,
/// Sync is in progress.
InProgress{ calendar: String, details: String},
/// Sync is finished
Finished{ success: bool },
}
impl Display for SyncEvent {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
match self {
SyncEvent::NotStarted => write!(f, "Not started"),
SyncEvent::Started => write!(f, "Sync has started..."),
SyncEvent::InProgress{calendar, details} => write!(f, "[{}] {}...", calendar, details),
SyncEvent::Finished{success} => match success {
true => write!(f, "Sync successfully finished"),
false => write!(f, "Sync finished with errors"),
}
}
}
}
impl Default for SyncEvent {
fn default() -> Self {
Self::NotStarted
}
}
/// See [`feedback_channel`]
pub type FeedbackSender = tokio::sync::watch::Sender<SyncEvent>;
/// See [`feedback_channel`]
pub type FeedbackReceiver = tokio::sync::watch::Receiver<SyncEvent>;
/// Create a feeback channel, that can be used to retrieve the current progress of a sync operation
pub fn feedback_channel() -> (FeedbackSender, FeedbackReceiver) {
tokio::sync::watch::channel(SyncEvent::default())
}
/// A structure that tracks the progression and the errors that happen during a sync
pub struct SyncProgress {
n_errors: u32,
feedback_channel: Option<FeedbackSender>
}
impl SyncProgress {
pub fn new() -> Self {
Self { n_errors: 0, feedback_channel: None }
}
pub fn new_with_feedback_channel(channel: FeedbackSender) -> Self {
Self { n_errors: 0, feedback_channel: Some(channel) }
}
pub fn is_success(&self) -> bool {
self.n_errors == 0
}
/// Log an error
pub fn error(&mut self, text: &str) {
log::error!("{}", text);
self.n_errors += 1;
}
/// Log a warning
pub fn warn(&mut self, text: &str) {
log::warn!("{}", text);
self.n_errors += 1;
}
/// Log an info
pub fn info(&mut self, text: &str) {
log::info!("{}", text);
}
/// Log a debug message
pub fn debug(&mut self, text: &str) {
log::debug!("{}", text);
}
/// Log a trace message
pub fn trace(&mut self, text: &str) {
log::trace!("{}", text);
}
/// Send an event as a feedback to the listener (if any).
pub fn feedback(&mut self, event: SyncEvent) {
self.feedback_channel
.as_ref()
.map(|sender| {
sender.send(event)
});
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
use url::Url;
/// Just a wrapper around a URL and credentials
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Resource {
url: Url,
username: String,
-12
View File
@@ -1,12 +0,0 @@
// TODO: change these values with yours
pub const URL: &str = "https://my.server.com/remote.php/dav/files/john";
pub const USERNAME: &str = "username";
pub const PASSWORD: &str = "secret_password";
pub const EXAMPLE_TASK_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/6121A0BE-C2E0-4F16-A3FA-658E54E7062A/74439558-CDFF-426C-92CD-ECDDACE971B0.ics";
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/";
pub const EXAMPLE_CREATED_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_that_we_have_created/";
pub const ORG_NAME: &str = "My organisation";
pub const PRODUCT_NAME: &str = "My CalDAV client";
+19 -3
View File
@@ -3,6 +3,7 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};
use ical::property::Property;
use crate::item::ItemId;
use crate::item::SyncStatus;
@@ -51,6 +52,13 @@ pub struct Task {
/// The display name of the task
name: String,
/// The PRODID, as defined in iCal files
ical_prod_id: String,
/// Extra parameters that have not been parsed from the iCal file (because they're not supported (yet) by this crate).
/// They are needed to serialize this item into an equivalent iCal file
extra_parameters: Vec<Property>,
}
@@ -66,13 +74,17 @@ impl Task {
let new_completion_status = if completed {
CompletionStatus::Completed(Some(Utc::now()))
} else { CompletionStatus::Uncompleted };
Self::new_with_parameters(name, new_uid, new_item_id, new_completion_status, new_sync_status, new_creation_date, new_last_modified)
let ical_prod_id = crate::ical::default_prod_id();
let extra_parameters = Vec::new();
Self::new_with_parameters(name, new_uid, new_item_id, new_completion_status, new_sync_status, new_creation_date, new_last_modified, ical_prod_id, extra_parameters)
}
/// Create a new Task instance, that may be synced already
/// Create a new Task instance, that may be synced on the server already
pub fn new_with_parameters(name: String, uid: String, id: ItemId,
completion_status: CompletionStatus,
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>) -> Self
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>,
ical_prod_id: String, extra_parameters: Vec<Property>,
) -> Self
{
Self {
id,
@@ -82,6 +94,8 @@ impl Task {
sync_status,
creation_date,
last_modified,
ical_prod_id,
extra_parameters,
}
}
@@ -89,10 +103,12 @@ impl Task {
pub fn uid(&self) -> &str { &self.uid }
pub fn name(&self) -> &str { &self.name }
pub fn completed(&self) -> bool { self.completion_status.is_completed() }
pub fn ical_prod_id(&self) -> &str { &self.ical_prod_id }
pub fn sync_status(&self) -> &SyncStatus { &self.sync_status }
pub fn last_modified(&self) -> &DateTime<Utc> { &self.last_modified }
pub fn creation_date(&self) -> Option<&DateTime<Utc>> { self.creation_date.as_ref() }
pub fn completion_status(&self) -> &CompletionStatus { &self.completion_status }
pub fn extra_parameters(&self) -> &[Property] { &self.extra_parameters }
#[cfg(any(test, feature = "integration_tests"))]
pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
+10 -4
View File
@@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use csscolorparser::Color;
use crate::item::SyncStatus;
use crate::item::Item;
@@ -21,7 +22,7 @@ pub trait CalDavSource<T: BaseCalendar> {
/// Returns the calendar matching the ID
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<T>>>;
/// Create a calendar if it did not exist, and return it
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents)
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>)
-> Result<Arc<Mutex<T>>, Box<dyn Error>>;
// Removing a calendar is not supported yet
@@ -39,6 +40,9 @@ pub trait BaseCalendar {
/// Returns the supported kinds of components for this calendar
fn supported_components(&self) -> crate::calendar::SupportedComponents;
/// Returns the user-defined color of this calendar
fn color(&self) -> Option<&Color>;
/// Add an item into this calendar, and return its new sync status.
/// For local calendars, the sync status is not modified.
/// For remote calendars, the sync status is updated by the server
@@ -64,7 +68,7 @@ pub trait BaseCalendar {
#[async_trait]
pub trait DavCalendar : BaseCalendar {
/// Create a new calendar
fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self;
fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self;
/// Get the IDs and the version tags of every item in this calendar
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>>;
@@ -94,16 +98,18 @@ pub trait DavCalendar : BaseCalendar {
#[async_trait]
pub trait CompleteCalendar : BaseCalendar {
/// Create a new calendar
fn new(name: String, id: CalendarId, supported_components: SupportedComponents) -> Self;
fn new(name: String, id: CalendarId, supported_components: SupportedComponents, color: Option<Color>) -> Self;
/// Get the IDs of all current items in this calendar
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>>;
/// Returns all items that this calendar contains
///
/// See [`crate::utils::comparison`] for helper functions that help sorting the results
async fn get_items(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>>;
/// Returns a particular item
async fn get_item_by_id_ref<'a>(&'a self, id: &ItemId) -> Option<&'a Item>;
async fn get_item_by_id<'a>(&'a self, id: &ItemId) -> Option<&'a Item>;
/// Returns a particular item
async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item>;
View File
+20
View File
@@ -0,0 +1,20 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Todo Corp LTD//Awesome Product ®//EN
BEGIN:VTODO
UID:20f57387-e116-4702-b463-d352aeaf80d0
X_FAVOURITE_PAINT_FINISH:matte
DTSTAMP:20211103T214742
CREATED:20211103T212345
LAST-MODIFIED:20211103T214742
SUMMARY:This is a task with ÜTF-8 characters
STATUS:NEEDS-ACTION
DUE:20211103T220000
PRIORITY:6
PERCENT-COMPLETE:48
IMAGE;DISPLAY=BADGE;FMTTYPE=image/png;VALUE=URI:http://example.com/images/p
arty.png
CONFERENCE;FEATURE=PHONE;LABEL=Attendee dial-in;VALUE=URI:tel:+1-888-555-04
56,,,555123
END:VTODO
END:VCALENDAR
-117
View File
@@ -1,117 +0,0 @@
//! Some tests of a CalDAV client.
//! Most of them are not really integration tests, but just development tests that should be cleaned up one day.
use reqwest::Method;
use reqwest::header::CONTENT_TYPE;
use minidom::Element;
use url::Url;
use kitchen_fridge::{calendar::SupportedComponents, client::Client};
use kitchen_fridge::traits::CalDavSource;
use kitchen_fridge::settings::URL;
use kitchen_fridge::settings::USERNAME;
use kitchen_fridge::settings::PASSWORD;
use kitchen_fridge::settings::EXAMPLE_TASK_URL;
use kitchen_fridge::settings::EXAMPLE_CREATED_CALENDAR_URL;
static EXAMPLE_TASKS_BODY_LAST_MODIFIED: &str = r#"
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
<C:calendar-data />
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:prop-filter name="LAST-MODIFIED">
<C:time-range start="20210228T002308Z"
end="20260105T000000Z"/>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
#[tokio::test]
#[ignore]
async fn show_calendars() {
let _ = env_logger::builder().is_test(true).try_init();
let client = Client::new(URL, USERNAME, PASSWORD).unwrap();
let calendars = client.get_calendars().await.unwrap();
println!("Calendars:");
kitchen_fridge::utils::print_dav_calendar_list(&calendars).await;
}
#[tokio::test]
async fn create_cal() {
let _ = env_logger::builder().is_test(true).try_init();
let mut client = Client::new(URL, USERNAME, PASSWORD).unwrap();
let id: Url = kitchen_fridge::settings::EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
let name = "prout".into();
let supported_components = SupportedComponents::TODO;
client.create_calendar(id, name, supported_components).await.unwrap();
}
#[tokio::test]
#[ignore]
async fn profind() {
let _ = env_logger::builder().is_test(true).try_init();
let url: Url = EXAMPLE_TASK_URL.parse().unwrap();
let method = Method::from_bytes(b"PROPFIND")
.expect("cannot create PROPFIND method.");
let res = reqwest::Client::new()
.request(method, url.as_str())
.header("Depth", 0)
.header(CONTENT_TYPE, "application/xml")
.basic_auth(USERNAME, Some(PASSWORD))
//.body(body)
.send()
.await
.unwrap();
println!("{:?}", res.text().await);
}
#[tokio::test]
#[ignore]
async fn last_modified() {
let _ = env_logger::builder().is_test(true).try_init();
let url: Url = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
let method = Method::from_bytes(b"REPORT")
.expect("cannot create REPORT method.");
let res = reqwest::Client::new()
.request(method, url.as_str())
.header("Depth", 1)
.header(CONTENT_TYPE, "application/xml")
.basic_auth(USERNAME, Some(PASSWORD))
.body(EXAMPLE_TASKS_BODY_LAST_MODIFIED)
.send()
.await
.unwrap();
let el: Element = res.text().await.unwrap().parse().unwrap();
kitchen_fridge::utils::print_xml(&el);
}
//
//
// TODO: test w/ wrong creds
// TODO: test withou connection
//
+10 -6
View File
@@ -22,12 +22,12 @@ use kitchen_fridge::traits::CompleteCalendar;
use kitchen_fridge::traits::DavCalendar;
use kitchen_fridge::cache::Cache;
use kitchen_fridge::Item;
use kitchen_fridge::ItemId;
use kitchen_fridge::SyncStatus;
use kitchen_fridge::item::ItemId;
use kitchen_fridge::item::SyncStatus;
use kitchen_fridge::Task;
use kitchen_fridge::task::CompletionStatus;
use kitchen_fridge::calendar::cached_calendar::CachedCalendar;
use kitchen_fridge::Provider;
use kitchen_fridge::provider::Provider;
use kitchen_fridge::mock_behaviour::MockBehaviour;
pub enum LocatedState {
@@ -380,7 +380,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
String::from("Task Q, created on the server"),
id_q.to_string(), id_q,
CompletionStatus::Uncompleted,
SyncStatus::random_synced(), Some(Utc::now()), Utc::now() )
SyncStatus::random_synced(), Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() )
))],
after_sync: LocatedState::BothSynced( ItemState{
calendar: third_cal.clone(),
@@ -400,7 +400,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
String::from("Task R, created locally"),
id_r.to_string(), id_r,
CompletionStatus::Uncompleted,
SyncStatus::NotSynced, Some(Utc::now()), Utc::now() )
SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() )
))],
remote_changes_to_apply: Vec::new(),
after_sync: LocatedState::BothSynced( ItemState{
@@ -578,7 +578,8 @@ pub fn scenarii_transient_task() -> Vec<ItemScenario> {
String::from("A transient task that will be deleted before the sync"),
id_transient.to_string(), id_transient,
CompletionStatus::Uncompleted,
SyncStatus::NotSynced, Some(Utc::now()), Utc::now() )
SyncStatus::NotSynced, Some(Utc::now()), Utc::now(),
"prod_id".to_string(), Vec::new() )
)),
ChangeToApply::Rename(String::from("A new name")),
@@ -642,6 +643,7 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc<M
sync_status,
Some(now),
now,
"prod_id".to_string(), Vec::new(),
));
match required_state {
@@ -692,11 +694,13 @@ async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId)
None => {
let new_name = format!("Test calendar for ID {}", id);
let supported_components = SupportedComponents::TODO;
let color = csscolorparser::parse("#ff8000"); // TODO: we should rather have specific colors, depending on the calendars
source.create_calendar(
id.clone(),
new_name.to_string(),
supported_components,
None,
).await
}
}
+1 -1
View File
@@ -340,7 +340,7 @@ async fn test_errors_in_regular_sync12() {
#[cfg(feature = "integration_tests")]
use kitchen_fridge::{traits::CalDavSource,
Provider,
provider::Provider,
cache::Cache,
calendar::cached_calendar::CachedCalendar,
};