Compare commits
No commits in common. "master" and "0.1" have entirely different histories.
|
|
@ -1,24 +0,0 @@
|
|||
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
|
||||
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
|
|
@ -1,15 +1,8 @@
|
|||
[package]
|
||||
name = "kitchen-fridge"
|
||||
version = "0.4.0"
|
||||
version = "0.1.0"
|
||||
authors = ["daladim"]
|
||||
edition = "2018"
|
||||
description = "A CalDAV (ical file management over WebDAV) library"
|
||||
repository = "https://github.com/daladim/kitchen-fridge"
|
||||
documentation = "https://docs.rs/kitchen-fridge"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
keywords = ["CalDAV", "client", "WebDAV", "todo", "iCloud"]
|
||||
categories = ["network-programming", "web-programming::http-client"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
@ -18,7 +11,7 @@ integration_tests = ["local_calendar_mocks_remote_calendars"]
|
|||
local_calendar_mocks_remote_calendars = []
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.9"
|
||||
env_logger = "0.8"
|
||||
log = "0.4"
|
||||
tokio = { version = "1.2", features = ["macros", "rt", "rt-multi-thread"]}
|
||||
reqwest = "0.11"
|
||||
|
|
@ -30,9 +23,6 @@ serde_json = "1.0"
|
|||
async-trait = "0.1"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
sanitize-filename = "0.3"
|
||||
ical-daladim = { version = "0.8", features = ["serde-derive"] }
|
||||
ical = "0.7"
|
||||
ics = "0.5"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
csscolorparser = { version = "0.5", features = ["serde"] }
|
||||
once_cell = "1.8"
|
||||
itertools = "0.10"
|
||||
|
|
|
|||
7
LICENSE
7
LICENSE
|
|
@ -1,7 +0,0 @@
|
|||
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.
|
||||
15
README.md
15
README.md
|
|
@ -1,15 +0,0 @@
|
|||
# kitchen-fridge
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/kitchen-fridge.svg" alt="kitchen-fridge logo" width="250"/>
|
||||
</p>
|
||||
|
||||
kitchen-fridge is a CalDAV (iCal file transfer over WebDAV) Rust client library.
|
||||
|
||||
CalDAV is described as "Calendaring Extensions to WebDAV" in [RFC 4791](https://datatracker.ietf.org/doc/html/rfc4791) and [RFC 7986](https://datatracker.ietf.org/doc/html/rfc7986) and the underlying iCal format is described at least in [RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545). \
|
||||
This library has been intensivley tested with Nextcloud servers. It should support Owncloud and iCloud as well, since they use the very same CalDAV protocol.
|
||||
|
||||
Its [documentation](https://docs.rs/kitchen-fridge/) is available on docs.rs.
|
||||
|
||||
|
||||
CalDAV is described as "Calendaring Extensions to WebDAV" in [RFC 4791](https://datatracker.ietf.org/doc/html/rfc4791) and [RFC 7986](https://datatracker.ietf.org/doc/html/rfc7986) and the underlying iCal format is described at least in [RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545).
|
||||
|
|
@ -1,22 +1,24 @@
|
|||
//! This is an example of how kitchen-fridge can be used
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::{Utc};
|
||||
use url::Url;
|
||||
|
||||
use kitchen_fridge::traits::CalDavSource;
|
||||
use kitchen_fridge::calendar::SupportedComponents;
|
||||
use kitchen_fridge::{client::Client, traits::CalDavSource};
|
||||
use kitchen_fridge::calendar::{CalendarId, SupportedComponents};
|
||||
use kitchen_fridge::Item;
|
||||
use kitchen_fridge::Task;
|
||||
use kitchen_fridge::task::CompletionStatus;
|
||||
use kitchen_fridge::item::ItemId;
|
||||
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;
|
||||
|
||||
mod shared;
|
||||
use shared::initial_sync;
|
||||
use shared::{URL, USERNAME, EXAMPLE_EXISTING_CALENDAR_URL, EXAMPLE_CREATED_CALENDAR_URL};
|
||||
|
||||
const CACHE_FOLDER: &str = "test_cache/provider_sync";
|
||||
|
||||
|
||||
|
|
@ -24,48 +26,67 @@ const CACHE_FOLDER: &str = "test_cache/provider_sync";
|
|||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
println!("This example show how to sync a remote server with a local cache, using a Provider.");
|
||||
println!("Make sure you have edited the constants in the 'shared.rs' file to include correct URLs and credentials.");
|
||||
println!("This examples show how to sync a remote server with a local cache, using a Provider.");
|
||||
println!("Make sure you have edited your settings.rs to include correct URLs and credentials.");
|
||||
println!("You can also set the RUST_LOG environment variable to display more info about the sync.");
|
||||
println!("");
|
||||
println!("This will use the following settings:");
|
||||
println!(" * URL = {}", URL);
|
||||
println!(" * USERNAME = {}", USERNAME);
|
||||
println!(" * EXAMPLE_EXISTING_CALENDAR_URL = {}", EXAMPLE_EXISTING_CALENDAR_URL);
|
||||
println!(" * EXAMPLE_CREATED_CALENDAR_URL = {}", EXAMPLE_CREATED_CALENDAR_URL);
|
||||
pause();
|
||||
|
||||
let mut provider = initial_sync(CACHE_FOLDER).await;
|
||||
let cache_path = Path::new(CACHE_FOLDER);
|
||||
|
||||
let client = Client::new(URL, USERNAME, PASSWORD).unwrap();
|
||||
let cache = match Cache::from_folder(&cache_path) {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => {
|
||||
log::warn!("Invalid cache file: {}. Using a default cache", err);
|
||||
Cache::new(&cache_path)
|
||||
}
|
||||
};
|
||||
let mut provider = CalDavProvider::new(client, cache);
|
||||
|
||||
let cals = provider.local().get_calendars().await.unwrap();
|
||||
println!("---- Local items, before sync -----");
|
||||
kitchen_fridge::utils::print_calendar_list(&cals).await;
|
||||
|
||||
println!("Starting a sync...");
|
||||
if provider.sync().await == false {
|
||||
log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync.");
|
||||
}
|
||||
provider.local().save_to_folder().unwrap();
|
||||
|
||||
println!("---- Local items, after sync -----");
|
||||
let cals = provider.local().get_calendars().await.unwrap();
|
||||
kitchen_fridge::utils::print_calendar_list(&cals).await;
|
||||
|
||||
add_items_and_sync_again(&mut provider).await;
|
||||
}
|
||||
|
||||
async fn add_items_and_sync_again(provider: &mut CalDavProvider) {
|
||||
async fn add_items_and_sync_again(provider: &mut CalDavProvider)
|
||||
{
|
||||
println!("\nNow, we'll add a calendar and a few tasks and run the sync again.");
|
||||
pause();
|
||||
|
||||
// Create a new calendar...
|
||||
let new_calendar_url: Url = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
|
||||
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_url.clone(), new_calendar_name.clone(), SupportedComponents::TODO, Some("#ff8000".parse().unwrap()))
|
||||
.create_calendar(new_calendar_id.clone(), new_calendar_name.clone(), SupportedComponents::TODO)
|
||||
.await {
|
||||
println!("Unable to add calendar, maybe it exists already. We're not adding it after all.");
|
||||
}
|
||||
|
||||
// ...and add a task in it
|
||||
let new_name = "This is a new task in a new calendar";
|
||||
let new_task = Task::new(String::from(new_name), true, &new_calendar_url);
|
||||
provider.local().get_calendar(&new_calendar_url).await.unwrap()
|
||||
let new_task = Task::new(String::from(new_name), true, &new_calendar_id);
|
||||
provider.local().get_calendar(&new_calendar_id).await.unwrap()
|
||||
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
|
||||
|
||||
|
||||
// Also create a task in a previously existing calendar
|
||||
let changed_calendar_url: Url = EXAMPLE_EXISTING_CALENDAR_URL.parse().unwrap();
|
||||
let changed_calendar_id: CalendarId = EXAMPLE_EXISTING_CALENDAR_URL.parse().unwrap();
|
||||
let new_task_name = "This is a new task we're adding as an example, with ÜTF-8 characters";
|
||||
let new_task = Task::new(String::from(new_task_name), false, &changed_calendar_url);
|
||||
let new_url = new_task.url().clone();
|
||||
provider.local().get_calendar(&changed_calendar_url).await.unwrap()
|
||||
let new_task = Task::new(String::from(new_task_name), false, &changed_calendar_id);
|
||||
let new_id = new_task.id().clone();
|
||||
provider.local().get_calendar(&changed_calendar_id).await.unwrap()
|
||||
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
|
||||
|
||||
|
||||
|
|
@ -76,20 +97,20 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider) {
|
|||
}
|
||||
provider.local().save_to_folder().unwrap();
|
||||
|
||||
complete_item_and_sync_again(provider, &changed_calendar_url, &new_url).await;
|
||||
complete_item_and_sync_again(provider, &changed_calendar_id, &new_id).await;
|
||||
}
|
||||
|
||||
async fn complete_item_and_sync_again(
|
||||
provider: &mut CalDavProvider,
|
||||
changed_calendar_url: &Url,
|
||||
url_to_complete: &Url)
|
||||
changed_calendar_id: &CalendarId,
|
||||
id_to_complete: &ItemId)
|
||||
{
|
||||
println!("\nNow, we'll mark this last task as completed, and run the sync again.");
|
||||
pause();
|
||||
|
||||
let completion_status = CompletionStatus::Completed(Some(Utc::now()));
|
||||
provider.local().get_calendar(changed_calendar_url).await.unwrap()
|
||||
.lock().unwrap().get_item_by_url_mut(url_to_complete).await.unwrap()
|
||||
provider.local().get_calendar(changed_calendar_id).await.unwrap()
|
||||
.lock().unwrap().get_item_by_id_mut(id_to_complete).await.unwrap()
|
||||
.unwrap_task_mut()
|
||||
.set_completion_status(completion_status);
|
||||
|
||||
|
|
@ -100,19 +121,19 @@ async fn complete_item_and_sync_again(
|
|||
}
|
||||
provider.local().save_to_folder().unwrap();
|
||||
|
||||
remove_items_and_sync_again(provider, changed_calendar_url, url_to_complete).await;
|
||||
remove_items_and_sync_again(provider, changed_calendar_id, id_to_complete).await;
|
||||
}
|
||||
|
||||
async fn remove_items_and_sync_again(
|
||||
provider: &mut CalDavProvider,
|
||||
changed_calendar_url: &Url,
|
||||
id_to_remove: &Url)
|
||||
changed_calendar_id: &CalendarId,
|
||||
id_to_remove: &ItemId)
|
||||
{
|
||||
println!("\nNow, we'll delete this last task, and run the sync again.");
|
||||
pause();
|
||||
|
||||
// Remove the task we had created
|
||||
provider.local().get_calendar(changed_calendar_url).await.unwrap()
|
||||
provider.local().get_calendar(changed_calendar_id).await.unwrap()
|
||||
.lock().unwrap()
|
||||
.mark_for_deletion(id_to_remove).await.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use kitchen_fridge::client::Client;
|
||||
use kitchen_fridge::traits::CalDavSource;
|
||||
use kitchen_fridge::CalDavProvider;
|
||||
use kitchen_fridge::cache::Cache;
|
||||
|
||||
|
||||
// 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_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/";
|
||||
|
||||
fn main() {
|
||||
panic!("This file is not supposed to be executed");
|
||||
}
|
||||
|
||||
|
||||
/// Initializes a Provider, and run an initial sync from the server
|
||||
pub async fn initial_sync(cache_folder: &str) -> CalDavProvider {
|
||||
let cache_path = Path::new(cache_folder);
|
||||
|
||||
let client = Client::new(URL, USERNAME, PASSWORD).unwrap();
|
||||
let cache = match Cache::from_folder(&cache_path) {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => {
|
||||
log::warn!("Invalid cache file: {}. Using a default cache", err);
|
||||
Cache::new(&cache_path)
|
||||
}
|
||||
};
|
||||
let mut provider = CalDavProvider::new(client, cache);
|
||||
|
||||
|
||||
let cals = provider.local().get_calendars().await.unwrap();
|
||||
println!("---- Local items, before sync -----");
|
||||
kitchen_fridge::utils::print_calendar_list(&cals).await;
|
||||
|
||||
println!("Starting a sync...");
|
||||
println!("Depending on your RUST_LOG value, you may see more or less details about the progress.");
|
||||
// Note that we could use sync_with_feedback() to have better and formatted feedback
|
||||
if provider.sync().await == false {
|
||||
log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync.");
|
||||
}
|
||||
provider.local().save_to_folder().unwrap();
|
||||
|
||||
println!("---- Local items, after sync -----");
|
||||
let cals = provider.local().get_calendars().await.unwrap();
|
||||
kitchen_fridge::utils::print_calendar_list(&cals).await;
|
||||
|
||||
provider
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
//! This is an example of how kitchen-fridge can be used.
|
||||
//! This binary simply toggles all completion statuses of the tasks it finds.
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use kitchen_fridge::item::Item;
|
||||
use kitchen_fridge::task::CompletionStatus;
|
||||
use kitchen_fridge::CalDavProvider;
|
||||
use kitchen_fridge::utils::pause;
|
||||
|
||||
mod shared;
|
||||
use shared::initial_sync;
|
||||
use shared::{URL, USERNAME};
|
||||
|
||||
const CACHE_FOLDER: &str = "test_cache/toggle_completion";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
println!("This example show how to sync a remote server with a local cache, using a Provider.");
|
||||
println!("Make sure you have edited the constants in the 'shared.rs' file to include correct URLs and credentials.");
|
||||
println!("You can also set the RUST_LOG environment variable to display more info about the sync.");
|
||||
println!("");
|
||||
println!("This will use the following settings:");
|
||||
println!(" * URL = {}", URL);
|
||||
println!(" * USERNAME = {}", USERNAME);
|
||||
pause();
|
||||
|
||||
let mut provider = initial_sync(CACHE_FOLDER).await;
|
||||
|
||||
toggle_all_tasks_and_sync_again(&mut provider).await.unwrap();
|
||||
}
|
||||
|
||||
async fn toggle_all_tasks_and_sync_again(provider: &mut CalDavProvider) -> Result<(), Box<dyn Error>> {
|
||||
let mut n_toggled = 0;
|
||||
|
||||
for (_url, cal) in provider.local().get_calendars_sync()?.iter() {
|
||||
for (_url, item) in cal.lock().unwrap().get_items_mut_sync()?.iter_mut() {
|
||||
match item {
|
||||
Item::Task(task) => {
|
||||
match task.completed() {
|
||||
false => task.set_completion_status(CompletionStatus::Completed(Some(Utc::now()))),
|
||||
true => task.set_completion_status(CompletionStatus::Uncompleted),
|
||||
};
|
||||
n_toggled += 1;
|
||||
}
|
||||
Item::Event(_) => {
|
||||
// Not doing anything with calendar events
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{} items toggled.", n_toggled);
|
||||
println!("Syncing...");
|
||||
|
||||
provider.sync().await;
|
||||
|
||||
println!("Syncing complete.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,388 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="212.79292mm"
|
||||
height="212.79292mm"
|
||||
viewBox="0 0 212.79291 212.79293"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="kitchen-fridge.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient850-3">
|
||||
<stop
|
||||
style="stop-color:#555555;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop846" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop848" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient850-3"
|
||||
id="linearGradient852"
|
||||
x1="-32.728302"
|
||||
y1="114.98944"
|
||||
x2="159.82718"
|
||||
y2="235.52802"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.7"
|
||||
inkscape:cx="732.34868"
|
||||
inkscape:cy="461.7807"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2510"
|
||||
inkscape:window-height="1376"
|
||||
inkscape:window-x="1970"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0.1"
|
||||
fit-margin-left="42.4"
|
||||
fit-margin-right="42.4"
|
||||
fit-margin-bottom="0.1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="Fond"
|
||||
style="display:none">
|
||||
<rect
|
||||
style="fill:#102335;fill-opacity:1;stroke:#b40f00;stroke-width:0.26499999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect867"
|
||||
width="213.55655"
|
||||
height="212.42261"
|
||||
x="0"
|
||||
y="0.37030393" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Fridge"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="translate(9.3800012,-27.370898)">
|
||||
<rect
|
||||
style="fill:url(#linearGradient852);fill-opacity:1;stroke:#000000;stroke-width:2.38100004;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect815"
|
||||
width="125.61668"
|
||||
height="206.86662"
|
||||
x="34.210499"
|
||||
y="28.661392"
|
||||
ry="20.312485" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.14329159;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect858"
|
||||
width="125.61105"
|
||||
height="5.1245685"
|
||||
x="34.210815"
|
||||
y="154.2338" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Handles"
|
||||
style="display:inline"
|
||||
transform="translate(9.3800012,-27.370898)">
|
||||
<rect
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect829"
|
||||
width="1.0690781"
|
||||
height="66.282845"
|
||||
x="147.5274"
|
||||
y="80.2659"
|
||||
ry="0.53453904" />
|
||||
<rect
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.16825604;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect829-3"
|
||||
width="1.4010528"
|
||||
height="36.142323"
|
||||
x="147.30231"
|
||||
y="165.60844"
|
||||
ry="0.29147035" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Notes"
|
||||
style="display:inline"
|
||||
transform="translate(9.3800012,-27.370898)">
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:0.27699531;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 72.822459,64.133289 17.55153,-1.762358 1.28192,19.265199 -17.80831,1.703646 z"
|
||||
id="rect850-6-3"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<rect
|
||||
style="fill:#baf0ff;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect850-6"
|
||||
width="17.639786"
|
||||
height="18.441595"
|
||||
x="66.050667"
|
||||
y="71.087967"
|
||||
ry="0"
|
||||
transform="rotate(-5.7338792)" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:0.37089203;stroke:none;stroke-width:1.43235683;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 57.052763,52.364374 16.446352,-0.717802 0.746667,19.117834 -17.908228,0.01483 z"
|
||||
id="rect850-5"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<rect
|
||||
style="fill:#fffaba;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect850"
|
||||
width="17.639786"
|
||||
height="18.441595"
|
||||
x="55.859333"
|
||||
y="51.646572"
|
||||
ry="0" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1050"
|
||||
width="11.091683"
|
||||
height="0.6681723"
|
||||
x="69.741707"
|
||||
y="74.331406"
|
||||
transform="rotate(-5.3501722)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1050-6"
|
||||
width="11.091683"
|
||||
height="0.6681723"
|
||||
x="69.741707"
|
||||
y="76.97068"
|
||||
transform="rotate(-5.3501722)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1050-2"
|
||||
width="11.091683"
|
||||
height="0.6681723"
|
||||
x="69.741707"
|
||||
y="79.609978"
|
||||
transform="rotate(-5.3501722)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1050-9"
|
||||
width="11.091683"
|
||||
height="0.6681723"
|
||||
x="69.741707"
|
||||
y="82.249275"
|
||||
transform="rotate(-5.3501722)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1050-1"
|
||||
width="11.091683"
|
||||
height="0.6681723"
|
||||
x="69.741707"
|
||||
y="84.88855"
|
||||
transform="rotate(-5.3501722)" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:0.37089203;stroke:none;stroke-width:1.43235695;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 112.99958,47.239269 16.44636,-0.717802 0.74666,19.117834 -17.90823,0.01483 z"
|
||||
id="rect850-5-2"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<rect
|
||||
style="fill:#ffbaf2;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect850-6-7"
|
||||
width="17.639786"
|
||||
height="18.441595"
|
||||
x="111.91032"
|
||||
y="46.230576"
|
||||
ry="0"
|
||||
transform="rotate(0.12870348)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1106"
|
||||
sodipodi:sides="5"
|
||||
sodipodi:cx="116.59633"
|
||||
sodipodi:cy="51.980659"
|
||||
sodipodi:r1="5.158371"
|
||||
sodipodi:r2="2.5791855"
|
||||
sodipodi:arg1="0.93247652"
|
||||
sodipodi:arg2="1.560795"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 119.66993,56.123337 -3.04781,-1.563621 -3.01592,1.624268 0.54527,-3.38182 -2.47674,-2.366385 3.3848,-0.526458 1.48521,-3.086775 1.54665,3.056451 3.39465,0.458653 -2.42891,2.415449 z"
|
||||
inkscape:transform-center-x="0.0074469719"
|
||||
inkscape:transform-center-y="-0.21178579"
|
||||
transform="matrix(0.46706944,0,0,0.44362881,61.135486,28.319186)" />
|
||||
<circle
|
||||
style="display:inline;fill:#000000;fill-opacity:0.28169017;stroke:none;stroke-width:0.94067413;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1113-7"
|
||||
cx="100.86058"
|
||||
cy="96.979729"
|
||||
r="1.4098805" />
|
||||
<path
|
||||
style="display:inline;fill:#000000;fill-opacity:0.4600939;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 99.418533,101.28282 25.586307,-0.99219 -0.90343,31.4674 -25.675065,0.0391 z"
|
||||
id="rect1109-3"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<rect
|
||||
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1109"
|
||||
width="26.058779"
|
||||
height="35.947754"
|
||||
x="99.023354"
|
||||
y="94.944237"
|
||||
ry="0" />
|
||||
<circle
|
||||
style="display:inline;fill:#000000;fill-opacity:0.28169017;stroke:none;stroke-width:0.94067413;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1113-7-0"
|
||||
cx="124.49057"
|
||||
cy="95.336449"
|
||||
r="1.4098805" />
|
||||
<circle
|
||||
style="display:inline;fill:#20b73e;fill-opacity:1;stroke:none;stroke-width:0.94067413;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1113-9"
|
||||
cx="124.63935"
|
||||
cy="95.253876"
|
||||
r="1.4098805" />
|
||||
<path
|
||||
style="display:inline;fill:none;stroke:#b6b6b6;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 104.55766,109.85454 2.45685,12.70945"
|
||||
id="path1326"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#b6b6b6;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 102.2898,115.38244 11.29205,-4.48847"
|
||||
id="path1359"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#b6b6b6;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 103.8962,120.01265 10.06362,-3.77976"
|
||||
id="path1361"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#b6b6b6;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 108.52641,109.85454 3.07106,9.82738"
|
||||
id="path1363"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#b40f00;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 110.27455,111.2247 1.79539,-3.07106"
|
||||
id="path1367"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#b40f00;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 109.6131,109.19308 2.50409,1.5119"
|
||||
id="path1369"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#b40f00;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 102.80952,113.6343 1.03944,-2.45685"
|
||||
id="path1371"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#b40f00;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 101.95908,112.16964 2.4096,1.18118"
|
||||
id="path1373"
|
||||
inkscape:connector-curvature="0" />
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:0.4600939;stroke:#4500a3;stroke-width:0.26499999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1375"
|
||||
cx="108.47916"
|
||||
cy="115.9494"
|
||||
rx="1.0866816"
|
||||
ry="1.2284226" />
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:0.4600939;stroke:#4500a3;stroke-width:0.26499999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1377"
|
||||
cx="106.77827"
|
||||
cy="111.53181"
|
||||
rx="1.1811756"
|
||||
ry="1.0158111" />
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:0.4600939;stroke:#4500a3;stroke-width:0.26499999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1381"
|
||||
cx="104.67578"
|
||||
cy="122.39862"
|
||||
rx="1.3937871"
|
||||
ry="1.2047991" />
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:0.4600939;stroke:#4500a3;stroke-width:0.26499999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1383"
|
||||
cx="109.32961"
|
||||
cy="120.88672"
|
||||
rx="1.2284226"
|
||||
ry="1.4410343" />
|
||||
<path
|
||||
style="fill:none;stroke:#b40f00;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 113.01488,120.6741 0.0472,-3.07105"
|
||||
id="path1385"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;stroke:#b40f00;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 111.59747,117.79204 2.55134,1.98437"
|
||||
id="path1387"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
style="display:inline;fill:#000000;fill-opacity:0.28169017;stroke:none;stroke-width:0.94067413;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1113-7-0-6"
|
||||
cx="100.85116"
|
||||
cy="96.796585"
|
||||
r="1.4098805" />
|
||||
<circle
|
||||
style="display:inline;fill:#20b73e;fill-opacity:1;stroke:none;stroke-width:0.94067413;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1113-9-2"
|
||||
cx="100.99994"
|
||||
cy="96.714012"
|
||||
r="1.4098805" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Footstands"
|
||||
style="display:inline"
|
||||
transform="translate(9.3800012,-27.370898)">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4623"
|
||||
width="7.7508163"
|
||||
height="2.6726952"
|
||||
x="54.030792"
|
||||
y="236.70012" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.38199997;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4623-6"
|
||||
width="7.7508163"
|
||||
height="2.6726952"
|
||||
x="133.54349"
|
||||
y="236.70012" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
64
src/cache.rs
64
src/cache.rs
|
|
@ -9,13 +9,12 @@ use std::ffi::OsStr;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use async_trait::async_trait;
|
||||
use csscolorparser::Color;
|
||||
use url::Url;
|
||||
|
||||
use crate::traits::CalDavSource;
|
||||
use crate::traits::BaseCalendar;
|
||||
use crate::traits::CompleteCalendar;
|
||||
use crate::calendar::cached_calendar::CachedCalendar;
|
||||
use crate::calendar::CalendarId;
|
||||
use crate::calendar::SupportedComponents;
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
|
|
@ -26,9 +25,6 @@ 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 functionality is provided by the `CalDavSource` async trait it implements.
|
||||
/// However, since these functions do not _need_ to be actually async, non-async versions of them are also provided for better convenience. See [`Cache::get_calendar_sync`] for example
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
backing_folder: PathBuf,
|
||||
|
|
@ -42,7 +38,7 @@ pub struct Cache {
|
|||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
struct CachedData {
|
||||
#[serde(skip)]
|
||||
calendars: HashMap<Url, Arc<Mutex<CachedCalendar>>>,
|
||||
calendars: HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
|
|
@ -87,7 +83,7 @@ impl Cache {
|
|||
continue;
|
||||
},
|
||||
Ok(cal) =>
|
||||
data.calendars.insert(cal.url().clone(), Arc::new(Mutex::new(cal))),
|
||||
data.calendars.insert(cal.id().clone(), Arc::new(Mutex::new(cal))),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
|
@ -132,8 +128,8 @@ impl Cache {
|
|||
serde_json::to_writer(file, &self.data)?;
|
||||
|
||||
// Save each calendar
|
||||
for (cal_url, cal_mutex) in &self.data.calendars {
|
||||
let file_name = sanitize_filename::sanitize(cal_url.as_str()) + ".cal";
|
||||
for (cal_id, cal_mutex) in &self.data.calendars {
|
||||
let file_name = sanitize_filename::sanitize(cal_id.as_str()) + ".cal";
|
||||
let cal_file = folder.join(file_name);
|
||||
let file = std::fs::File::create(&cal_file)?;
|
||||
let cal = cal_mutex.lock().unwrap();
|
||||
|
|
@ -157,10 +153,10 @@ impl Cache {
|
|||
return Ok(false);
|
||||
}
|
||||
|
||||
for (calendar_url, cal_l) in calendars_l {
|
||||
log::debug!("Comparing calendars {}", calendar_url);
|
||||
for (calendar_id, cal_l) in calendars_l {
|
||||
log::debug!("Comparing calendars {}", calendar_id);
|
||||
let cal_l = cal_l.lock().unwrap();
|
||||
let cal_r = match calendars_r.get(&calendar_url) {
|
||||
let cal_r = match calendars_r.get(&calendar_id) {
|
||||
Some(c) => c.lock().unwrap(),
|
||||
None => return Err("should not happen, we've just tested keys are the same".into()),
|
||||
};
|
||||
|
|
@ -184,40 +180,29 @@ impl Drop for Cache {
|
|||
}
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
/// The non-async version of [`crate::traits::CalDavSource::get_calendars`]
|
||||
pub fn get_calendars_sync(&self) -> Result<HashMap<Url, 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>> {
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?;
|
||||
|
||||
Ok(self.data.calendars.iter()
|
||||
.map(|(url, cal)| (url.clone(), cal.clone()))
|
||||
.map(|(id, cal)| (id.clone(), cal.clone()))
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
/// The non-async version of [`crate::traits::CalDavSource::get_calendar`]
|
||||
pub fn get_calendar_sync(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
|
||||
self.data.calendars.get(url).map(|arc| arc.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CalDavSource<CachedCalendar> for Cache {
|
||||
async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
|
||||
self.get_calendars_sync()
|
||||
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> {
|
||||
self.data.calendars.get(id).map(|arc| arc.clone())
|
||||
}
|
||||
|
||||
async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
|
||||
self.get_calendar_sync(url)
|
||||
}
|
||||
|
||||
async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> {
|
||||
log::debug!("Inserting local calendar {}", url);
|
||||
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> 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, url.clone(), supported_components, color);
|
||||
let new_calendar = CachedCalendar::new(name, id.clone(), supported_components);
|
||||
let arc = Arc::new(Mutex::new(new_calendar));
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
|
|
@ -225,7 +210,7 @@ impl CalDavSource<CachedCalendar> for Cache {
|
|||
arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour)));
|
||||
};
|
||||
|
||||
match self.data.calendars.insert(url, arc.clone()) {
|
||||
match self.data.calendars.insert(id, arc.clone()) {
|
||||
Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()),
|
||||
None => Ok(arc),
|
||||
}
|
||||
|
|
@ -248,25 +233,23 @@ 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();
|
||||
|
||||
{
|
||||
let mut bucket_list = bucket_list.lock().unwrap();
|
||||
let cal_url = bucket_list.url().clone();
|
||||
let cal_id = bucket_list.id().clone();
|
||||
bucket_list.add_item(Item::Task(Task::new(
|
||||
String::from("Attend a concert of JS Bach"), false, &cal_url
|
||||
String::from("Attend a concert of JS Bach"), false, &cal_id
|
||||
))).await.unwrap();
|
||||
|
||||
bucket_list.add_item(Item::Task(Task::new(
|
||||
String::from("Climb the Lighthouse of Alexandria"), true, &cal_url
|
||||
String::from("Climb the Lighthouse of Alexandria"), true, &cal_id
|
||||
))).await.unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -294,12 +277,11 @@ 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 a second calendar with the same URL
|
||||
// We should not be able to add twice the same calendar
|
||||
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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ use std::error::Error;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use async_trait::async_trait;
|
||||
use csscolorparser::Color;
|
||||
use url::Url;
|
||||
|
||||
use crate::item::SyncStatus;
|
||||
use crate::traits::{BaseCalendar, CompleteCalendar};
|
||||
use crate::calendar::SupportedComponents;
|
||||
use crate::calendar::{CalendarId, SupportedComponents};
|
||||
use crate::Item;
|
||||
use crate::item::ItemId;
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
|
@ -18,20 +17,16 @@ use crate::mock_behaviour::MockBehaviour;
|
|||
|
||||
|
||||
/// A calendar used by the [`cache`](crate::cache) module
|
||||
///
|
||||
/// Most of its functionality is provided by the async traits it implements.
|
||||
/// However, since these functions do not _need_ to be actually async, non-async versions of them are also provided for better convenience. See [`CachedCalendar::add_item_sync`] for example
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CachedCalendar {
|
||||
name: String,
|
||||
url: Url,
|
||||
id: CalendarId,
|
||||
supported_components: SupportedComponents,
|
||||
color: Option<Color>,
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
#[serde(skip)]
|
||||
mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>,
|
||||
|
||||
items: HashMap<Url, Item>,
|
||||
items: HashMap<ItemId, Item>,
|
||||
}
|
||||
|
||||
impl CachedCalendar {
|
||||
|
|
@ -43,43 +38,43 @@ impl CachedCalendar {
|
|||
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
fn add_item_maybe_mocked(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
|
||||
async 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)
|
||||
self.add_or_update_item_force_synced(item).await
|
||||
} else {
|
||||
self.regular_add_or_update_item(item)
|
||||
self.regular_add_or_update_item(item).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
fn update_item_maybe_mocked(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
|
||||
async 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)
|
||||
self.add_or_update_item_force_synced(item).await
|
||||
} else {
|
||||
self.regular_add_or_update_item(item)
|
||||
self.regular_add_or_update_item(item).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Add or update an item
|
||||
fn regular_add_or_update_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
|
||||
async 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.url().clone(), item);
|
||||
self.items.insert(item.id().clone(), item);
|
||||
Ok(ss_clone)
|
||||
}
|
||||
|
||||
/// 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")]
|
||||
fn add_or_update_item_force_synced(&mut self, mut item: Item) -> Result<SyncStatus, Box<dyn Error>> {
|
||||
async 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(_) => (),
|
||||
_ => item.set_sync_status(SyncStatus::random_synced()),
|
||||
};
|
||||
let ss_clone = item.sync_status().clone();
|
||||
self.items.insert(item.url().clone(), item);
|
||||
self.items.insert(item.id().clone(), item);
|
||||
Ok(ss_clone)
|
||||
}
|
||||
|
||||
|
|
@ -87,10 +82,8 @@ impl CachedCalendar {
|
|||
#[cfg(any(test, feature = "integration_tests"))]
|
||||
pub async fn has_same_observable_content_as(&self, other: &CachedCalendar) -> Result<bool, Box<dyn Error>> {
|
||||
if self.name != other.name
|
||||
|| self.url != other.url
|
||||
|| self.supported_components != other.supported_components
|
||||
|| self.color != other.color
|
||||
{
|
||||
|| self.id != other.id
|
||||
|| self.supported_components != other.supported_components {
|
||||
log::debug!("Calendar properties mismatch");
|
||||
return Ok(false);
|
||||
}
|
||||
|
|
@ -103,13 +96,13 @@ impl CachedCalendar {
|
|||
log::debug!("Different keys for items");
|
||||
return Ok(false);
|
||||
}
|
||||
for (url_l, item_l) in items_l {
|
||||
let item_r = match items_r.get(&url_l) {
|
||||
for (id_l, item_l) in items_l {
|
||||
let item_r = match items_r.get(&id_l) {
|
||||
Some(c) => c,
|
||||
None => return Err("should not happen, we've just tested keys are the same".into()),
|
||||
};
|
||||
if item_l.has_same_observable_content_as(&item_r) == false {
|
||||
log::debug!("Different items for URL {}:", url_l);
|
||||
log::debug!("Different items for id {}:", id_l);
|
||||
log::debug!("{:#?}", item_l);
|
||||
log::debug!("{:#?}", item_r);
|
||||
return Ok(false);
|
||||
|
|
@ -118,68 +111,81 @@ impl CachedCalendar {
|
|||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::get_item_urls`]
|
||||
pub fn get_item_urls_sync(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
||||
Ok(self.items.iter()
|
||||
.map(|(url, _)| url.clone())
|
||||
.collect()
|
||||
)
|
||||
|
||||
#[async_trait]
|
||||
impl BaseCalendar for CachedCalendar {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::get_items`]
|
||||
pub fn get_items_sync(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>> {
|
||||
Ok(self.items.iter()
|
||||
.map(|(url, item)| (url.clone(), item))
|
||||
.collect()
|
||||
)
|
||||
fn id(&self) -> &CalendarId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::get_items_mut`]
|
||||
pub fn get_items_mut_sync(&mut self) -> Result<HashMap<Url, &mut Item>, Box<dyn Error>> {
|
||||
Ok(self.items.iter_mut()
|
||||
.map(|(url, item)| (url.clone(), item))
|
||||
.collect()
|
||||
)
|
||||
fn supported_components(&self) -> SupportedComponents {
|
||||
self.supported_components
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::get_item_by_url`]
|
||||
pub fn get_item_by_url_sync<'a>(&'a self, url: &Url) -> Option<&'a Item> {
|
||||
self.items.get(url)
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::get_item_by_url_mut`]
|
||||
pub fn get_item_by_url_mut_sync<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item> {
|
||||
self.items.get_mut(url)
|
||||
}
|
||||
|
||||
/// 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.url()) {
|
||||
return Err(format!("Item {:?} cannot be added, it exists already", item.url()).into());
|
||||
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);
|
||||
return self.regular_add_or_update_item(item).await;
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
return self.add_item_maybe_mocked(item);
|
||||
return self.add_item_maybe_mocked(item).await;
|
||||
}
|
||||
|
||||
/// 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.url()) == false {
|
||||
return Err(format!("Item {:?} cannot be updated, it does not already exist", item.url()).into());
|
||||
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);
|
||||
return self.regular_add_or_update_item(item).await;
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
return self.update_item_maybe_mocked(item);
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::mark_for_deletion`]
|
||||
pub fn mark_for_deletion_sync(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
match self.items.get_mut(item_url) {
|
||||
async fn get_item_ids(&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>> {
|
||||
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> {
|
||||
self.items.get(id)
|
||||
}
|
||||
|
||||
async fn get_item_by_id_mut<'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>> {
|
||||
match self.items.get_mut(item_id) {
|
||||
None => Err("no item for this key".into()),
|
||||
Some(item) => {
|
||||
match item.sync_status() {
|
||||
|
|
@ -197,7 +203,7 @@ impl CachedCalendar {
|
|||
},
|
||||
SyncStatus::NotSynced => {
|
||||
// This was never synced to the server, we can safely delete it as soon as now
|
||||
self.items.remove(item_url);
|
||||
self.items.remove(item_id);
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
|
|
@ -205,82 +211,12 @@ impl CachedCalendar {
|
|||
}
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::immediately_delete_item`]
|
||||
pub fn immediately_delete_item_sync(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
match self.items.remove(item_url) {
|
||||
None => Err(format!("Item {} is absent from this calendar", item_url).into()),
|
||||
async fn immediately_delete_item(&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 url(&self) -> &Url {
|
||||
&self.url
|
||||
}
|
||||
|
||||
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, url: Url, supported_components: SupportedComponents, color: Option<Color>) -> Self {
|
||||
Self {
|
||||
name, url, supported_components, color,
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
mock_behaviour: None,
|
||||
items: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
||||
self.get_item_urls_sync()
|
||||
}
|
||||
|
||||
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>> {
|
||||
self.get_items_sync()
|
||||
}
|
||||
|
||||
async fn get_items_mut(&mut self) -> Result<HashMap<Url, &mut Item>, Box<dyn Error>> {
|
||||
self.get_items_mut_sync()
|
||||
}
|
||||
|
||||
async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item> {
|
||||
self.get_item_by_url_sync(url)
|
||||
}
|
||||
|
||||
async fn get_item_by_url_mut<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item> {
|
||||
self.get_item_by_url_mut_sync(url)
|
||||
}
|
||||
|
||||
async fn mark_for_deletion(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
self.mark_for_deletion_sync(item_url)
|
||||
}
|
||||
|
||||
async fn immediately_delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
self.immediately_delete_item_sync(item_url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -295,11 +231,11 @@ 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, color: Option<Color>) -> Self {
|
||||
crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components, color)
|
||||
fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self {
|
||||
crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components)
|
||||
}
|
||||
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<Url, VersionTag>, Box<dyn Error>> {
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> {
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_version_tags())?;
|
||||
|
||||
|
|
@ -307,38 +243,30 @@ impl DavCalendar for CachedCalendar {
|
|||
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for (url, item) in self.items.iter() {
|
||||
for (id, item) in self.items.iter() {
|
||||
let vt = match item.sync_status() {
|
||||
SyncStatus::Synced(vt) => vt.clone(),
|
||||
_ => {
|
||||
panic!("Mock calendars must contain only SyncStatus::Synced. Got {:?}", item);
|
||||
}
|
||||
};
|
||||
result.insert(url.clone(), vt);
|
||||
result.insert(id.clone(), vt);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>> {
|
||||
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>> {
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_url())?;
|
||||
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_id())?;
|
||||
|
||||
Ok(self.items.get(url).cloned())
|
||||
Ok(self.items.get(id).cloned())
|
||||
}
|
||||
|
||||
async fn get_items_by_url(&self, urls: &[Url]) -> Result<Vec<Option<Item>>, Box<dyn Error>> {
|
||||
let mut v = Vec::new();
|
||||
for url in urls {
|
||||
v.push(DavCalendar::get_item_by_url(self, url).await?);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_delete_item())?;
|
||||
|
||||
self.immediately_delete_item(item_url).await
|
||||
self.immediately_delete_item(item_id).await
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ bitflags! {
|
|||
impl SupportedComponents {
|
||||
pub fn to_xml_string(&self) -> String {
|
||||
format!(r#"
|
||||
<B:supported-calendar-component-set>
|
||||
<C:supported-calendar-component-set>
|
||||
{} {}
|
||||
</B:supported-calendar-component-set>
|
||||
</C:supported-calendar-component-set>
|
||||
"#,
|
||||
if self.contains(Self::EVENT) { "<B:comp name=\"VEVENT\"/>" } else { "" },
|
||||
if self.contains(Self::TODO) { "<B:comp name=\"VTODO\"/>" } else { "" },
|
||||
if self.contains(Self::EVENT) { "<C:comp name=\"VEVENT\"/>" } else { "" },
|
||||
if self.contains(Self::TODO) { "<C:comp name=\"VTODO\"/>" } else { "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -77,3 +77,6 @@ impl Default for SearchFilter {
|
|||
SearchFilter::All
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub type CalendarId = url::Url;
|
||||
|
|
|
|||
|
|
@ -4,17 +4,16 @@ use std::sync::Mutex;
|
|||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::{header::CONTENT_TYPE, header::CONTENT_LENGTH};
|
||||
use csscolorparser::Color;
|
||||
use url::Url;
|
||||
|
||||
use crate::traits::BaseCalendar;
|
||||
use crate::traits::DavCalendar;
|
||||
use crate::calendar::SupportedComponents;
|
||||
use crate::calendar::CalendarId;
|
||||
use crate::item::Item;
|
||||
use crate::item::ItemId;
|
||||
use crate::item::VersionTag;
|
||||
use crate::item::SyncStatus;
|
||||
use crate::resource::Resource;
|
||||
use crate::utils::find_elem;
|
||||
|
||||
static TASKS_BODY: &str = r#"
|
||||
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
|
|
@ -29,45 +28,31 @@ static TASKS_BODY: &str = r#"
|
|||
</c:calendar-query>
|
||||
"#;
|
||||
|
||||
static MULTIGET_BODY_PREFIX: &str = r#"
|
||||
<c:calendar-multiget xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<c:calendar-data />
|
||||
</d:prop>
|
||||
"#;
|
||||
static MULTIGET_BODY_SUFFIX: &str = r#"
|
||||
</c:calendar-multiget>
|
||||
"#;
|
||||
|
||||
|
||||
|
||||
/// 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<Url, VersionTag>>>,
|
||||
cached_version_tags: Mutex<Option<HashMap<ItemId, VersionTag>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BaseCalendar for RemoteCalendar {
|
||||
fn name(&self) -> &str { &self.name }
|
||||
fn url(&self) -> &Url { &self.resource.url() }
|
||||
fn id(&self) -> &CalendarId { &self.resource.url() }
|
||||
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)?;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.put(item.url().clone())
|
||||
.put(item.id().as_url().clone())
|
||||
.header("If-None-Match", "*")
|
||||
.header(CONTENT_TYPE, "text/calendar")
|
||||
.header(CONTENT_LENGTH, ical_text.len())
|
||||
|
|
@ -82,7 +67,7 @@ impl BaseCalendar for RemoteCalendar {
|
|||
|
||||
let reply_hdrs = response.headers();
|
||||
match reply_hdrs.get("ETag") {
|
||||
None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.url()).into()),
|
||||
None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.id()).into()),
|
||||
Some(etag) => {
|
||||
let vtag_str = etag.to_str()?;
|
||||
let vtag = VersionTag::from(String::from(vtag_str));
|
||||
|
|
@ -101,7 +86,7 @@ impl BaseCalendar for RemoteCalendar {
|
|||
let ical_text = crate::ical::build_from(&item)?;
|
||||
|
||||
let request = reqwest::Client::new()
|
||||
.put(item.url().clone())
|
||||
.put(item.id().as_url().clone())
|
||||
.header("If-Match", old_etag.as_str())
|
||||
.header(CONTENT_TYPE, "text/calendar")
|
||||
.header(CONTENT_LENGTH, ical_text.len())
|
||||
|
|
@ -116,7 +101,7 @@ impl BaseCalendar for RemoteCalendar {
|
|||
|
||||
let reply_hdrs = request.headers();
|
||||
match reply_hdrs.get("ETag") {
|
||||
None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.url()).into()),
|
||||
None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.id()).into()),
|
||||
Some(etag) => {
|
||||
let vtag_str = etag.to_str()?;
|
||||
let vtag = VersionTag::from(String::from(vtag_str));
|
||||
|
|
@ -128,15 +113,15 @@ impl BaseCalendar for RemoteCalendar {
|
|||
|
||||
#[async_trait]
|
||||
impl DavCalendar for RemoteCalendar {
|
||||
fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self {
|
||||
fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self {
|
||||
Self {
|
||||
name, resource, supported_components, color,
|
||||
name, resource, supported_components,
|
||||
cached_version_tags: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<Url, VersionTag>, Box<dyn Error>> {
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> {
|
||||
if let Some(map) = &*self.cached_version_tags.lock().unwrap() {
|
||||
log::debug!("Version tags are already cached.");
|
||||
return Ok(map.clone());
|
||||
|
|
@ -148,19 +133,19 @@ impl DavCalendar for RemoteCalendar {
|
|||
for response in responses {
|
||||
let item_url = crate::utils::find_elem(&response, "href")
|
||||
.map(|elem| self.resource.combine(&elem.text()));
|
||||
let item_url = match item_url {
|
||||
let item_id = match item_url {
|
||||
None => {
|
||||
log::warn!("Unable to extract HREF");
|
||||
continue;
|
||||
},
|
||||
Some(resource) => {
|
||||
resource.url().clone()
|
||||
ItemId::from(&resource)
|
||||
},
|
||||
};
|
||||
|
||||
let version_tag = match crate::utils::find_elem(&response, "getetag") {
|
||||
None => {
|
||||
log::warn!("Unable to extract ETAG for item {}, ignoring it", item_url);
|
||||
log::warn!("Unable to extract ETAG for item {}, ignoring it", item_id);
|
||||
continue;
|
||||
},
|
||||
Some(etag) => {
|
||||
|
|
@ -168,7 +153,7 @@ impl DavCalendar for RemoteCalendar {
|
|||
}
|
||||
};
|
||||
|
||||
items.insert(item_url.clone(), version_tag);
|
||||
items.insert(item_id, version_tag);
|
||||
}
|
||||
|
||||
// Note: the mutex cannot be locked during this whole async function, but it can safely be re-entrant (this will just waste an unnecessary request)
|
||||
|
|
@ -176,9 +161,9 @@ impl DavCalendar for RemoteCalendar {
|
|||
Ok(items)
|
||||
}
|
||||
|
||||
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>> {
|
||||
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>> {
|
||||
let res = reqwest::Client::new()
|
||||
.get(url.clone())
|
||||
.get(id.as_url().clone())
|
||||
.header(CONTENT_TYPE, "text/calendar")
|
||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||
.send()
|
||||
|
|
@ -192,52 +177,18 @@ impl DavCalendar for RemoteCalendar {
|
|||
|
||||
// This is supposed to be cached
|
||||
let version_tags = self.get_item_version_tags().await?;
|
||||
let vt = match version_tags.get(url) {
|
||||
None => return Err(format!("Inconsistent data: {} has no version tag", url).into()),
|
||||
let vt = match version_tags.get(id) {
|
||||
None => return Err(format!("Inconsistent data: {} has no version tag", id).into()),
|
||||
Some(vt) => vt,
|
||||
};
|
||||
|
||||
let item = crate::ical::parse(&text, url.clone(), SyncStatus::Synced(vt.clone()))?;
|
||||
let item = crate::ical::parse(&text, id.clone(), SyncStatus::Synced(vt.clone()))?;
|
||||
Ok(Some(item))
|
||||
}
|
||||
|
||||
async fn get_items_by_url(&self, urls: &[Url]) -> Result<Vec<Option<Item>>, Box<dyn Error>> {
|
||||
// Build the request body
|
||||
let mut hrefs = String::new();
|
||||
for url in urls {
|
||||
hrefs.push_str(&format!(" <d:href>{}</d:href>\n", url.path()));
|
||||
}
|
||||
let body = format!("{}{}{}", MULTIGET_BODY_PREFIX, hrefs, MULTIGET_BODY_SUFFIX);
|
||||
|
||||
// Send the request
|
||||
let xml_replies = crate::client::sub_request_and_extract_elems(&self.resource, "REPORT", body, "response").await?;
|
||||
|
||||
// This is supposed to be cached
|
||||
let version_tags = self.get_item_version_tags().await?;
|
||||
|
||||
// Parse the results
|
||||
let mut results = Vec::new();
|
||||
for xml_reply in xml_replies {
|
||||
let href = find_elem(&xml_reply, "href").ok_or("Missing HREF")?.text();
|
||||
let mut url = self.resource.url().clone();
|
||||
url.set_path(&href);
|
||||
let ical_data = find_elem(&xml_reply, "calendar-data").ok_or("Missing calendar-data")?.text();
|
||||
|
||||
let vt = match version_tags.get(&url) {
|
||||
None => return Err(format!("Inconsistent data: {} has no version tag", url).into()),
|
||||
Some(vt) => vt,
|
||||
};
|
||||
|
||||
let item = crate::ical::parse(&ical_data, url.clone(), SyncStatus::Synced(vt.clone()))?;
|
||||
results.push(Some(item));
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
|
||||
let del_response = reqwest::Client::new()
|
||||
.delete(item_url.clone())
|
||||
.delete(item_id.as_url().clone())
|
||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||
.send()
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ 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};
|
||||
use crate::calendar::remote_calendar::RemoteCalendar;
|
||||
use crate::calendar::CalendarId;
|
||||
use crate::calendar::SupportedComponents;
|
||||
use crate::traits::CalDavSource;
|
||||
use crate::traits::BaseCalendar;
|
||||
|
|
@ -42,7 +42,6 @@ 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>
|
||||
|
|
@ -98,7 +97,6 @@ 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,
|
||||
|
||||
|
|
@ -108,11 +106,11 @@ pub struct Client {
|
|||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Default)]
|
||||
struct CachedReplies {
|
||||
principal: Option<Resource>,
|
||||
calendar_home_set: Option<Resource>,
|
||||
calendars: Option<HashMap<Url, Arc<Mutex<RemoteCalendar>>>>,
|
||||
calendars: Option<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
|
|
@ -206,16 +204,9 @@ impl Client {
|
|||
},
|
||||
Ok(sc) => sc,
|
||||
};
|
||||
|
||||
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);
|
||||
let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components);
|
||||
log::info!("Found calendar {}", this_calendar.name());
|
||||
calendars.insert(this_calendar.url().clone(), Arc::new(Mutex::new(this_calendar)));
|
||||
calendars.insert(this_calendar.id().clone(), Arc::new(Mutex::new(this_calendar)));
|
||||
}
|
||||
|
||||
let mut replies = self.cached_replies.lock().unwrap();
|
||||
|
|
@ -227,7 +218,7 @@ impl Client {
|
|||
|
||||
#[async_trait]
|
||||
impl CalDavSource<RemoteCalendar> for Client {
|
||||
async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
|
||||
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
|
||||
self.populate_calendars().await?;
|
||||
|
||||
match &self.cached_replies.lock().unwrap().calendars {
|
||||
|
|
@ -238,7 +229,7 @@ impl CalDavSource<RemoteCalendar> for Client {
|
|||
};
|
||||
}
|
||||
|
||||
async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<RemoteCalendar>>> {
|
||||
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<RemoteCalendar>>> {
|
||||
if let Err(err) = self.populate_calendars().await {
|
||||
log::warn!("Unable to fetch calendars: {}", err);
|
||||
return None;
|
||||
|
|
@ -247,26 +238,26 @@ impl CalDavSource<RemoteCalendar> for Client {
|
|||
self.cached_replies.lock().unwrap()
|
||||
.calendars
|
||||
.as_ref()
|
||||
.and_then(|cals| cals.get(url))
|
||||
.and_then(|cals| cals.get(id))
|
||||
.map(|cal| cal.clone())
|
||||
}
|
||||
|
||||
async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
|
||||
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
|
||||
self.populate_calendars().await?;
|
||||
|
||||
match self.cached_replies.lock().unwrap().calendars.as_ref() {
|
||||
None => return Err("No calendars have been fetched".into()),
|
||||
Some(cals) => {
|
||||
if cals.contains_key(&url) {
|
||||
if cals.contains_key(&id) {
|
||||
return Err("This calendar already exists".into());
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
let creation_body = calendar_body(name, supported_components, color);
|
||||
let creation_body = calendar_body(name, supported_components);
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.request(Method::from_bytes(b"MKCALENDAR").unwrap(), url.clone())
|
||||
.request(Method::from_bytes(b"MKCALENDAR").unwrap(), id.clone())
|
||||
.header(CONTENT_TYPE, "application/xml")
|
||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||
.body(creation_body)
|
||||
|
|
@ -278,30 +269,25 @@ impl CalDavSource<RemoteCalendar> for Client {
|
|||
return Err(format!("Unexpected HTTP status code. Expected CREATED, got {}", status.as_u16()).into());
|
||||
}
|
||||
|
||||
self.get_calendar(&url).await.ok_or(format!("Unable to insert calendar {:?}", url).into())
|
||||
self.get_calendar(&id).await.ok_or(format!("Unable to insert calendar {:?}", id).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn calendar_body(name: String, supported_components: SupportedComponents, color: Option<Color>) -> String {
|
||||
let color_property = match color {
|
||||
None => "".to_string(),
|
||||
Some(color) => format!("<D:calendar-color xmlns:D=\"http://apple.com/ns/ical/\">{}FF</D:calendar-color>", color.to_hex_string().to_ascii_uppercase()),
|
||||
};
|
||||
|
||||
fn calendar_body(name: String, supported_components: SupportedComponents) -> String {
|
||||
// This is taken from https://tools.ietf.org/html/rfc4791#page-24
|
||||
format!(r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
|
||||
<A:set xmlns:A="DAV:">
|
||||
<A:prop>
|
||||
<A:displayname>{}</A:displayname>
|
||||
<C:mkcalendar xmlns:D="DAV:"
|
||||
xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:set>
|
||||
<D:prop>
|
||||
<D:displayname>{}</D:displayname>
|
||||
{}
|
||||
{}
|
||||
</A:prop>
|
||||
</A:set>
|
||||
</B:mkcalendar>
|
||||
</D:prop>
|
||||
</D:set>
|
||||
</C:mkcalendar>
|
||||
"#,
|
||||
name,
|
||||
color_property,
|
||||
supported_components.to_xml_string(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
//! Support for library configuration options
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// Part of the ProdID string that describes the organization (example of a ProdID string: `-//ABC Corporation//My Product//EN`).
|
||||
/// Feel free to override it when initing this library.
|
||||
pub static ORG_NAME: Lazy<Arc<Mutex<String>>> = Lazy::new(|| Arc::new(Mutex::new("My organization".to_string())));
|
||||
|
||||
/// Part of the ProdID string that describes the product name (example of a ProdID string: `-//ABC Corporation//My Product//EN`).
|
||||
/// Feel free to override it when initing this library.
|
||||
pub static PRODUCT_NAME: Lazy<Arc<Mutex<String>>> = Lazy::new(|| Arc::new(Mutex::new("KitchenFridge".to_string())));
|
||||
14
src/event.rs
14
src/event.rs
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use url::Url;
|
||||
|
||||
use crate::item::ItemId;
|
||||
use crate::item::SyncStatus;
|
||||
|
||||
/// TODO: implement `Event` one day.
|
||||
/// This crate currently only supports tasks, not calendar events.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Event {
|
||||
uid: String,
|
||||
id: ItemId,
|
||||
name: String,
|
||||
sync_status: SyncStatus,
|
||||
}
|
||||
|
|
@ -20,22 +20,18 @@ impl Event {
|
|||
unimplemented!();
|
||||
}
|
||||
|
||||
pub fn url(&self) -> &Url {
|
||||
unimplemented!();
|
||||
pub fn id(&self) -> &ItemId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn uid(&self) -> &str {
|
||||
&self.uid
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn ical_prod_id(&self) -> &str {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,38 +5,33 @@ 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>> {
|
||||
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 s_last_modified = format_date_time(item.last_modified());
|
||||
|
||||
let mut todo = ToDo::new(
|
||||
task.uid(),
|
||||
item.uid(),
|
||||
s_last_modified.clone(),
|
||||
);
|
||||
|
||||
task.creation_date().map(|dt|
|
||||
item.creation_date().map(|dt|
|
||||
todo.push(Created::new(format_date_time(dt)))
|
||||
);
|
||||
todo.push(LastModified::new(s_last_modified));
|
||||
todo.push(Summary::new(task.name()));
|
||||
todo.push(Summary::new(item.name()));
|
||||
|
||||
match task.completion_status() {
|
||||
match item {
|
||||
Item::Task(t) => {
|
||||
match t.completion_status() {
|
||||
CompletionStatus::Uncompleted => {
|
||||
todo.push(Status::needs_action());
|
||||
},
|
||||
|
|
@ -48,14 +43,13 @@ pub fn build_from_task(task: &Task) -> Result<String, Box<dyn Error>> {
|
|||
todo.push(Status::completed());
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
},
|
||||
_ => {
|
||||
unimplemented!()
|
||||
},
|
||||
}
|
||||
|
||||
let mut calendar = ICalendar::new("2.0", task.ical_prod_id());
|
||||
let mut calendar = ICalendar::new("2.0", ical_product_id());
|
||||
calendar.add_todo(todo);
|
||||
|
||||
Ok(calendar.to_string())
|
||||
|
|
@ -66,26 +60,10 @@ 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() {
|
||||
|
|
@ -104,7 +82,7 @@ mod tests {
|
|||
COMPLETED:{}\r\n\
|
||||
STATUS:COMPLETED\r\n\
|
||||
END:VTODO\r\n\
|
||||
END:VCALENDAR\r\n", ORG_NAME.lock().unwrap(), PRODUCT_NAME.lock().unwrap(), 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);
|
||||
}
|
||||
|
|
@ -124,18 +102,18 @@ 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.lock().unwrap(), PRODUCT_NAME.lock().unwrap(), 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);
|
||||
}
|
||||
|
||||
fn build_task(completed: bool) -> (String, String, String) {
|
||||
let cal_url = "http://my.calend.ar/id".parse().unwrap();
|
||||
let cal_id = "http://my.calend.ar/id".parse().unwrap();
|
||||
let now = Utc::now();
|
||||
let s_now = format_date_time(&now);
|
||||
|
||||
let task = Item::Task(Task::new(
|
||||
String::from("This is a task with ÜTF-8 characters"), completed, &cal_url
|
||||
String::from("This is a task with ÜTF-8 characters"), completed, &cal_id
|
||||
));
|
||||
|
||||
let ical = build_from(&task).unwrap();
|
||||
|
|
|
|||
|
|
@ -6,51 +6,3 @@ 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.lock().unwrap(), PRODUCT_NAME.lock().unwrap())
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,30 +4,26 @@ use std::error::Error;
|
|||
|
||||
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use url::Url;
|
||||
|
||||
use crate::Item;
|
||||
use crate::item::SyncStatus;
|
||||
use crate::item::ItemId;
|
||||
use crate::Task;
|
||||
use crate::task::CompletionStatus;
|
||||
use crate::Event;
|
||||
|
||||
|
||||
/// Parse an iCal file into the internal representation [`crate::Item`]
|
||||
pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> {
|
||||
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 iCal data to parse for item {}", item_url).into()),
|
||||
None => return Err(format!("Invalid uCal data to parse for item {}", item_id).into()),
|
||||
Some(item) => match item {
|
||||
Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()),
|
||||
Err(err) => return Err(format!("Unable to parse uCal 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())
|
||||
|
|
@ -40,38 +36,11 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
|||
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 {
|
||||
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."
|
||||
// "In the case of an iCalendar object that doesn't specify a "METHOD"
|
||||
// property [e.g.: VTODO and VEVENT], this property is equivalent to the "LAST-MODIFIED" property".
|
||||
last_modified = parse_date_time_from_property(&prop.value);
|
||||
},
|
||||
"LAST-MODIFIED" => {
|
||||
// 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."
|
||||
// In practise, for VEVENT and VTODO, this is generally the same value as DTSTAMP.
|
||||
last_modified = parse_date_time_from_property(&prop.value);
|
||||
if prop.name == "SUMMARY" {
|
||||
name = prop.value.clone();
|
||||
}
|
||||
"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" => {
|
||||
if prop.name == "STATUS" {
|
||||
// Possible values:
|
||||
// "NEEDS-ACTION" ;Indicates to-do needs action.
|
||||
// "COMPLETED" ;Indicates to-do completed.
|
||||
|
|
@ -81,23 +50,37 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
|||
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 {
|
||||
Some(name) => name,
|
||||
None => return Err(format!("Missing name for item {}", item_url).into()),
|
||||
None => return Err(format!("Missing name for item {}", item_id).into()),
|
||||
};
|
||||
let uid = match uid {
|
||||
Some(uid) => uid,
|
||||
None => return Err(format!("Missing UID for item {}", item_url).into()),
|
||||
None => return Err(format!("Missing UID for item {}", item_id).into()),
|
||||
};
|
||||
let last_modified = match last_modified {
|
||||
Some(dt) => dt,
|
||||
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_url).into()),
|
||||
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_id).into()),
|
||||
};
|
||||
let completion_status = match completed {
|
||||
false => {
|
||||
|
|
@ -109,7 +92,7 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
|||
true => CompletionStatus::Completed(completion_date),
|
||||
};
|
||||
|
||||
Item::Task(Task::new_with_parameters(name, uid, item_url, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters))
|
||||
Item::Task(Task::new_with_parameters(name, uid, item_id, completion_status, sync_status, creation_date, last_modified))
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -123,8 +106,7 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
|
|||
}
|
||||
|
||||
fn parse_date_time(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
|
||||
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%SZ")
|
||||
.or_else(|_err| Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S") )
|
||||
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S")
|
||||
}
|
||||
|
||||
fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> {
|
||||
|
|
@ -140,16 +122,6 @@ 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),
|
||||
|
|
@ -254,13 +226,13 @@ END:VCALENDAR
|
|||
fn test_ical_parsing() {
|
||||
let version_tag = VersionTag::from(String::from("test-tag"));
|
||||
let sync_status = SyncStatus::Synced(version_tag);
|
||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||
let item_id: ItemId = "http://some.id/for/testing".parse().unwrap();
|
||||
|
||||
let item = parse(EXAMPLE_ICAL, item_url.clone(), sync_status.clone()).unwrap();
|
||||
let item = parse(EXAMPLE_ICAL, item_id.clone(), sync_status.clone()).unwrap();
|
||||
let task = item.unwrap_task();
|
||||
|
||||
assert_eq!(task.name(), "Do not forget to do this");
|
||||
assert_eq!(task.url(), &item_url);
|
||||
assert_eq!(task.id(), &item_id);
|
||||
assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com");
|
||||
assert_eq!(task.completed(), false);
|
||||
assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted);
|
||||
|
|
@ -272,9 +244,9 @@ END:VCALENDAR
|
|||
fn test_completed_ical_parsing() {
|
||||
let version_tag = VersionTag::from(String::from("test-tag"));
|
||||
let sync_status = SyncStatus::Synced(version_tag);
|
||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||
let item_id: ItemId = "http://some.id/for/testing".parse().unwrap();
|
||||
|
||||
let item = parse(EXAMPLE_ICAL_COMPLETED, item_url.clone(), sync_status.clone()).unwrap();
|
||||
let item = parse(EXAMPLE_ICAL_COMPLETED, item_id.clone(), sync_status.clone()).unwrap();
|
||||
let task = item.unwrap_task();
|
||||
|
||||
assert_eq!(task.completed(), true);
|
||||
|
|
@ -285,9 +257,9 @@ END:VCALENDAR
|
|||
fn test_completed_without_date_ical_parsing() {
|
||||
let version_tag = VersionTag::from(String::from("test-tag"));
|
||||
let sync_status = SyncStatus::Synced(version_tag);
|
||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||
let item_id: ItemId = "http://some.id/for/testing".parse().unwrap();
|
||||
|
||||
let item = parse(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_url.clone(), sync_status.clone()).unwrap();
|
||||
let item = parse(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_id.clone(), sync_status.clone()).unwrap();
|
||||
let task = item.unwrap_task();
|
||||
|
||||
assert_eq!(task.completed(), true);
|
||||
|
|
@ -298,9 +270,9 @@ END:VCALENDAR
|
|||
fn test_multiple_items_in_ical() {
|
||||
let version_tag = VersionTag::from(String::from("test-tag"));
|
||||
let sync_status = SyncStatus::Synced(version_tag);
|
||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||
let item_id: ItemId = "http://some.id/for/testing".parse().unwrap();
|
||||
|
||||
let item = parse(EXAMPLE_MULTIPLE_ICAL, item_url.clone(), sync_status.clone());
|
||||
let item = parse(EXAMPLE_MULTIPLE_ICAL, item_id.clone(), sync_status.clone());
|
||||
assert!(item.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
133
src/item.rs
133
src/item.rs
|
|
@ -1,10 +1,17 @@
|
|||
//! CalDAV items (todo, events, journals...)
|
||||
// TODO: move Event and Task to nest them in crate::items::calendar::Calendar?
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use url::Url;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::resource::Resource;
|
||||
use crate::calendar::CalendarId;
|
||||
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Item {
|
||||
|
|
@ -12,27 +19,48 @@ 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 {
|
||||
synthetise_common_getter!(url, &Url);
|
||||
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 id(&self) -> &ItemId {
|
||||
match self {
|
||||
Item::Event(e) => e.id(),
|
||||
Item::Task(t) => t.id(),
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
|
|
@ -87,6 +115,67 @@ impl Item {
|
|||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||
pub struct ItemId {
|
||||
content: Url,
|
||||
}
|
||||
impl ItemId{
|
||||
/// Generate a random ItemId.
|
||||
pub fn random(parent_calendar: &CalendarId) -> Self {
|
||||
let random = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
||||
let u = parent_calendar.join(&random).unwrap(/* this cannot panic since we've just created a string that is a valid URL */);
|
||||
Self { content:u }
|
||||
}
|
||||
|
||||
pub fn as_url(&self) -> &Url {
|
||||
&self.content
|
||||
}
|
||||
}
|
||||
impl From<Url> for ItemId {
|
||||
fn from(url: Url) -> Self {
|
||||
Self { content: url }
|
||||
}
|
||||
}
|
||||
impl From<&Resource> for ItemId {
|
||||
fn from(resource: &Resource) -> Self {
|
||||
Self { content: resource.url().clone() }
|
||||
}
|
||||
}
|
||||
impl FromStr for ItemId {
|
||||
type Err = url::ParseError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let u: Url = s.parse()?;
|
||||
Ok(Self::from(u))
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ItemId {}
|
||||
impl Display for ItemId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(f, "{}", self.content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to support serde
|
||||
impl Serialize for ItemId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.content.as_str())
|
||||
}
|
||||
}
|
||||
/// Used to support serde
|
||||
impl<'de> Deserialize<'de> for ItemId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<ItemId, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let u = Url::deserialize(deserializer)?;
|
||||
Ok(ItemId{ content: u })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// A VersionTag is basically a CalDAV `ctag` or `etag`. Whenever it changes, this means the data has changed.
|
||||
|
|
@ -107,7 +196,7 @@ impl VersionTag {
|
|||
&self.tag
|
||||
}
|
||||
|
||||
/// Generate a random VersionTag
|
||||
/// Generate a random VesionTag
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
pub fn random() -> Self {
|
||||
let random = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
||||
|
|
@ -117,7 +206,7 @@ impl VersionTag {
|
|||
|
||||
|
||||
|
||||
/// Describes whether this item has been synced already, or modified since the last time it was synced
|
||||
/// Desribes 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
|
||||
|
|
|
|||
20
src/lib.rs
20
src/lib.rs
|
|
@ -1,8 +1,6 @@
|
|||
//! This crate provides a CalDAV client library. \
|
||||
//! CalDAV is described as "Calendaring Extensions to WebDAV" in [RFC 4791](https://datatracker.ietf.org/doc/html/rfc4791) and [RFC 7986](https://datatracker.ietf.org/doc/html/rfc7986) and the underlying iCal format is described at least in [RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545). \
|
||||
//! This library has been intensivley tested with Nextcloud servers. It should support Owncloud and iCloud as well, since they use the very same CalDAV protocol.
|
||||
//! This crate provides a way to manage CalDAV data.
|
||||
//!
|
||||
//! This initial implementation only supports TODO events. Thus it can fetch and update a CalDAV-hosted todo-list...just like [sticky notes on a kitchen fridge](https://www.google.com/search?q=kitchen+fridge+todo+list&tbm=isch) would. \
|
||||
//! Its initial implementation only supported TODO events, so that it could fetch and update a CalDAV-hosted todo-list...just like [sticky notes on a kitchen fridge](https://www.google.com/search?q=kitchen+fridge+todo+list&tbm=isch) would. \
|
||||
//! Supporting other items (and especially regular CalDAV calendar events) should be fairly trivial, as it should boil down to adding little logic in iCal files parsing, but any help is appreciated :-)
|
||||
//!
|
||||
//! ## Possible uses
|
||||
|
|
@ -14,20 +12,13 @@
|
|||
//!
|
||||
//! These two "data sources" (actual client and local cache) can be used together in a [`CalDavProvider`](CalDavProvider). \
|
||||
//! A `CalDavProvider` abstracts these two sources by merging them together into one virtual source. \
|
||||
//! It also handles synchronisation between the local cache and the server, and robustly recovers from any network error (so that it never corrupts the local or remote source).
|
||||
//! It also handles synchronisation between the local cache and the server.
|
||||
//!
|
||||
//! Note that many methods are defined in common traits (see [`crate::traits`]).
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! See example usage in the `examples/` folder, that you can run using `cargo run --example <example-name>`. \
|
||||
//! You can also have a look at [`Voilà`](https://github.com/daladim/voila-tasks), a GUI app that uses `kitchen-fridge` under the hood.
|
||||
//!
|
||||
//! ## Configuration options
|
||||
//!
|
||||
//! Have a look at the [`config`] module to see what default options can be overridden.
|
||||
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/daladim/kitchen-fridge/master/resources/kitchen-fridge.svg")]
|
||||
//! See example usage in the `examples/` folder, that you can run using `cargo run --example <example-name>`
|
||||
|
||||
pub mod traits;
|
||||
|
||||
|
|
@ -44,10 +35,9 @@ pub mod mock_behaviour;
|
|||
pub mod client;
|
||||
pub use client::Client;
|
||||
pub mod cache;
|
||||
pub use cache::Cache;
|
||||
pub mod ical;
|
||||
|
||||
pub mod config;
|
||||
pub mod settings;
|
||||
pub mod utils;
|
||||
pub mod resource;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub struct MockBehaviour {
|
|||
|
||||
// From the DavCalendar trait
|
||||
pub get_item_version_tags_behaviour: (u32, u32),
|
||||
pub get_item_by_url_behaviour: (u32, u32),
|
||||
pub get_item_by_id_behaviour: (u32, u32),
|
||||
pub delete_item_behaviour: (u32, u32),
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ impl MockBehaviour {
|
|||
add_item_behaviour: (0, n_fails),
|
||||
update_item_behaviour: (0, n_fails),
|
||||
get_item_version_tags_behaviour: (0, n_fails),
|
||||
get_item_by_url_behaviour: (0, n_fails),
|
||||
get_item_by_id_behaviour: (0, n_fails),
|
||||
delete_item_behaviour: (0, n_fails),
|
||||
}
|
||||
}
|
||||
|
|
@ -84,9 +84,9 @@ impl MockBehaviour {
|
|||
if self.is_suspended { return Ok(()) }
|
||||
decrement(&mut self.get_item_version_tags_behaviour, "get_item_version_tags")
|
||||
}
|
||||
pub fn can_get_item_by_url(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
pub fn can_get_item_by_id(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
if self.is_suspended { return Ok(()) }
|
||||
decrement(&mut self.get_item_by_url_behaviour, "get_item_by_url")
|
||||
decrement(&mut self.get_item_by_id_behaviour, "get_item_by_id")
|
||||
}
|
||||
pub fn can_delete_item(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
if self.is_suspended { return Ok(()) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,401 @@
|
|||
//! This modules abstracts data sources and merges them in a single virtual one
|
||||
|
||||
use std::error::Error;
|
||||
use std::collections::HashSet;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::traits::{BaseCalendar, CalDavSource, DavCalendar};
|
||||
use crate::traits::CompleteCalendar;
|
||||
use crate::item::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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
pub struct Provider<L, T, R, U>
|
||||
where
|
||||
L: CalDavSource<T>,
|
||||
T: CompleteCalendar + Sync + Send,
|
||||
R: CalDavSource<U>,
|
||||
U: DavCalendar + Sync + Send,
|
||||
{
|
||||
/// The remote source (usually a server)
|
||||
remote: R,
|
||||
/// The local cache
|
||||
local: L,
|
||||
|
||||
phantom_t: PhantomData<T>,
|
||||
phantom_u: PhantomData<U>,
|
||||
}
|
||||
|
||||
impl<L, T, R, U> Provider<L, T, R, U>
|
||||
where
|
||||
L: CalDavSource<T>,
|
||||
T: CompleteCalendar + Sync + Send,
|
||||
R: CalDavSource<U>,
|
||||
U: DavCalendar + Sync + Send,
|
||||
{
|
||||
/// Create a provider.
|
||||
///
|
||||
/// `remote` is usually a [`Client`](crate::client::Client), `local` is usually a [`Cache`](crate::cache::Cache).
|
||||
/// However, both can be interchangeable. The only difference is that `remote` always wins in case of a sync conflict
|
||||
pub fn new(remote: R, local: L) -> Self {
|
||||
Self { remote, local,
|
||||
phantom_t: PhantomData, phantom_u: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the data source described as `local`
|
||||
pub fn local(&self) -> &L { &self.local }
|
||||
/// Returns the data source described as `local`
|
||||
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.
|
||||
/// 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`.
|
||||
///
|
||||
/// 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)
|
||||
///
|
||||
/// 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()
|
||||
}
|
||||
|
||||
async fn run_sync(&mut self, result: &mut SyncResult) -> Result<(), Box<dyn Error>> {
|
||||
result.info("Starting a sync.");
|
||||
|
||||
let mut handled_calendars = HashSet::new();
|
||||
|
||||
// Sync every remote calendar
|
||||
let cals_remote = self.remote.get_calendars().await?;
|
||||
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));
|
||||
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));
|
||||
continue;
|
||||
}
|
||||
handled_calendars.insert(cal_id);
|
||||
}
|
||||
|
||||
// Sync every local calendar that would not be in the remote yet
|
||||
let cals_local = self.local.get_calendars().await?;
|
||||
for (cal_id, cal_local) in cals_local {
|
||||
if handled_calendars.contains(&cal_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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));
|
||||
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));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
async fn get_or_insert_local_counterpart_calendar(&mut self, cal_id: &CalendarId, needle: Arc<Mutex<U>>) -> Result<Arc<Mutex<T>>, Box<dyn Error>> {
|
||||
get_or_insert_counterpart_calendar("local", &mut self.local, cal_id, needle).await
|
||||
}
|
||||
async fn get_or_insert_remote_counterpart_calendar(&mut self, cal_id: &CalendarId, needle: Arc<Mutex<T>>) -> Result<Arc<Mutex<U>>, Box<dyn Error>> {
|
||||
get_or_insert_counterpart_calendar("remote", &mut self.remote, cal_id, needle).await
|
||||
}
|
||||
|
||||
|
||||
async fn sync_calendar_pair(cal_local: Arc<Mutex<T>>, cal_remote: Arc<Mutex<U>>, result: &mut SyncResult) -> Result<(), Box<dyn Error>> {
|
||||
let mut cal_remote = cal_remote.lock().unwrap();
|
||||
let mut cal_local = cal_local.lock().unwrap();
|
||||
|
||||
// Step 1 - find the differences
|
||||
result.debug("Finding the differences to sync...");
|
||||
let mut local_del = HashSet::new();
|
||||
let mut remote_del = HashSet::new();
|
||||
let mut local_changes = HashSet::new();
|
||||
let mut remote_changes = HashSet::new();
|
||||
let mut local_additions = HashSet::new();
|
||||
let mut remote_additions = HashSet::new();
|
||||
|
||||
let remote_items = cal_remote.get_item_version_tags().await?;
|
||||
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 {
|
||||
None => {
|
||||
// This was created on the remote
|
||||
result.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));
|
||||
}
|
||||
|
||||
match local_item.sync_status() {
|
||||
SyncStatus::NotSynced => {
|
||||
result.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));
|
||||
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));
|
||||
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));
|
||||
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));
|
||||
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));
|
||||
remote_changes.insert(id);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
None => {
|
||||
result.error(&format!("Inconsistent state: missing task {} from the local tasks", id));
|
||||
continue;
|
||||
},
|
||||
Some(item) => item,
|
||||
};
|
||||
|
||||
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));
|
||||
remote_del.insert(id);
|
||||
},
|
||||
SyncStatus::NotSynced => {
|
||||
// This item has just been locally created
|
||||
result.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));
|
||||
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));
|
||||
remote_del.insert(id);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Step 2 - commit changes
|
||||
result.trace("Committing changes...");
|
||||
for id_del in local_del {
|
||||
result.debug(&format!("> Pushing local deletion {} to the server", id_del));
|
||||
match cal_remote.delete_item(&id_del).await {
|
||||
Err(err) => {
|
||||
result.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));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for id_del in remote_del {
|
||||
result.debug(&format!("> Applying remote deletion {} locally", id_del));
|
||||
if let Err(err) = cal_local.immediately_delete_item(&id_del).await {
|
||||
result.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));
|
||||
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));
|
||||
continue;
|
||||
},
|
||||
Ok(item) => match item {
|
||||
None => {
|
||||
result.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));
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for id_change in remote_changes {
|
||||
result.debug(&format!("> Applying remote change {} locally", id_change));
|
||||
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));
|
||||
continue;
|
||||
},
|
||||
Ok(item) => match item {
|
||||
None => {
|
||||
result.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));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for id_add in local_additions {
|
||||
result.debug(&format!("> Pushing local addition {} to the server", id_add));
|
||||
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));
|
||||
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)),
|
||||
Ok(new_ss) => {
|
||||
// Update local sync status
|
||||
item.set_sync_status(new_ss);
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
for id_change in local_changes {
|
||||
result.debug(&format!("> Pushing local change {} to the server", id_change));
|
||||
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));
|
||||
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)),
|
||||
Ok(new_ss) => {
|
||||
// Update local sync status
|
||||
item.set_sync_status(new_ss);
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn get_or_insert_counterpart_calendar<H, N, I>(haystack_descr: &str, haystack: &mut H, cal_id: &CalendarId, needle: Arc<Mutex<N>>)
|
||||
-> Result<Arc<Mutex<I>>, Box<dyn Error>>
|
||||
where
|
||||
H: CalDavSource<I>,
|
||||
I: BaseCalendar,
|
||||
N: BaseCalendar,
|
||||
{
|
||||
loop {
|
||||
if let Some(cal) = haystack.get_calendar(&cal_id).await {
|
||||
break Ok(cal);
|
||||
}
|
||||
|
||||
// This calendar does not exist locally yet, let's add it
|
||||
log::debug!("Adding a {} calendar {}", haystack_descr, cal_id);
|
||||
let src = needle.lock().unwrap();
|
||||
let name = src.name().to_string();
|
||||
let supported_comps = src.supported_components();
|
||||
if let Err(err) = haystack.create_calendar(
|
||||
cal_id.clone(),
|
||||
name,
|
||||
supported_comps,
|
||||
).await{
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,515 +0,0 @@
|
|||
//! 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;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use url::Url;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::traits::{BaseCalendar, CalDavSource, DavCalendar};
|
||||
use crate::traits::CompleteCalendar;
|
||||
use crate::item::SyncStatus;
|
||||
|
||||
pub mod sync_progress;
|
||||
use sync_progress::SyncProgress;
|
||||
use sync_progress::{FeedbackSender, SyncEvent};
|
||||
|
||||
/// How many items will be batched in a single HTTP request when downloading from the server
|
||||
#[cfg(not(test))]
|
||||
const DOWNLOAD_BATCH_SIZE: usize = 30;
|
||||
/// How many items will be batched in a single HTTP request when downloading from the server
|
||||
#[cfg(test)]
|
||||
const DOWNLOAD_BATCH_SIZE: usize = 3;
|
||||
|
||||
// I am too lazy to actually make `fetch_and_apply` generic over an async closure.
|
||||
// Let's work around by passing an enum, so that `fetch_and_apply` will know what to do
|
||||
enum BatchDownloadType {
|
||||
RemoteAdditions,
|
||||
RemoteChanges,
|
||||
}
|
||||
|
||||
impl Display for BatchDownloadType {
|
||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::RemoteAdditions => write!(f, "remote additions"),
|
||||
Self::RemoteChanges => write!(f, "remote changes"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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>,
|
||||
T: CompleteCalendar + Sync + Send,
|
||||
R: CalDavSource<U>,
|
||||
U: DavCalendar + Sync + Send,
|
||||
{
|
||||
/// The remote source (usually a server)
|
||||
remote: R,
|
||||
/// The local cache
|
||||
local: L,
|
||||
|
||||
phantom_t: PhantomData<T>,
|
||||
phantom_u: PhantomData<U>,
|
||||
}
|
||||
|
||||
impl<L, T, R, U> Provider<L, T, R, U>
|
||||
where
|
||||
L: CalDavSource<T>,
|
||||
T: CompleteCalendar + Sync + Send,
|
||||
R: CalDavSource<U>,
|
||||
U: DavCalendar + Sync + Send,
|
||||
{
|
||||
/// Create a provider.
|
||||
///
|
||||
/// `remote` is usually a [`Client`](crate::client::Client), `local` is usually a [`Cache`](crate::cache::Cache).
|
||||
/// However, both can be interchangeable. The only difference is that `remote` always wins in case of a sync conflict
|
||||
pub fn new(remote: R, local: L) -> Self {
|
||||
Self { remote, local,
|
||||
phantom_t: PhantomData, phantom_u: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the data source described as `local`
|
||||
pub fn local(&self) -> &L { &self.local }
|
||||
/// Returns the data source described as `local`
|
||||
pub fn local_mut(&mut self) -> &mut L { &mut self.local }
|
||||
/// Returns the data source described as `remote`.
|
||||
///
|
||||
/// 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`, 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).
|
||||
///
|
||||
/// 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 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
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
// Sync every remote calendar
|
||||
let cals_remote = self.remote.get_calendars().await?;
|
||||
for (cal_url, cal_remote) in cals_remote {
|
||||
let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_url, cal_remote.clone()).await {
|
||||
Err(err) => {
|
||||
progress.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_url, err));
|
||||
continue;
|
||||
},
|
||||
Ok(arc) => arc,
|
||||
};
|
||||
|
||||
if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, progress).await {
|
||||
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err));
|
||||
continue;
|
||||
}
|
||||
handled_calendars.insert(cal_url);
|
||||
}
|
||||
|
||||
// Sync every local calendar that would not be in the remote yet
|
||||
let cals_local = self.local.get_calendars().await?;
|
||||
for (cal_url, cal_local) in cals_local {
|
||||
if handled_calendars.contains(&cal_url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_url, cal_local.clone()).await {
|
||||
Err(err) => {
|
||||
progress.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_url, err));
|
||||
continue;
|
||||
},
|
||||
Ok(arc) => arc,
|
||||
};
|
||||
|
||||
if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, progress).await {
|
||||
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
progress.info("Sync ended");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
async fn get_or_insert_local_counterpart_calendar(&mut self, cal_url: &Url, needle: Arc<Mutex<U>>) -> Result<Arc<Mutex<T>>, Box<dyn Error>> {
|
||||
get_or_insert_counterpart_calendar("local", &mut self.local, cal_url, needle).await
|
||||
}
|
||||
async fn get_or_insert_remote_counterpart_calendar(&mut self, cal_url: &Url, needle: Arc<Mutex<T>>) -> Result<Arc<Mutex<U>>, Box<dyn Error>> {
|
||||
get_or_insert_counterpart_calendar("remote", &mut self.remote, cal_url, needle).await
|
||||
}
|
||||
|
||||
|
||||
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.reset_counter();
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
items_done_already: 0,
|
||||
details: "started".to_string()
|
||||
});
|
||||
|
||||
// Step 1 - find the differences
|
||||
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();
|
||||
let mut remote_changes = HashSet::new();
|
||||
let mut local_additions = HashSet::new();
|
||||
let mut remote_additions = HashSet::new();
|
||||
|
||||
let remote_items = cal_remote.get_item_version_tags().await?;
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
items_done_already: 0,
|
||||
details: format!("{} remote items", remote_items.len()),
|
||||
});
|
||||
|
||||
let mut local_items_to_handle = cal_local.get_item_urls().await?;
|
||||
for (url, remote_tag) in remote_items {
|
||||
progress.trace(&format!("***** Considering remote item {}...", url));
|
||||
match cal_local.get_item_by_url(&url).await {
|
||||
None => {
|
||||
// This was created on the remote
|
||||
progress.debug(&format!("* {} is a remote addition", url));
|
||||
remote_additions.insert(url);
|
||||
},
|
||||
Some(local_item) => {
|
||||
if local_items_to_handle.remove(&url) == false {
|
||||
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url));
|
||||
}
|
||||
|
||||
match local_item.sync_status() {
|
||||
SyncStatus::NotSynced => {
|
||||
progress.error(&format!("URL reuse between remote and local sources ({}). Ignoring this item in the sync", url));
|
||||
continue;
|
||||
},
|
||||
SyncStatus::Synced(local_tag) => {
|
||||
if &remote_tag != local_tag {
|
||||
// This has been modified on the remote
|
||||
progress.debug(&format!("* {} is a remote change", url));
|
||||
remote_changes.insert(url);
|
||||
}
|
||||
},
|
||||
SyncStatus::LocallyModified(local_tag) => {
|
||||
if &remote_tag == local_tag {
|
||||
// This has been changed locally
|
||||
progress.debug(&format!("* {} is a local change", url));
|
||||
local_changes.insert(url);
|
||||
} else {
|
||||
progress.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", url));
|
||||
progress.debug(&format!("* {} is considered a remote change", url));
|
||||
remote_changes.insert(url);
|
||||
}
|
||||
},
|
||||
SyncStatus::LocallyDeleted(local_tag) => {
|
||||
if &remote_tag == local_tag {
|
||||
// This has been locally deleted
|
||||
progress.debug(&format!("* {} is a local deletion", url));
|
||||
local_del.insert(url);
|
||||
} else {
|
||||
progress.info(&format!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", url));
|
||||
progress.debug(&format!("* {} is a considered a remote change", url));
|
||||
remote_changes.insert(url);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also iterate on the local tasks that are not on the remote
|
||||
for url in local_items_to_handle {
|
||||
progress.trace(&format!("##### Considering local item {}...", url));
|
||||
let local_item = match cal_local.get_item_by_url(&url).await {
|
||||
None => {
|
||||
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url));
|
||||
continue;
|
||||
},
|
||||
Some(item) => item,
|
||||
};
|
||||
|
||||
match local_item.sync_status() {
|
||||
SyncStatus::Synced(_) => {
|
||||
// This item has been removed from the remote
|
||||
progress.debug(&format!("# {} is a deletion from the server", url));
|
||||
remote_del.insert(url);
|
||||
},
|
||||
SyncStatus::NotSynced => {
|
||||
// This item has just been locally created
|
||||
progress.debug(&format!("# {} has been locally created", url));
|
||||
local_additions.insert(url);
|
||||
},
|
||||
SyncStatus::LocallyDeleted(_) => {
|
||||
// This item has been deleted from both sources
|
||||
progress.debug(&format!("# {} has been deleted from both sources", url));
|
||||
remote_del.insert(url);
|
||||
},
|
||||
SyncStatus::LocallyModified(_) => {
|
||||
progress.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", url));
|
||||
remote_del.insert(url);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Step 2 - commit changes
|
||||
progress.trace("Committing changes...");
|
||||
for url_del in local_del {
|
||||
progress.debug(&format!("> Pushing local deletion {} to the server", url_del));
|
||||
progress.increment_counter(1);
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
items_done_already: progress.counter(),
|
||||
details: Self::item_name(&cal_local, &url_del).await,
|
||||
});
|
||||
|
||||
match cal_remote.delete_item(&url_del).await {
|
||||
Err(err) => {
|
||||
progress.warn(&format!("Unable to delete remote item {}: {}", url_del, err));
|
||||
},
|
||||
Ok(()) => {
|
||||
// Change the local copy from "marked to deletion" to "actually deleted"
|
||||
if let Err(err) = cal_local.immediately_delete_item(&url_del).await {
|
||||
progress.error(&format!("Unable to permanently delete local item {}: {}", url_del, err));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for url_del in remote_del {
|
||||
progress.debug(&format!("> Applying remote deletion {} locally", url_del));
|
||||
progress.increment_counter(1);
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
items_done_already: progress.counter(),
|
||||
details: Self::item_name(&cal_local, &url_del).await,
|
||||
});
|
||||
if let Err(err) = cal_local.immediately_delete_item(&url_del).await {
|
||||
progress.warn(&format!("Unable to delete local item {}: {}", url_del, err));
|
||||
}
|
||||
}
|
||||
|
||||
Self::apply_remote_additions(
|
||||
remote_additions,
|
||||
&mut *cal_local,
|
||||
&mut *cal_remote,
|
||||
progress,
|
||||
&cal_name
|
||||
).await;
|
||||
|
||||
Self::apply_remote_changes(
|
||||
remote_changes,
|
||||
&mut *cal_local,
|
||||
&mut *cal_remote,
|
||||
progress,
|
||||
&cal_name
|
||||
).await;
|
||||
|
||||
|
||||
for url_add in local_additions {
|
||||
progress.debug(&format!("> Pushing local addition {} to the server", url_add));
|
||||
progress.increment_counter(1);
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
items_done_already: progress.counter(),
|
||||
details: Self::item_name(&cal_local, &url_add).await,
|
||||
});
|
||||
match cal_local.get_item_by_url_mut(&url_add).await {
|
||||
None => {
|
||||
progress.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", url_add));
|
||||
continue;
|
||||
},
|
||||
Some(item) => {
|
||||
match cal_remote.add_item(item.clone()).await {
|
||||
Err(err) => progress.error(&format!("Unable to add item {} to remote calendar: {}", url_add, err)),
|
||||
Ok(new_ss) => {
|
||||
// Update local sync status
|
||||
item.set_sync_status(new_ss);
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
for url_change in local_changes {
|
||||
progress.debug(&format!("> Pushing local change {} to the server", url_change));
|
||||
progress.increment_counter(1);
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
items_done_already: progress.counter(),
|
||||
details: Self::item_name(&cal_local, &url_change).await,
|
||||
});
|
||||
match cal_local.get_item_by_url_mut(&url_change).await {
|
||||
None => {
|
||||
progress.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", url_change));
|
||||
continue;
|
||||
},
|
||||
Some(item) => {
|
||||
match cal_remote.update_item(item.clone()).await {
|
||||
Err(err) => progress.error(&format!("Unable to update item {} in remote calendar: {}", url_change, err)),
|
||||
Ok(new_ss) => {
|
||||
// Update local sync status
|
||||
item.set_sync_status(new_ss);
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
async fn item_name(cal: &T, url: &Url) -> String {
|
||||
cal.get_item_by_url(url).await.map(|item| item.name()).unwrap_or_default().to_string()
|
||||
}
|
||||
|
||||
async fn apply_remote_additions(
|
||||
mut remote_additions: HashSet<Url>,
|
||||
cal_local: &mut T,
|
||||
cal_remote: &mut U,
|
||||
progress: &mut SyncProgress,
|
||||
cal_name: &str
|
||||
) {
|
||||
for batch in remote_additions.drain().chunks(DOWNLOAD_BATCH_SIZE).into_iter() {
|
||||
Self::fetch_batch_and_apply(BatchDownloadType::RemoteAdditions, batch, cal_local, cal_remote, progress, cal_name).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_remote_changes(
|
||||
mut remote_changes: HashSet<Url>,
|
||||
cal_local: &mut T,
|
||||
cal_remote: &mut U,
|
||||
progress: &mut SyncProgress,
|
||||
cal_name: &str
|
||||
) {
|
||||
for batch in remote_changes.drain().chunks(DOWNLOAD_BATCH_SIZE).into_iter() {
|
||||
Self::fetch_batch_and_apply(BatchDownloadType::RemoteChanges, batch, cal_local, cal_remote, progress, cal_name).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_batch_and_apply<I: Iterator<Item = Url>>(
|
||||
batch_type: BatchDownloadType,
|
||||
remote_additions: I,
|
||||
cal_local: &mut T,
|
||||
cal_remote: &mut U,
|
||||
progress: &mut SyncProgress,
|
||||
cal_name: &str
|
||||
) {
|
||||
progress.debug(&format!("> Applying a batch of {} locally", batch_type) /* too bad Chunks does not implement ExactSizeIterator, that could provide useful debug info. See https://github.com/rust-itertools/itertools/issues/171 */);
|
||||
|
||||
let list_of_additions: Vec<Url> = remote_additions.map(|url| url.clone()).collect();
|
||||
match cal_remote.get_items_by_url(&list_of_additions).await {
|
||||
Err(err) => {
|
||||
progress.warn(&format!("Unable to get the batch of {} {:?}: {}. Skipping them.", batch_type, list_of_additions, err));
|
||||
},
|
||||
Ok(items) => {
|
||||
for item in items {
|
||||
match item {
|
||||
None => {
|
||||
progress.error(&format!("Inconsistency: an item from the batch has vanished from the remote end"));
|
||||
continue;
|
||||
},
|
||||
Some(new_item) => {
|
||||
let local_update_result = match batch_type {
|
||||
BatchDownloadType::RemoteAdditions => cal_local.add_item(new_item.clone()).await,
|
||||
BatchDownloadType::RemoteChanges => cal_local.update_item(new_item.clone()).await,
|
||||
};
|
||||
if let Err(err) = local_update_result {
|
||||
progress.error(&format!("Not able to add item {} to local calendar: {}", new_item.url(), err));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Notifying every item at the same time would not make sense. Let's notify only one of them
|
||||
let one_item_name = match list_of_additions.get(0) {
|
||||
Some(url) => Self::item_name(&cal_local, &url).await,
|
||||
None => String::from("<unable to get the name of the first batched item>"),
|
||||
};
|
||||
progress.increment_counter(list_of_additions.len());
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.to_string(),
|
||||
items_done_already: progress.counter(),
|
||||
details: one_item_name,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn get_or_insert_counterpart_calendar<H, N, I>(haystack_descr: &str, haystack: &mut H, cal_url: &Url, needle: Arc<Mutex<N>>)
|
||||
-> Result<Arc<Mutex<I>>, Box<dyn Error>>
|
||||
where
|
||||
H: CalDavSource<I>,
|
||||
I: BaseCalendar,
|
||||
N: BaseCalendar,
|
||||
{
|
||||
loop {
|
||||
if let Some(cal) = haystack.get_calendar(&cal_url).await {
|
||||
break Ok(cal);
|
||||
}
|
||||
|
||||
// This calendar does not exist locally yet, let's add it
|
||||
log::debug!("Adding a {} calendar {}", haystack_descr, cal_url);
|
||||
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_url.clone(),
|
||||
name,
|
||||
supported_comps,
|
||||
color.cloned(),
|
||||
).await{
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
//! 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, items_done_already: usize, 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, items_done_already, details} => write!(f, "{} [{}/?] {}...", calendar, items_done_already, 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>,
|
||||
counter: usize,
|
||||
}
|
||||
impl SyncProgress {
|
||||
pub fn new() -> Self {
|
||||
Self { n_errors: 0, feedback_channel: None, counter: 0 }
|
||||
}
|
||||
pub fn new_with_feedback_channel(channel: FeedbackSender) -> Self {
|
||||
Self { n_errors: 0, feedback_channel: Some(channel), counter: 0 }
|
||||
}
|
||||
|
||||
/// Reset the user-info counter
|
||||
pub fn reset_counter(&mut self) {
|
||||
self.counter = 0;
|
||||
}
|
||||
/// Increments the user-info counter.
|
||||
pub fn increment_counter(&mut self, increment: usize) {
|
||||
self.counter += increment;
|
||||
}
|
||||
/// Retrieves the current user-info counter.
|
||||
/// This counts "arbitrary things", that's provided as a convenience but it is not used internally
|
||||
/// (e.g. that can be used to keep track of the items handled for the current calendar)
|
||||
pub fn counter(&self) -> usize {
|
||||
self.counter
|
||||
}
|
||||
|
||||
|
||||
|
||||
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,7 +1,7 @@
|
|||
use url::Url;
|
||||
|
||||
/// Just a wrapper around a URL and credentials
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct Resource {
|
||||
url: Url,
|
||||
username: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
// 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";
|
||||
44
src/task.rs
44
src/task.rs
|
|
@ -3,11 +3,10 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
use ical::property::Property;
|
||||
use url::Url;
|
||||
|
||||
use crate::item::ItemId;
|
||||
use crate::item::SyncStatus;
|
||||
use crate::utils::random_url;
|
||||
use crate::calendar::CalendarId;
|
||||
|
||||
/// RFC5545 defines the completion as several optional fields, yet some combinations make no sense.
|
||||
/// This enum provides an API that forbids such impossible combinations.
|
||||
|
|
@ -33,11 +32,10 @@ impl CompletionStatus {
|
|||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
/// The task URL
|
||||
url: Url,
|
||||
id: ItemId,
|
||||
|
||||
/// Persistent, globally unique identifier for the calendar component
|
||||
/// The [RFC](https://tools.ietf.org/html/rfc5545#page-117) recommends concatenating a timestamp with the server's domain name.
|
||||
/// UUID are even better so we'll generate them, but we have to support tasks from the server, that may have any arbitrary strings here.
|
||||
/// The [RFC](https://tools.ietf.org/html/rfc5545#page-117) recommends concatenating a timestamp with the server's domain name, but UUID are even better
|
||||
uid: String,
|
||||
|
||||
/// The sync status of this item
|
||||
|
|
@ -53,21 +51,14 @@ 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>,
|
||||
}
|
||||
|
||||
|
||||
impl Task {
|
||||
/// Create a brand new Task that is not on a server yet.
|
||||
/// This will pick a new (random) task ID.
|
||||
pub fn new(name: String, completed: bool, parent_calendar_url: &Url) -> Self {
|
||||
let new_url = random_url(parent_calendar_url);
|
||||
pub fn new(name: String, completed: bool, parent_calendar_id: &CalendarId) -> Self {
|
||||
let new_item_id = ItemId::random(parent_calendar_id);
|
||||
let new_sync_status = SyncStatus::NotSynced;
|
||||
let new_uid = Uuid::new_v4().to_hyphenated().to_string();
|
||||
let new_creation_date = Some(Utc::now());
|
||||
|
|
@ -75,46 +66,37 @@ impl Task {
|
|||
let new_completion_status = if completed {
|
||||
CompletionStatus::Completed(Some(Utc::now()))
|
||||
} else { CompletionStatus::Uncompleted };
|
||||
let ical_prod_id = crate::ical::default_prod_id();
|
||||
let extra_parameters = Vec::new();
|
||||
Self::new_with_parameters(name, new_uid, new_url, new_completion_status, new_sync_status, new_creation_date, new_last_modified, ical_prod_id, extra_parameters)
|
||||
Self::new_with_parameters(name, new_uid, new_item_id, new_completion_status, new_sync_status, new_creation_date, new_last_modified)
|
||||
}
|
||||
|
||||
/// Create a new Task instance, that may be synced on the server already
|
||||
pub fn new_with_parameters(name: String, uid: String, new_url: Url,
|
||||
/// Create a new Task instance, that may be synced 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>,
|
||||
ical_prod_id: String, extra_parameters: Vec<Property>,
|
||||
) -> Self
|
||||
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>) -> Self
|
||||
{
|
||||
Self {
|
||||
url: new_url,
|
||||
id,
|
||||
uid,
|
||||
name,
|
||||
completion_status,
|
||||
sync_status,
|
||||
creation_date,
|
||||
last_modified,
|
||||
ical_prod_id,
|
||||
extra_parameters,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url(&self) -> &Url { &self.url }
|
||||
pub fn id(&self) -> &ItemId { &self.id }
|
||||
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 {
|
||||
self.url == other.url
|
||||
&& self.uid == other.uid
|
||||
self.id == other.id
|
||||
&& self.name == other.name
|
||||
// sync status must be the same variant, but we ignore its embedded version tag
|
||||
&& std::mem::discriminant(&self.sync_status) == std::mem::discriminant(&other.sync_status)
|
||||
|
|
|
|||
|
|
@ -1,53 +1,44 @@
|
|||
//! Traits used by multiple structs in this crate
|
||||
|
||||
use std::error::Error;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use csscolorparser::Color;
|
||||
use url::Url;
|
||||
|
||||
use crate::item::SyncStatus;
|
||||
use crate::item::Item;
|
||||
use crate::item::ItemId;
|
||||
use crate::item::VersionTag;
|
||||
use crate::calendar::CalendarId;
|
||||
use crate::calendar::SupportedComponents;
|
||||
use crate::resource::Resource;
|
||||
|
||||
/// This trait must be implemented by data sources (either local caches or remote CalDAV clients)
|
||||
///
|
||||
/// Note that some concrete types (e.g. [`crate::cache::Cache`]) can also provide non-async versions of these functions
|
||||
#[async_trait]
|
||||
pub trait CalDavSource<T: BaseCalendar> {
|
||||
/// Returns the current calendars that this source contains
|
||||
/// This function may trigger an update (that can be a long process, or that can even fail, e.g. in case of a remote server)
|
||||
async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<T>>>, Box<dyn Error>>;
|
||||
/// Returns the calendar matching the URL
|
||||
async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<T>>>;
|
||||
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<T>>>, Box<dyn Error>>;
|
||||
/// 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, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>)
|
||||
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents)
|
||||
-> Result<Arc<Mutex<T>>, Box<dyn Error>>;
|
||||
|
||||
// Removing a calendar is not supported yet
|
||||
}
|
||||
|
||||
/// This trait contains functions that are common to all calendars
|
||||
///
|
||||
/// Note that some concrete types (e.g. [`crate::calendar::cached_calendar::CachedCalendar`]) can also provide non-async versions of these functions
|
||||
#[async_trait]
|
||||
pub trait BaseCalendar {
|
||||
/// Returns the calendar name
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Returns the calendar URL
|
||||
fn url(&self) -> &Url;
|
||||
/// Returns the calendar unique ID
|
||||
fn id(&self) -> &CalendarId;
|
||||
|
||||
/// 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
|
||||
|
|
@ -70,31 +61,25 @@ pub trait BaseCalendar {
|
|||
|
||||
|
||||
/// Functions availabe for calendars that are backed by a CalDAV server
|
||||
///
|
||||
/// Note that some concrete types (e.g. [`crate::calendar::cached_calendar::CachedCalendar`]) can also provide non-async versions of these functions
|
||||
#[async_trait]
|
||||
pub trait DavCalendar : BaseCalendar {
|
||||
/// Create a new calendar
|
||||
fn new(name: String, resource: Resource, supported_components: SupportedComponents, color: Option<Color>) -> Self;
|
||||
fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self;
|
||||
|
||||
/// Get the URLs and the version tags of every item in this calendar
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<Url, VersionTag>, Box<dyn Error>>;
|
||||
/// 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>>;
|
||||
|
||||
/// Returns a particular item
|
||||
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>>;
|
||||
|
||||
/// Returns a set of items.
|
||||
/// This is usually faster than calling multiple consecutive [`DavCalendar::get_item_by_url`], since it only issues one HTTP request.
|
||||
async fn get_items_by_url(&self, urls: &[Url]) -> Result<Vec<Option<Item>>, Box<dyn Error>>;
|
||||
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>>;
|
||||
|
||||
/// Delete an item
|
||||
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>>;
|
||||
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>>;
|
||||
|
||||
/// Get the URLs of all current items in this calendar
|
||||
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
||||
/// Get the IDs of all current items in this calendar
|
||||
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> {
|
||||
let items = self.get_item_version_tags().await?;
|
||||
Ok(items.iter()
|
||||
.map(|(url, _tag)| url.clone())
|
||||
.map(|(id, _tag)| id.clone())
|
||||
.collect())
|
||||
}
|
||||
|
||||
|
|
@ -106,33 +91,28 @@ pub trait DavCalendar : BaseCalendar {
|
|||
/// Functions availabe for calendars we have full knowledge of
|
||||
///
|
||||
/// Usually, these are local calendars fully backed by a local folder
|
||||
///
|
||||
/// Note that some concrete types (e.g. [`crate::calendar::cached_calendar::CachedCalendar`]) can also provide non-async versions of these functions
|
||||
#[async_trait]
|
||||
pub trait CompleteCalendar : BaseCalendar {
|
||||
/// Create a new calendar
|
||||
fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option<Color>) -> Self;
|
||||
fn new(name: String, id: CalendarId, supported_components: SupportedComponents) -> Self;
|
||||
|
||||
/// Get the URLs of all current items in this calendar
|
||||
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>>;
|
||||
/// 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
|
||||
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>>;
|
||||
|
||||
/// Returns all items that this calendar contains
|
||||
async fn get_items_mut(&mut self) -> Result<HashMap<Url, &mut Item>, Box<dyn Error>>;
|
||||
async fn get_items(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>>;
|
||||
|
||||
/// Returns a particular item
|
||||
async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item>;
|
||||
async fn get_item_by_id_ref<'a>(&'a self, id: &ItemId) -> Option<&'a Item>;
|
||||
|
||||
/// Returns a particular item
|
||||
async fn get_item_by_url_mut<'a>(&'a mut self, url: &Url) -> Option<&'a mut Item>;
|
||||
async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item>;
|
||||
|
||||
/// Mark an item for deletion.
|
||||
/// This is required so that the upcoming sync will know it should also also delete this task from the server
|
||||
/// (and then call [`CompleteCalendar::immediately_delete_item`] once it has been successfully deleted on the server)
|
||||
async fn mark_for_deletion(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>>;
|
||||
async fn mark_for_deletion(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>>;
|
||||
|
||||
/// Immediately remove an item. See [`CompleteCalendar::mark_for_deletion`]
|
||||
async fn immediately_delete_item(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>>;
|
||||
async fn immediately_delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Some utility functions
|
||||
///! Some utility functions
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
|
@ -6,10 +6,10 @@ use std::hash::Hash;
|
|||
use std::io::{stdin, stdout, Read, Write};
|
||||
|
||||
use minidom::Element;
|
||||
use url::Url;
|
||||
|
||||
use crate::traits::CompleteCalendar;
|
||||
use crate::traits::DavCalendar;
|
||||
use crate::calendar::CalendarId;
|
||||
use crate::Item;
|
||||
use crate::item::SyncStatus;
|
||||
|
||||
|
|
@ -62,12 +62,12 @@ pub fn print_xml(element: &Element) {
|
|||
}
|
||||
|
||||
/// A debug utility that pretty-prints calendars
|
||||
pub async fn print_calendar_list<C>(cals: &HashMap<Url, Arc<Mutex<C>>>)
|
||||
pub async fn print_calendar_list<C>(cals: &HashMap<CalendarId, Arc<Mutex<C>>>)
|
||||
where
|
||||
C: CompleteCalendar,
|
||||
{
|
||||
for (url, cal) in cals {
|
||||
println!("CAL {} ({})", cal.lock().unwrap().name(), url);
|
||||
for (id, cal) in cals {
|
||||
println!("CAL {} ({})", cal.lock().unwrap().name(), id);
|
||||
match cal.lock().unwrap().get_items().await {
|
||||
Err(_err) => continue,
|
||||
Ok(map) => {
|
||||
|
|
@ -80,17 +80,17 @@ where
|
|||
}
|
||||
|
||||
/// A debug utility that pretty-prints calendars
|
||||
pub async fn print_dav_calendar_list<C>(cals: &HashMap<Url, Arc<Mutex<C>>>)
|
||||
pub async fn print_dav_calendar_list<C>(cals: &HashMap<CalendarId, Arc<Mutex<C>>>)
|
||||
where
|
||||
C: DavCalendar,
|
||||
{
|
||||
for (url, cal) in cals {
|
||||
println!("CAL {} ({})", cal.lock().unwrap().name(), url);
|
||||
for (id, cal) in cals {
|
||||
println!("CAL {} ({})", cal.lock().unwrap().name(), id);
|
||||
match cal.lock().unwrap().get_item_version_tags().await {
|
||||
Err(_err) => continue,
|
||||
Ok(map) => {
|
||||
for (url, version_tag) in map {
|
||||
println!(" * {} (version {:?})", url, version_tag);
|
||||
for (id, version_tag) in map {
|
||||
println!(" * {} (version {:?})", id, version_tag);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ pub fn print_task(item: &Item) {
|
|||
SyncStatus::LocallyModified(_) => "~",
|
||||
SyncStatus::LocallyDeleted(_) => "x",
|
||||
};
|
||||
println!(" {}{} {}\t{}", completion, sync, task.name(), task.url());
|
||||
println!(" {}{} {}\t{}", completion, sync, task.name(), task.id());
|
||||
},
|
||||
_ => return,
|
||||
}
|
||||
|
|
@ -148,10 +148,3 @@ pub fn pause() {
|
|||
stdout.flush().unwrap();
|
||||
stdin().read_exact(&mut [0]).unwrap();
|
||||
}
|
||||
|
||||
|
||||
/// Generate a random URL with a given prefix
|
||||
pub fn random_url(parent_calendar: &Url) -> Url {
|
||||
let random = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
||||
parent_calendar.join(&random).unwrap(/* this cannot panic since we've just created a string that is a valid URL */)
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
//! 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
|
||||
//
|
||||
|
|
@ -11,10 +11,10 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::error::Error;
|
||||
use url::Url;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use kitchen_fridge::calendar::CalendarId;
|
||||
use kitchen_fridge::calendar::SupportedComponents;
|
||||
use kitchen_fridge::traits::CalDavSource;
|
||||
use kitchen_fridge::traits::BaseCalendar;
|
||||
|
|
@ -22,13 +22,13 @@ use kitchen_fridge::traits::CompleteCalendar;
|
|||
use kitchen_fridge::traits::DavCalendar;
|
||||
use kitchen_fridge::cache::Cache;
|
||||
use kitchen_fridge::Item;
|
||||
use kitchen_fridge::item::SyncStatus;
|
||||
use kitchen_fridge::ItemId;
|
||||
use kitchen_fridge::SyncStatus;
|
||||
use kitchen_fridge::Task;
|
||||
use kitchen_fridge::task::CompletionStatus;
|
||||
use kitchen_fridge::calendar::cached_calendar::CachedCalendar;
|
||||
use kitchen_fridge::provider::Provider;
|
||||
use kitchen_fridge::Provider;
|
||||
use kitchen_fridge::mock_behaviour::MockBehaviour;
|
||||
use kitchen_fridge::utils::random_url;
|
||||
|
||||
pub enum LocatedState {
|
||||
/// Item does not exist yet or does not exist anymore
|
||||
|
|
@ -44,7 +44,7 @@ pub enum LocatedState {
|
|||
pub struct ItemState {
|
||||
// TODO: if/when this crate supports Events as well, we could add such events here
|
||||
/// The calendar it is in
|
||||
calendar: Url,
|
||||
calendar: CalendarId,
|
||||
/// Its name
|
||||
name: String,
|
||||
/// Its completion status
|
||||
|
|
@ -54,15 +54,15 @@ pub struct ItemState {
|
|||
pub enum ChangeToApply {
|
||||
Rename(String),
|
||||
SetCompletion(bool),
|
||||
Create(Url, Item),
|
||||
Create(CalendarId, Item),
|
||||
/// "remove" means "mark for deletion" in the local calendar, or "immediately delete" on the remote calendar
|
||||
Remove,
|
||||
// ChangeCalendar(Url) is useless, as long as changing a calendar is implemented as "delete in one calendar and re-create it in another one"
|
||||
// ChangeCalendar(CalendarId) is useless, as long as changing a calendar is implemented as "delete in one calendar and re-create it in another one"
|
||||
}
|
||||
|
||||
|
||||
pub struct ItemScenario {
|
||||
url: Url,
|
||||
id: ItemId,
|
||||
initial_state: LocatedState,
|
||||
local_changes_to_apply: Vec<ChangeToApply>,
|
||||
remote_changes_to_apply: Vec<ChangeToApply>,
|
||||
|
|
@ -87,13 +87,13 @@ pub struct ItemScenario {
|
|||
pub fn scenarii_basic() -> Vec<ItemScenario> {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
let first_cal = Url::from("https://some.calend.ar/calendar-1/".parse().unwrap());
|
||||
let second_cal = Url::from("https://some.calend.ar/calendar-2/".parse().unwrap());
|
||||
let third_cal = Url::from("https://some.calend.ar/calendar-3/".parse().unwrap());
|
||||
let first_cal = CalendarId::from("https://some.calend.ar/calendar-1/".parse().unwrap());
|
||||
let second_cal = CalendarId::from("https://some.calend.ar/calendar-2/".parse().unwrap());
|
||||
let third_cal = CalendarId::from("https://some.calend.ar/calendar-3/".parse().unwrap());
|
||||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&first_cal),
|
||||
id: ItemId::random(&first_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: first_cal.clone(),
|
||||
name: String::from("Task A"),
|
||||
|
|
@ -111,7 +111,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&first_cal),
|
||||
id: ItemId::random(&first_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: first_cal.clone(),
|
||||
name: String::from("Task B"),
|
||||
|
|
@ -125,7 +125,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&first_cal),
|
||||
id: ItemId::random(&first_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: first_cal.clone(),
|
||||
name: String::from("Task C"),
|
||||
|
|
@ -139,7 +139,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&first_cal),
|
||||
id: ItemId::random(&first_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: first_cal.clone(),
|
||||
name: String::from("Task D"),
|
||||
|
|
@ -157,7 +157,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&first_cal),
|
||||
id: ItemId::random(&first_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: first_cal.clone(),
|
||||
name: String::from("Task E"),
|
||||
|
|
@ -175,7 +175,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&first_cal),
|
||||
id: ItemId::random(&first_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: first_cal.clone(),
|
||||
name: String::from("Task F"),
|
||||
|
|
@ -194,7 +194,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&second_cal),
|
||||
id: ItemId::random(&second_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: second_cal.clone(),
|
||||
name: String::from("Task G"),
|
||||
|
|
@ -212,7 +212,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&second_cal),
|
||||
id: ItemId::random(&second_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: second_cal.clone(),
|
||||
name: String::from("Task H"),
|
||||
|
|
@ -230,7 +230,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&second_cal),
|
||||
id: ItemId::random(&second_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: second_cal.clone(),
|
||||
name: String::from("Task I"),
|
||||
|
|
@ -249,7 +249,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&second_cal),
|
||||
id: ItemId::random(&second_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: second_cal.clone(),
|
||||
name: String::from("Task J"),
|
||||
|
|
@ -263,7 +263,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&second_cal),
|
||||
id: ItemId::random(&second_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: second_cal.clone(),
|
||||
name: String::from("Task K"),
|
||||
|
|
@ -281,7 +281,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&second_cal),
|
||||
id: ItemId::random(&second_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: second_cal.clone(),
|
||||
name: String::from("Task L"),
|
||||
|
|
@ -295,7 +295,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&second_cal),
|
||||
id: ItemId::random(&second_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: second_cal.clone(),
|
||||
name: String::from("Task M"),
|
||||
|
|
@ -313,7 +313,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&third_cal),
|
||||
id: ItemId::random(&third_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: third_cal.clone(),
|
||||
name: String::from("Task N"),
|
||||
|
|
@ -331,7 +331,7 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&third_cal),
|
||||
id: ItemId::random(&third_cal),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: third_cal.clone(),
|
||||
name: String::from("Task O"),
|
||||
|
|
@ -347,10 +347,10 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
}
|
||||
);
|
||||
|
||||
let url_p = random_url(&third_cal);
|
||||
let id_p = ItemId::random(&third_cal);
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: url_p.clone(),
|
||||
id: id_p.clone(),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: third_cal.clone(),
|
||||
name: String::from("Task P"),
|
||||
|
|
@ -369,18 +369,18 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
}
|
||||
);
|
||||
|
||||
let url_q = random_url(&third_cal);
|
||||
let id_q = ItemId::random(&third_cal);
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: url_q.clone(),
|
||||
id: id_q.clone(),
|
||||
initial_state: LocatedState::None,
|
||||
local_changes_to_apply: Vec::new(),
|
||||
remote_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task(
|
||||
Task::new_with_parameters(
|
||||
String::from("Task Q, created on the server"),
|
||||
url_q.to_string(), url_q,
|
||||
id_q.to_string(), id_q,
|
||||
CompletionStatus::Uncompleted,
|
||||
SyncStatus::random_synced(), Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() )
|
||||
SyncStatus::random_synced(), Some(Utc::now()), Utc::now() )
|
||||
))],
|
||||
after_sync: LocatedState::BothSynced( ItemState{
|
||||
calendar: third_cal.clone(),
|
||||
|
|
@ -390,17 +390,17 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
}
|
||||
);
|
||||
|
||||
let url_r = random_url(&third_cal);
|
||||
let id_r = ItemId::random(&third_cal);
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: url_r.clone(),
|
||||
id: id_r.clone(),
|
||||
initial_state: LocatedState::None,
|
||||
local_changes_to_apply: vec![ChangeToApply::Create(third_cal.clone(), Item::Task(
|
||||
Task::new_with_parameters(
|
||||
String::from("Task R, created locally"),
|
||||
url_r.to_string(), url_r,
|
||||
id_r.to_string(), id_r,
|
||||
CompletionStatus::Uncompleted,
|
||||
SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() )
|
||||
SyncStatus::NotSynced, Some(Utc::now()), Utc::now() )
|
||||
))],
|
||||
remote_changes_to_apply: Vec::new(),
|
||||
after_sync: LocatedState::BothSynced( ItemState{
|
||||
|
|
@ -418,12 +418,12 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
|||
pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
let cal1 = Url::from("https://some.calend.ar/first/".parse().unwrap());
|
||||
let cal2 = Url::from("https://some.calend.ar/second/".parse().unwrap());
|
||||
let cal1 = CalendarId::from("https://some.calend.ar/first/".parse().unwrap());
|
||||
let cal2 = CalendarId::from("https://some.calend.ar/second/".parse().unwrap());
|
||||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&cal1),
|
||||
id: ItemId::random(&cal1),
|
||||
initial_state: LocatedState::Remote( ItemState{
|
||||
calendar: cal1.clone(),
|
||||
name: String::from("Task A1"),
|
||||
|
|
@ -441,7 +441,7 @@ pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&cal2),
|
||||
id: ItemId::random(&cal2),
|
||||
initial_state: LocatedState::Remote( ItemState{
|
||||
calendar: cal2.clone(),
|
||||
name: String::from("Task A2"),
|
||||
|
|
@ -459,7 +459,7 @@ pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&cal1),
|
||||
id: ItemId::random(&cal1),
|
||||
initial_state: LocatedState::Remote( ItemState{
|
||||
calendar: cal1.clone(),
|
||||
name: String::from("Task B1"),
|
||||
|
|
@ -482,12 +482,12 @@ pub fn scenarii_first_sync_to_local() -> Vec<ItemScenario> {
|
|||
pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
let cal3 = Url::from("https://some.calend.ar/third/".parse().unwrap());
|
||||
let cal4 = Url::from("https://some.calend.ar/fourth/".parse().unwrap());
|
||||
let cal3 = CalendarId::from("https://some.calend.ar/third/".parse().unwrap());
|
||||
let cal4 = CalendarId::from("https://some.calend.ar/fourth/".parse().unwrap());
|
||||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&cal3),
|
||||
id: ItemId::random(&cal3),
|
||||
initial_state: LocatedState::Local( ItemState{
|
||||
calendar: cal3.clone(),
|
||||
name: String::from("Task A3"),
|
||||
|
|
@ -505,7 +505,7 @@ pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&cal4),
|
||||
id: ItemId::random(&cal4),
|
||||
initial_state: LocatedState::Local( ItemState{
|
||||
calendar: cal4.clone(),
|
||||
name: String::from("Task A4"),
|
||||
|
|
@ -523,7 +523,7 @@ pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
|
|||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&cal3),
|
||||
id: ItemId::random(&cal3),
|
||||
initial_state: LocatedState::Local( ItemState{
|
||||
calendar: cal3.clone(),
|
||||
name: String::from("Task B3"),
|
||||
|
|
@ -547,11 +547,11 @@ pub fn scenarii_first_sync_to_server() -> Vec<ItemScenario> {
|
|||
pub fn scenarii_transient_task() -> Vec<ItemScenario> {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
let cal = Url::from("https://some.calend.ar/transient/".parse().unwrap());
|
||||
let cal = CalendarId::from("https://some.calend.ar/transient/".parse().unwrap());
|
||||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: random_url(&cal),
|
||||
id: ItemId::random(&cal),
|
||||
initial_state: LocatedState::Local( ItemState{
|
||||
calendar: cal.clone(),
|
||||
name: String::from("A task, so that the calendar actually exists"),
|
||||
|
|
@ -567,19 +567,18 @@ pub fn scenarii_transient_task() -> Vec<ItemScenario> {
|
|||
}
|
||||
);
|
||||
|
||||
let url_transient = random_url(&cal);
|
||||
let id_transient = ItemId::random(&cal);
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
url: url_transient.clone(),
|
||||
id: id_transient.clone(),
|
||||
initial_state: LocatedState::None,
|
||||
local_changes_to_apply: vec![
|
||||
ChangeToApply::Create(cal, Item::Task(
|
||||
Task::new_with_parameters(
|
||||
String::from("A transient task that will be deleted before the sync"),
|
||||
url_transient.to_string(), url_transient,
|
||||
id_transient.to_string(), id_transient,
|
||||
CompletionStatus::Uncompleted,
|
||||
SyncStatus::NotSynced, Some(Utc::now()), Utc::now(),
|
||||
"prod_id".to_string(), Vec::new() )
|
||||
SyncStatus::NotSynced, Some(Utc::now()), Utc::now() )
|
||||
)),
|
||||
|
||||
ChangeToApply::Rename(String::from("A new name")),
|
||||
|
|
@ -637,13 +636,12 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc<M
|
|||
let new_item = Item::Task(
|
||||
Task::new_with_parameters(
|
||||
state.name.clone(),
|
||||
item.url.to_string(),
|
||||
item.url.clone(),
|
||||
item.id.to_string(),
|
||||
item.id.clone(),
|
||||
completion_status,
|
||||
sync_status,
|
||||
Some(now),
|
||||
now,
|
||||
"prod_id".to_string(), Vec::new(),
|
||||
));
|
||||
|
||||
match required_state {
|
||||
|
|
@ -667,54 +665,52 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc<M
|
|||
async fn apply_changes_on_provider(provider: &mut Provider<Cache, CachedCalendar, Cache, CachedCalendar>, scenarii: &[ItemScenario]) {
|
||||
// Apply changes to each item
|
||||
for item in scenarii {
|
||||
let initial_calendar_url = match &item.initial_state {
|
||||
let initial_calendar_id = match &item.initial_state {
|
||||
LocatedState::None => None,
|
||||
LocatedState::Local(state) => Some(state.calendar.clone()),
|
||||
LocatedState::Remote(state) => Some(state.calendar.clone()),
|
||||
LocatedState::BothSynced(state) => Some(state.calendar.clone()),
|
||||
};
|
||||
|
||||
let mut calendar_url = initial_calendar_url.clone();
|
||||
let mut calendar_id = initial_calendar_id.clone();
|
||||
for local_change in &item.local_changes_to_apply {
|
||||
calendar_url = Some(apply_change(provider.local(), calendar_url, &item.url, local_change, false).await);
|
||||
calendar_id = Some(apply_change(provider.local(), calendar_id, &item.id, local_change, false).await);
|
||||
}
|
||||
|
||||
let mut calendar_url = initial_calendar_url;
|
||||
let mut calendar_id = initial_calendar_id;
|
||||
for remote_change in &item.remote_changes_to_apply {
|
||||
calendar_url = Some(apply_change(provider.remote(), calendar_url, &item.url, remote_change, true).await);
|
||||
calendar_id = Some(apply_change(provider.remote(), calendar_id, &item.id, remote_change, true).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_or_insert_calendar(source: &mut Cache, url: &Url)
|
||||
async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId)
|
||||
-> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>>
|
||||
{
|
||||
match source.get_calendar(url).await {
|
||||
match source.get_calendar(id).await {
|
||||
Some(cal) => Ok(cal),
|
||||
None => {
|
||||
let new_name = format!("Test calendar for URL {}", url);
|
||||
let new_name = format!("Test calendar for ID {}", id);
|
||||
let supported_components = SupportedComponents::TODO;
|
||||
let color = csscolorparser::parse("#ff8000").unwrap(); // TODO: we should rather have specific colors, depending on the calendars
|
||||
|
||||
source.create_calendar(
|
||||
url.clone(),
|
||||
id.clone(),
|
||||
new_name.to_string(),
|
||||
supported_components,
|
||||
Some(color),
|
||||
).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a single change on a given source, and returns the calendar URL that was modified
|
||||
async fn apply_change<S, C>(source: &S, calendar_url: Option<Url>, item_url: &Url, change: &ChangeToApply, is_remote: bool) -> Url
|
||||
/// Apply a single change on a given source, and returns the calendar ID that was modified
|
||||
async fn apply_change<S, C>(source: &S, calendar_id: Option<CalendarId>, item_id: &ItemId, change: &ChangeToApply, is_remote: bool) -> CalendarId
|
||||
where
|
||||
S: CalDavSource<C>,
|
||||
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
||||
{
|
||||
match calendar_url {
|
||||
match calendar_id {
|
||||
Some(cal) => {
|
||||
apply_changes_on_an_existing_item(source, &cal, item_url, change, is_remote).await;
|
||||
apply_changes_on_an_existing_item(source, &cal, item_id, change, is_remote).await;
|
||||
cal
|
||||
},
|
||||
None => {
|
||||
|
|
@ -723,14 +719,14 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
async fn apply_changes_on_an_existing_item<S, C>(source: &S, calendar_url: &Url, item_url: &Url, change: &ChangeToApply, is_remote: bool)
|
||||
async fn apply_changes_on_an_existing_item<S, C>(source: &S, calendar_id: &CalendarId, item_id: &ItemId, change: &ChangeToApply, is_remote: bool)
|
||||
where
|
||||
S: CalDavSource<C>,
|
||||
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
||||
{
|
||||
let cal = source.get_calendar(calendar_url).await.unwrap();
|
||||
let cal = source.get_calendar(calendar_id).await.unwrap();
|
||||
let mut cal = cal.lock().unwrap();
|
||||
let task = cal.get_item_by_url_mut(item_url).await.unwrap().unwrap_task_mut();
|
||||
let task = cal.get_item_by_id_mut(item_id).await.unwrap().unwrap_task_mut();
|
||||
|
||||
match change {
|
||||
ChangeToApply::Rename(new_name) => {
|
||||
|
|
@ -753,18 +749,18 @@ where
|
|||
},
|
||||
ChangeToApply::Remove => {
|
||||
match is_remote {
|
||||
false => cal.mark_for_deletion(item_url).await.unwrap(),
|
||||
true => cal.delete_item(item_url).await.unwrap(),
|
||||
false => cal.mark_for_deletion(item_id).await.unwrap(),
|
||||
true => cal.delete_item(item_id).await.unwrap(),
|
||||
};
|
||||
},
|
||||
ChangeToApply::Create(_calendar_url, _item) => {
|
||||
ChangeToApply::Create(_calendar_id, _item) => {
|
||||
panic!("This function only handles already existing items");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an item, and returns the URL of the calendar it was inserted in
|
||||
async fn create_test_item<S, C>(source: &S, change: &ChangeToApply) -> Url
|
||||
/// Create an item, and returns the calendar ID it was inserted in
|
||||
async fn create_test_item<S, C>(source: &S, change: &ChangeToApply) -> CalendarId
|
||||
where
|
||||
S: CalDavSource<C>,
|
||||
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
||||
|
|
@ -775,10 +771,10 @@ where
|
|||
ChangeToApply::Remove => {
|
||||
panic!("This function only creates items that do not exist yet");
|
||||
}
|
||||
ChangeToApply::Create(calendar_url, item) => {
|
||||
let cal = source.get_calendar(calendar_url).await.unwrap();
|
||||
ChangeToApply::Create(calendar_id, item) => {
|
||||
let cal = source.get_calendar(calendar_id).await.unwrap();
|
||||
cal.lock().unwrap().add_item(item.clone()).await.unwrap();
|
||||
calendar_url.clone()
|
||||
calendar_id.clone()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,4 @@
|
|||
fn do_not_forget_to_run_tests_with_specific_features() {
|
||||
// This is just a reminder that there are tests that can be run only when cargo feature "integration_tests" is enabled.
|
||||
// See `sync.rs`
|
||||
// See also the CI configuration in the `.gitlab` folder
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ impl TestFlavour {
|
|||
Self {
|
||||
scenarii: scenarii::scenarii_basic(),
|
||||
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
|
||||
get_item_by_url_behaviour: (3,2),
|
||||
get_item_by_id_behaviour: (3,2),
|
||||
..MockBehaviour::default()
|
||||
})),
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ impl TestFlavour {
|
|||
scenarii: scenarii::scenarii_basic(),
|
||||
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
|
||||
add_item_behaviour: (2,3),
|
||||
get_item_by_url_behaviour: (1,12),
|
||||
get_item_by_id_behaviour: (1,4),
|
||||
..MockBehaviour::default()
|
||||
})),
|
||||
}
|
||||
|
|
@ -183,7 +183,7 @@ impl TestFlavour {
|
|||
delete_item_behaviour: (1,1),
|
||||
create_calendar_behaviour: (1,4),
|
||||
get_item_version_tags_behaviour: (3,1),
|
||||
get_item_by_url_behaviour: (0,41),
|
||||
get_item_by_id_behaviour: (0,41),
|
||||
..MockBehaviour::default()
|
||||
})),
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue