Compare commits
37
Commits
0.1
...
github_actions
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96eec57951 | ||
|
|
0f55850b6d | ||
|
|
413b2b285e | ||
|
|
582c187e04 | ||
|
|
9dcf7e0499 | ||
|
|
b0db19b586 | ||
|
|
75fe00983d | ||
|
|
39867a9f15 | ||
|
|
f7acadc3e2 | ||
|
|
49bdb3d199 | ||
|
|
d6e93d846e | ||
|
|
40503f46d5 | ||
|
|
7d09d72175 | ||
|
|
d8c89ec727 | ||
|
|
9bd45d5a07 | ||
|
|
b404fc68e8 | ||
|
|
7fb98a471b | ||
|
|
04c8b3a2ee | ||
|
|
f7ec5d29be | ||
|
|
38fd04c3a4 | ||
|
|
2e4be4a754 | ||
|
|
4c54515b79 | ||
|
|
e563cb3b2f | ||
|
|
92b3089e5e | ||
|
|
db662d2f4a | ||
|
|
9234d57cdc | ||
|
|
b6e8ce2421 | ||
|
|
6d984cdc7f | ||
|
|
78bb78f61f | ||
|
|
17716575d8 | ||
|
|
2b339a7aff | ||
|
|
fbd076875a | ||
|
|
72e05dc6c9 | ||
|
|
e6dda89d01 | ||
|
|
bf68d81ec8 | ||
|
|
58cfb42f64 | ||
|
|
ab8f6a9413 |
@@ -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
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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>> {
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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() );
|
||||
@@ -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!()
|
||||
}
|
||||
|
||||
+50
-28
@@ -5,51 +5,57 @@ 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() {
|
||||
CompletionStatus::Uncompleted => {
|
||||
todo.push(Status::needs_action());
|
||||
},
|
||||
CompletionStatus::Completed(completion_date) => {
|
||||
todo.push(PercentComplete::new("100"));
|
||||
completion_date.as_ref().map(|dt| todo.push(
|
||||
Completed::new(format_date_time(dt))
|
||||
));
|
||||
todo.push(Status::completed());
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
unimplemented!()
|
||||
match task.completion_status() {
|
||||
CompletionStatus::Uncompleted => {
|
||||
todo.push(Status::needs_action());
|
||||
},
|
||||
CompletionStatus::Completed(completion_date) => {
|
||||
todo.push(PercentComplete::new("100"));
|
||||
completion_date.as_ref().map(|dt| todo.push(
|
||||
Completed::new(format_date_time(dt))
|
||||
));
|
||||
todo.push(Status::completed());
|
||||
}
|
||||
}
|
||||
|
||||
let mut calendar = ICalendar::new("2.0", ical_product_id());
|
||||
// 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", 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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-33
@@ -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,38 +40,42 @@ 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" {
|
||||
// Possible values:
|
||||
// "NEEDS-ACTION" ;Indicates to-do needs action.
|
||||
// "COMPLETED" ;Indicates to-do completed.
|
||||
// "IN-PROCESS" ;Indicates to-do in process of.
|
||||
// "CANCELLED" ;Indicates to-do was cancelled.
|
||||
if prop.value.as_ref().map(|s| s.as_str()) == Some("COMPLETED") {
|
||||
completed = true;
|
||||
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.
|
||||
// "IN-PROCESS" ;Indicates to-do in process of.
|
||||
// "CANCELLED" ;Indicates to-do was cancelled.
|
||||
if prop.value.as_ref().map(|s| s.as_str()) == Some("COMPLETED") {
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// 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 == "UID" {
|
||||
uid = prop.value.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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user