Compare commits
20
Commits
github_actions
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5018ae9b97 | ||
|
|
1be59891ca | ||
|
|
ac4d37d086 | ||
|
|
e765812fa9 | ||
|
|
96b8feff5e | ||
|
|
a8e5bfbc63 | ||
|
|
d28309b21d | ||
|
|
f6ae164dda | ||
|
|
c18516b9c8 | ||
|
|
5acb4e1c5b | ||
|
|
b8e945cf93 | ||
|
|
0022a8ffe7 | ||
|
|
e152b73c51 | ||
|
|
38a44e2773 | ||
|
|
0075ebfbdb | ||
|
|
2f7c14d0aa | ||
|
|
56b86adf02 | ||
|
|
91966b9d5c | ||
|
|
f4c0160f1e | ||
|
|
fbf3005a6f |
Generated
+183
-230
File diff suppressed because it is too large
Load Diff
+6
-7
@@ -1,12 +1,14 @@
|
||||
[package]
|
||||
name = "kitchen-fridge"
|
||||
version = "0.1.0"
|
||||
version = "0.3.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"
|
||||
keywords = ["CalDAV", "client", "WebDAV", "todo", "RFC4791"]
|
||||
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
|
||||
@@ -16,7 +18,7 @@ integration_tests = ["local_calendar_mocks_remote_calendars"]
|
||||
local_calendar_mocks_remote_calendars = []
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.8"
|
||||
env_logger = "0.9"
|
||||
log = "0.4"
|
||||
tokio = { version = "1.2", features = ["macros", "rt", "rt-multi-thread"]}
|
||||
reqwest = "0.11"
|
||||
@@ -28,11 +30,8 @@ serde_json = "1.0"
|
||||
async-trait = "0.1"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
sanitize-filename = "0.3"
|
||||
ical = { version = "0.6", features = ["serde-derive"] }
|
||||
ical-daladim = { version = "0.8", features = ["serde-derive"] }
|
||||
ics = "0.5"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
csscolorparser = { version = "0.5", features = ["serde"] }
|
||||
once_cell = "1.8"
|
||||
|
||||
[patch.crates-io]
|
||||
ical = { git = "https://github.com/daladim/ical-rs.git", branch = "ical_serde" }
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# kitchen-fridge
|
||||
|
||||
kitchen-fridge is a CalDAV (iCal file transfer over WebDAV) Rust library.
|
||||
<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).
|
||||
|
||||
+28
-21
@@ -3,13 +3,13 @@
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::{Utc};
|
||||
use url::Url;
|
||||
|
||||
use kitchen_fridge::{client::Client, traits::CalDavSource};
|
||||
use kitchen_fridge::calendar::{CalendarId, SupportedComponents};
|
||||
use kitchen_fridge::calendar::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;
|
||||
@@ -24,7 +24,6 @@ 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/";
|
||||
|
||||
@@ -35,8 +34,14 @@ async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
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!("Make sure you have edited the constants in this 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);
|
||||
println!(" * EXAMPLE_EXISTING_CALENDAR_URL = {}", EXAMPLE_EXISTING_CALENDAR_URL);
|
||||
println!(" * EXAMPLE_CREATED_CALENDAR_URL = {}", EXAMPLE_CREATED_CALENDAR_URL);
|
||||
pause();
|
||||
|
||||
let cache_path = Path::new(CACHE_FOLDER);
|
||||
@@ -56,6 +61,8 @@ async fn main() {
|
||||
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.");
|
||||
}
|
||||
@@ -74,27 +81,27 @@ async fn add_items_and_sync_again(provider: &mut CalDavProvider)
|
||||
pause();
|
||||
|
||||
// Create a new calendar...
|
||||
let new_calendar_id: CalendarId = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
|
||||
let new_calendar_url: Url = EXAMPLE_CREATED_CALENDAR_URL.parse().unwrap();
|
||||
let new_calendar_name = "A brave new calendar".to_string();
|
||||
if let Err(_err) = provider.local_mut()
|
||||
.create_calendar(new_calendar_id.clone(), new_calendar_name.clone(), SupportedComponents::TODO, None)
|
||||
.create_calendar(new_calendar_url.clone(), new_calendar_name.clone(), SupportedComponents::TODO, Some("#ff8000".parse().unwrap()))
|
||||
.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_id);
|
||||
provider.local().get_calendar(&new_calendar_id).await.unwrap()
|
||||
let new_task = Task::new(String::from(new_name), true, &new_calendar_url);
|
||||
provider.local().get_calendar(&new_calendar_url).await.unwrap()
|
||||
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
|
||||
|
||||
|
||||
// Also create a task in a previously existing calendar
|
||||
let changed_calendar_id: CalendarId = EXAMPLE_EXISTING_CALENDAR_URL.parse().unwrap();
|
||||
let changed_calendar_url: Url = 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_id);
|
||||
let new_id = new_task.id().clone();
|
||||
provider.local().get_calendar(&changed_calendar_id).await.unwrap()
|
||||
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()
|
||||
.lock().unwrap().add_item(Item::Task(new_task)).await.unwrap();
|
||||
|
||||
|
||||
@@ -105,20 +112,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_id, &new_id).await;
|
||||
complete_item_and_sync_again(provider, &changed_calendar_url, &new_url).await;
|
||||
}
|
||||
|
||||
async fn complete_item_and_sync_again(
|
||||
provider: &mut CalDavProvider,
|
||||
changed_calendar_id: &CalendarId,
|
||||
id_to_complete: &ItemId)
|
||||
changed_calendar_url: &Url,
|
||||
url_to_complete: &Url)
|
||||
{
|
||||
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_id).await.unwrap()
|
||||
.lock().unwrap().get_item_by_id_mut(id_to_complete).await.unwrap()
|
||||
provider.local().get_calendar(changed_calendar_url).await.unwrap()
|
||||
.lock().unwrap().get_item_by_url_mut(url_to_complete).await.unwrap()
|
||||
.unwrap_task_mut()
|
||||
.set_completion_status(completion_status);
|
||||
|
||||
@@ -129,19 +136,19 @@ async fn complete_item_and_sync_again(
|
||||
}
|
||||
provider.local().save_to_folder().unwrap();
|
||||
|
||||
remove_items_and_sync_again(provider, changed_calendar_id, id_to_complete).await;
|
||||
remove_items_and_sync_again(provider, changed_calendar_url, url_to_complete).await;
|
||||
}
|
||||
|
||||
async fn remove_items_and_sync_again(
|
||||
provider: &mut CalDavProvider,
|
||||
changed_calendar_id: &CalendarId,
|
||||
id_to_remove: &ItemId)
|
||||
changed_calendar_url: &Url,
|
||||
id_to_remove: &Url)
|
||||
{
|
||||
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_id).await.unwrap()
|
||||
provider.local().get_calendar(changed_calendar_url).await.unwrap()
|
||||
.lock().unwrap()
|
||||
.mark_for_deletion(id_to_remove).await.unwrap();
|
||||
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
<?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:#7a7a7a;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop846" />
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:0;"
|
||||
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="730.92011"
|
||||
inkscape:cy="461.7807"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer2"
|
||||
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></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 15 KiB |
+25
-23
@@ -10,12 +10,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")]
|
||||
@@ -41,7 +41,7 @@ pub struct Cache {
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
struct CachedData {
|
||||
#[serde(skip)]
|
||||
calendars: HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>,
|
||||
calendars: HashMap<Url, Arc<Mutex<CachedCalendar>>>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
@@ -86,7 +86,7 @@ impl Cache {
|
||||
continue;
|
||||
},
|
||||
Ok(cal) =>
|
||||
data.calendars.insert(cal.id().clone(), Arc::new(Mutex::new(cal))),
|
||||
data.calendars.insert(cal.url().clone(), Arc::new(Mutex::new(cal))),
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -131,8 +131,8 @@ impl Cache {
|
||||
serde_json::to_writer(file, &self.data)?;
|
||||
|
||||
// Save each calendar
|
||||
for (cal_id, cal_mutex) in &self.data.calendars {
|
||||
let file_name = sanitize_filename::sanitize(cal_id.as_str()) + ".cal";
|
||||
for (cal_url, cal_mutex) in &self.data.calendars {
|
||||
let file_name = sanitize_filename::sanitize(cal_url.as_str()) + ".cal";
|
||||
let cal_file = folder.join(file_name);
|
||||
let file = std::fs::File::create(&cal_file)?;
|
||||
let cal = cal_mutex.lock().unwrap();
|
||||
@@ -156,10 +156,10 @@ impl Cache {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
for (calendar_id, cal_l) in calendars_l {
|
||||
log::debug!("Comparing calendars {}", calendar_id);
|
||||
for (calendar_url, cal_l) in calendars_l {
|
||||
log::debug!("Comparing calendars {}", calendar_url);
|
||||
let cal_l = cal_l.lock().unwrap();
|
||||
let cal_r = match calendars_r.get(&calendar_id) {
|
||||
let cal_r = match calendars_r.get(&calendar_url) {
|
||||
Some(c) => c.lock().unwrap(),
|
||||
None => return Err("should not happen, we've just tested keys are the same".into()),
|
||||
};
|
||||
@@ -184,37 +184,39 @@ impl Drop for Cache {
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn get_calendars_sync(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
|
||||
/// 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>> {
|
||||
#[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(|(id, cal)| (id.clone(), cal.clone()))
|
||||
.map(|(url, cal)| (url.clone(), cal.clone()))
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_calendar_sync(&self, id: &CalendarId) -> Option<Arc<Mutex<CachedCalendar>>> {
|
||||
self.data.calendars.get(id).map(|arc| arc.clone())
|
||||
/// 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<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
|
||||
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.get_calendar_sync(id)
|
||||
async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<CachedCalendar>>> {
|
||||
self.get_calendar_sync(url)
|
||||
}
|
||||
|
||||
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> {
|
||||
log::debug!("Inserting local calendar {}", id);
|
||||
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);
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?;
|
||||
|
||||
let new_calendar = CachedCalendar::new(name, id.clone(), supported_components, color);
|
||||
let new_calendar = CachedCalendar::new(name, url.clone(), supported_components, color);
|
||||
let arc = Arc::new(Mutex::new(new_calendar));
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
@@ -222,7 +224,7 @@ impl CalDavSource<CachedCalendar> for Cache {
|
||||
arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour)));
|
||||
};
|
||||
|
||||
match self.data.calendars.insert(id, arc.clone()) {
|
||||
match self.data.calendars.insert(url, arc.clone()) {
|
||||
Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()),
|
||||
None => Ok(arc),
|
||||
}
|
||||
@@ -257,13 +259,13 @@ mod tests {
|
||||
|
||||
{
|
||||
let mut bucket_list = bucket_list.lock().unwrap();
|
||||
let cal_id = bucket_list.id().clone();
|
||||
let cal_url = bucket_list.url().clone();
|
||||
bucket_list.add_item(Item::Task(Task::new(
|
||||
String::from("Attend a concert of JS Bach"), false, &cal_id
|
||||
String::from("Attend a concert of JS Bach"), false, &cal_url
|
||||
))).await.unwrap();
|
||||
|
||||
bucket_list.add_item(Item::Task(Task::new(
|
||||
String::from("Climb the Lighthouse of Alexandria"), true, &cal_id
|
||||
String::from("Climb the Lighthouse of Alexandria"), true, &cal_url
|
||||
))).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -291,7 +293,7 @@ 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 id
|
||||
// We should not be able to add a second calendar with the same URL
|
||||
let second_addition_same_calendar = cache.create_calendar(
|
||||
Url::parse("https://caldav.com/shopping").unwrap(),
|
||||
"My shopping list".to_string(),
|
||||
|
||||
@@ -4,12 +4,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::{CalendarId, SupportedComponents};
|
||||
use crate::calendar::SupportedComponents;
|
||||
use crate::Item;
|
||||
use crate::item::ItemId;
|
||||
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -23,14 +23,14 @@ use crate::mock_behaviour::MockBehaviour;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CachedCalendar {
|
||||
name: String,
|
||||
id: CalendarId,
|
||||
url: Url,
|
||||
supported_components: SupportedComponents,
|
||||
color: Option<Color>,
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
#[serde(skip)]
|
||||
mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>,
|
||||
|
||||
items: HashMap<ItemId, Item>,
|
||||
items: HashMap<Url, Item>,
|
||||
}
|
||||
|
||||
impl CachedCalendar {
|
||||
@@ -65,7 +65,7 @@ impl CachedCalendar {
|
||||
fn regular_add_or_update_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
|
||||
let ss_clone = item.sync_status().clone();
|
||||
log::debug!("Adding or updating an item with {:?}", ss_clone);
|
||||
self.items.insert(item.id().clone(), item);
|
||||
self.items.insert(item.url().clone(), item);
|
||||
Ok(ss_clone)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ impl CachedCalendar {
|
||||
_ => item.set_sync_status(SyncStatus::random_synced()),
|
||||
};
|
||||
let ss_clone = item.sync_status().clone();
|
||||
self.items.insert(item.id().clone(), item);
|
||||
self.items.insert(item.url().clone(), item);
|
||||
Ok(ss_clone)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ 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.id != other.id
|
||||
|| self.url != other.url
|
||||
|| self.supported_components != other.supported_components
|
||||
|| self.color != other.color
|
||||
{
|
||||
@@ -102,13 +102,13 @@ impl CachedCalendar {
|
||||
log::debug!("Different keys for items");
|
||||
return Ok(false);
|
||||
}
|
||||
for (id_l, item_l) in items_l {
|
||||
let item_r = match items_r.get(&id_l) {
|
||||
for (url_l, item_l) in items_l {
|
||||
let item_r = match items_r.get(&url_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 id {}:", id_l);
|
||||
log::debug!("Different items for URL {}:", url_l);
|
||||
log::debug!("{:#?}", item_l);
|
||||
log::debug!("{:#?}", item_r);
|
||||
return Ok(false);
|
||||
@@ -118,36 +118,36 @@ impl CachedCalendar {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::get_item_ids`]
|
||||
pub fn get_item_ids_sync(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> {
|
||||
/// 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(|(id, _)| id.clone())
|
||||
.map(|(url, _)| url.clone())
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::get_items`]
|
||||
pub fn get_items_sync(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>> {
|
||||
pub fn get_items_sync(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>> {
|
||||
Ok(self.items.iter()
|
||||
.map(|(id, item)| (id.clone(), item))
|
||||
.map(|(url, item)| (url.clone(), item))
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::get_item_by_id`]
|
||||
pub fn get_item_by_id_sync<'a>(&'a self, id: &ItemId) -> Option<&'a Item> {
|
||||
self.items.get(id)
|
||||
/// 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_id_mut`]
|
||||
pub fn get_item_by_id_mut_sync<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> {
|
||||
self.items.get_mut(id)
|
||||
/// 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.id()) {
|
||||
return Err(format!("Item {:?} cannot be added, it exists already", item.id()).into());
|
||||
if self.items.contains_key(item.url()) {
|
||||
return Err(format!("Item {:?} cannot be added, it exists already", item.url()).into());
|
||||
}
|
||||
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))]
|
||||
return self.regular_add_or_update_item(item);
|
||||
@@ -158,8 +158,8 @@ impl CachedCalendar {
|
||||
|
||||
/// The non-async version of [`Self::update_item`]
|
||||
pub fn update_item_sync(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
|
||||
if self.items.contains_key(item.id()) == false {
|
||||
return Err(format!("Item {:?} cannot be updated, it does not already exist", item.id()).into());
|
||||
if self.items.contains_key(item.url()) == false {
|
||||
return Err(format!("Item {:?} cannot be updated, it does not already exist", item.url()).into());
|
||||
}
|
||||
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))]
|
||||
return self.regular_add_or_update_item(item);
|
||||
@@ -169,8 +169,8 @@ impl CachedCalendar {
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::mark_for_deletion`]
|
||||
pub fn mark_for_deletion_sync(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
|
||||
match self.items.get_mut(item_id) {
|
||||
pub fn mark_for_deletion_sync(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
match self.items.get_mut(item_url) {
|
||||
None => Err("no item for this key".into()),
|
||||
Some(item) => {
|
||||
match item.sync_status() {
|
||||
@@ -188,7 +188,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_id);
|
||||
self.items.remove(item_url);
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
@@ -197,9 +197,9 @@ impl CachedCalendar {
|
||||
}
|
||||
|
||||
/// The non-async version of [`Self::immediately_delete_item`]
|
||||
pub fn immediately_delete_item_sync(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
|
||||
match self.items.remove(item_id) {
|
||||
None => Err(format!("Item {} is absent from this calendar", item_id).into()),
|
||||
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()),
|
||||
Some(_) => Ok(())
|
||||
}
|
||||
}
|
||||
@@ -213,8 +213,8 @@ impl BaseCalendar for CachedCalendar {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn id(&self) -> &CalendarId {
|
||||
&self.id
|
||||
fn url(&self) -> &Url {
|
||||
&self.url
|
||||
}
|
||||
|
||||
fn supported_components(&self) -> SupportedComponents {
|
||||
@@ -236,37 +236,37 @@ impl BaseCalendar for CachedCalendar {
|
||||
|
||||
#[async_trait]
|
||||
impl CompleteCalendar for CachedCalendar {
|
||||
fn new(name: String, id: CalendarId, supported_components: SupportedComponents, color: Option<Color>) -> Self {
|
||||
fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option<Color>) -> Self {
|
||||
Self {
|
||||
name, id, supported_components, color,
|
||||
name, url, supported_components, color,
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
mock_behaviour: None,
|
||||
items: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>> {
|
||||
self.get_item_ids_sync()
|
||||
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
||||
self.get_item_urls_sync()
|
||||
}
|
||||
|
||||
async fn get_items(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>> {
|
||||
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>> {
|
||||
self.get_items_sync()
|
||||
}
|
||||
|
||||
async fn get_item_by_id<'a>(&'a self, id: &ItemId) -> Option<&'a Item> {
|
||||
self.get_item_by_id_sync(id)
|
||||
async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item> {
|
||||
self.get_item_by_url_sync(url)
|
||||
}
|
||||
|
||||
async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item> {
|
||||
self.get_item_by_id_mut_sync(id)
|
||||
async fn 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_id: &ItemId) -> Result<(), Box<dyn Error>> {
|
||||
self.mark_for_deletion_sync(item_id)
|
||||
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_id: &ItemId) -> Result<(), Box<dyn Error>> {
|
||||
self.immediately_delete_item_sync(item_id)
|
||||
async fn immediately_delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
self.immediately_delete_item_sync(item_url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ impl DavCalendar for CachedCalendar {
|
||||
crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components, color)
|
||||
}
|
||||
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> {
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<Url, 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())?;
|
||||
|
||||
@@ -294,30 +294,30 @@ impl DavCalendar for CachedCalendar {
|
||||
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for (id, item) in self.items.iter() {
|
||||
for (url, 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(id.clone(), vt);
|
||||
result.insert(url.clone(), vt);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>> {
|
||||
async fn get_item_by_url(&self, url: &Url) -> 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_id())?;
|
||||
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_url())?;
|
||||
|
||||
Ok(self.items.get(id).cloned())
|
||||
Ok(self.items.get(url).cloned())
|
||||
}
|
||||
|
||||
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
|
||||
async fn delete_item(&mut self, item_url: &Url) -> 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_id).await
|
||||
self.immediately_delete_item(item_url).await
|
||||
}
|
||||
}
|
||||
|
||||
+4
-7
@@ -23,12 +23,12 @@ bitflags! {
|
||||
impl SupportedComponents {
|
||||
pub fn to_xml_string(&self) -> String {
|
||||
format!(r#"
|
||||
<C:supported-calendar-component-set>
|
||||
<B:supported-calendar-component-set>
|
||||
{} {}
|
||||
</C:supported-calendar-component-set>
|
||||
</B:supported-calendar-component-set>
|
||||
"#,
|
||||
if self.contains(Self::EVENT) { "<C:comp name=\"VEVENT\"/>" } else { "" },
|
||||
if self.contains(Self::TODO) { "<C:comp name=\"VTODO\"/>" } else { "" },
|
||||
if self.contains(Self::EVENT) { "<B:comp name=\"VEVENT\"/>" } else { "" },
|
||||
if self.contains(Self::TODO) { "<B:comp name=\"VTODO\"/>" } else { "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,3 @@ impl Default for SearchFilter {
|
||||
SearchFilter::All
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub type CalendarId = url::Url;
|
||||
|
||||
@@ -5,13 +5,12 @@ 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;
|
||||
@@ -40,13 +39,13 @@ pub struct RemoteCalendar {
|
||||
supported_components: SupportedComponents,
|
||||
color: Option<Color>,
|
||||
|
||||
cached_version_tags: Mutex<Option<HashMap<ItemId, VersionTag>>>,
|
||||
cached_version_tags: Mutex<Option<HashMap<Url, VersionTag>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BaseCalendar for RemoteCalendar {
|
||||
fn name(&self) -> &str { &self.name }
|
||||
fn id(&self) -> &CalendarId { &self.resource.url() }
|
||||
fn url(&self) -> &Url { &self.resource.url() }
|
||||
fn supported_components(&self) -> crate::calendar::SupportedComponents {
|
||||
self.supported_components
|
||||
}
|
||||
@@ -58,7 +57,7 @@ impl BaseCalendar for RemoteCalendar {
|
||||
let ical_text = crate::ical::build_from(&item)?;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.put(item.id().as_url().clone())
|
||||
.put(item.url().clone())
|
||||
.header("If-None-Match", "*")
|
||||
.header(CONTENT_TYPE, "text/calendar")
|
||||
.header(CONTENT_LENGTH, ical_text.len())
|
||||
@@ -73,7 +72,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.id()).into()),
|
||||
None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.url()).into()),
|
||||
Some(etag) => {
|
||||
let vtag_str = etag.to_str()?;
|
||||
let vtag = VersionTag::from(String::from(vtag_str));
|
||||
@@ -92,7 +91,7 @@ impl BaseCalendar for RemoteCalendar {
|
||||
let ical_text = crate::ical::build_from(&item)?;
|
||||
|
||||
let request = reqwest::Client::new()
|
||||
.put(item.id().as_url().clone())
|
||||
.put(item.url().clone())
|
||||
.header("If-Match", old_etag.as_str())
|
||||
.header(CONTENT_TYPE, "text/calendar")
|
||||
.header(CONTENT_LENGTH, ical_text.len())
|
||||
@@ -107,7 +106,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.id()).into()),
|
||||
None => Err(format!("No ETag in these response headers: {:?} (request was {:?})", reply_hdrs, item.url()).into()),
|
||||
Some(etag) => {
|
||||
let vtag_str = etag.to_str()?;
|
||||
let vtag = VersionTag::from(String::from(vtag_str));
|
||||
@@ -127,7 +126,7 @@ impl DavCalendar for RemoteCalendar {
|
||||
}
|
||||
|
||||
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> {
|
||||
async fn get_item_version_tags(&self) -> Result<HashMap<Url, 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());
|
||||
@@ -139,19 +138,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_id = match item_url {
|
||||
let item_url = match item_url {
|
||||
None => {
|
||||
log::warn!("Unable to extract HREF");
|
||||
continue;
|
||||
},
|
||||
Some(resource) => {
|
||||
ItemId::from(&resource)
|
||||
resource.url().clone()
|
||||
},
|
||||
};
|
||||
|
||||
let version_tag = match crate::utils::find_elem(&response, "getetag") {
|
||||
None => {
|
||||
log::warn!("Unable to extract ETAG for item {}, ignoring it", item_id);
|
||||
log::warn!("Unable to extract ETAG for item {}, ignoring it", item_url);
|
||||
continue;
|
||||
},
|
||||
Some(etag) => {
|
||||
@@ -159,7 +158,7 @@ impl DavCalendar for RemoteCalendar {
|
||||
}
|
||||
};
|
||||
|
||||
items.insert(item_id, version_tag);
|
||||
items.insert(item_url.clone(), 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)
|
||||
@@ -167,9 +166,9 @@ impl DavCalendar for RemoteCalendar {
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>> {
|
||||
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>> {
|
||||
let res = reqwest::Client::new()
|
||||
.get(id.as_url().clone())
|
||||
.get(url.clone())
|
||||
.header(CONTENT_TYPE, "text/calendar")
|
||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||
.send()
|
||||
@@ -183,18 +182,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(id) {
|
||||
None => return Err(format!("Inconsistent data: {} has no version tag", id).into()),
|
||||
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(&text, id.clone(), SyncStatus::Synced(vt.clone()))?;
|
||||
let item = crate::ical::parse(&text, url.clone(), SyncStatus::Synced(vt.clone()))?;
|
||||
Ok(Some(item))
|
||||
}
|
||||
|
||||
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
|
||||
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
|
||||
let del_response = reqwest::Client::new()
|
||||
.delete(item_id.as_url().clone())
|
||||
.delete(item_url.clone())
|
||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
+25
-21
@@ -15,7 +15,6 @@ 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;
|
||||
@@ -113,7 +112,7 @@ pub struct Client {
|
||||
struct CachedReplies {
|
||||
principal: Option<Resource>,
|
||||
calendar_home_set: Option<Resource>,
|
||||
calendars: Option<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>>,
|
||||
calendars: Option<HashMap<Url, Arc<Mutex<RemoteCalendar>>>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -216,7 +215,7 @@ impl Client {
|
||||
|
||||
let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color);
|
||||
log::info!("Found calendar {}", this_calendar.name());
|
||||
calendars.insert(this_calendar.id().clone(), Arc::new(Mutex::new(this_calendar)));
|
||||
calendars.insert(this_calendar.url().clone(), Arc::new(Mutex::new(this_calendar)));
|
||||
}
|
||||
|
||||
let mut replies = self.cached_replies.lock().unwrap();
|
||||
@@ -228,7 +227,7 @@ impl Client {
|
||||
|
||||
#[async_trait]
|
||||
impl CalDavSource<RemoteCalendar> for Client {
|
||||
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
|
||||
async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
|
||||
self.populate_calendars().await?;
|
||||
|
||||
match &self.cached_replies.lock().unwrap().calendars {
|
||||
@@ -239,7 +238,7 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||
};
|
||||
}
|
||||
|
||||
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<RemoteCalendar>>> {
|
||||
async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<RemoteCalendar>>> {
|
||||
if let Err(err) = self.populate_calendars().await {
|
||||
log::warn!("Unable to fetch calendars: {}", err);
|
||||
return None;
|
||||
@@ -248,26 +247,26 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||
self.cached_replies.lock().unwrap()
|
||||
.calendars
|
||||
.as_ref()
|
||||
.and_then(|cals| cals.get(id))
|
||||
.and_then(|cals| cals.get(url))
|
||||
.map(|cal| cal.clone())
|
||||
}
|
||||
|
||||
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
|
||||
async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
|
||||
self.populate_calendars().await?;
|
||||
|
||||
match self.cached_replies.lock().unwrap().calendars.as_ref() {
|
||||
None => return Err("No calendars have been fetched".into()),
|
||||
Some(cals) => {
|
||||
if cals.contains_key(&id) {
|
||||
if cals.contains_key(&url) {
|
||||
return Err("This calendar already exists".into());
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
let creation_body = calendar_body(name, supported_components);
|
||||
let creation_body = calendar_body(name, supported_components, color);
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.request(Method::from_bytes(b"MKCALENDAR").unwrap(), id.clone())
|
||||
.request(Method::from_bytes(b"MKCALENDAR").unwrap(), url.clone())
|
||||
.header(CONTENT_TYPE, "application/xml")
|
||||
.basic_auth(self.resource.username(), Some(self.resource.password()))
|
||||
.body(creation_body)
|
||||
@@ -279,25 +278,30 @@ impl CalDavSource<RemoteCalendar> for Client {
|
||||
return Err(format!("Unexpected HTTP status code. Expected CREATED, got {}", status.as_u16()).into());
|
||||
}
|
||||
|
||||
self.get_calendar(&id).await.ok_or(format!("Unable to insert calendar {:?}", id).into())
|
||||
self.get_calendar(&url).await.ok_or(format!("Unable to insert calendar {:?}", url).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn calendar_body(name: String, supported_components: SupportedComponents) -> String {
|
||||
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()),
|
||||
};
|
||||
|
||||
// This is taken from https://tools.ietf.org/html/rfc4791#page-24
|
||||
format!(r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:mkcalendar xmlns:D="DAV:"
|
||||
xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:set>
|
||||
<D:prop>
|
||||
<D:displayname>{}</D:displayname>
|
||||
<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
|
||||
<A:set xmlns:A="DAV:">
|
||||
<A:prop>
|
||||
<A:displayname>{}</A:displayname>
|
||||
{}
|
||||
</D:prop>
|
||||
</D:set>
|
||||
</C:mkcalendar>
|
||||
{}
|
||||
</A:prop>
|
||||
</A:set>
|
||||
</B:mkcalendar>
|
||||
"#,
|
||||
name,
|
||||
color_property,
|
||||
supported_components.to_xml_string(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -1,11 +1,12 @@
|
||||
//! Support for compile-time 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`)
|
||||
/// You can override it at compile-time with the `KITCHEN_FRIDGE_ICAL_ORG_NAME` environment variable, or keep the default value
|
||||
pub static ORG_NAME: Lazy<String> = Lazy::new(|| option_env!("KITCHEN_FRIDGE_ICAL_ORG_NAME").unwrap_or("My organization").to_string() );
|
||||
/// Part of the ProdID string that describes the 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`)
|
||||
/// You can override it at compile-time with the `KITCHEN_FRIDGE_ICAL_PRODUCT_NAME` environment variable, or keep the default value
|
||||
pub static PRODUCT_NAME: Lazy<String> = Lazy::new(|| option_env!("KITCHEN_FRIDGE_ICAL_PRODUCT_NAME").unwrap_or("KitchenFridge").to_string() );
|
||||
/// 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())));
|
||||
|
||||
+5
-5
@@ -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 {
|
||||
id: ItemId,
|
||||
uid: String,
|
||||
name: String,
|
||||
sync_status: SyncStatus,
|
||||
}
|
||||
@@ -20,12 +20,12 @@ impl Event {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &ItemId {
|
||||
&self.id
|
||||
pub fn url(&self) -> &Url {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
pub fn uid(&self) -> &str {
|
||||
unimplemented!()
|
||||
&self.uid
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
|
||||
+4
-4
@@ -104,7 +104,7 @@ mod tests {
|
||||
COMPLETED:{}\r\n\
|
||||
STATUS:COMPLETED\r\n\
|
||||
END:VTODO\r\n\
|
||||
END:VCALENDAR\r\n", *ORG_NAME, *PRODUCT_NAME, uid, s_now, s_now, s_now, s_now);
|
||||
END:VCALENDAR\r\n", ORG_NAME.lock().unwrap(), PRODUCT_NAME.lock().unwrap(), uid, s_now, s_now, s_now, s_now);
|
||||
|
||||
assert_eq!(ical, expected_ical);
|
||||
}
|
||||
@@ -124,18 +124,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, *PRODUCT_NAME, uid, s_now, s_now, s_now);
|
||||
END:VCALENDAR\r\n", ORG_NAME.lock().unwrap(), PRODUCT_NAME.lock().unwrap(), uid, s_now, s_now, s_now);
|
||||
|
||||
assert_eq!(ical, expected_ical);
|
||||
}
|
||||
|
||||
fn build_task(completed: bool) -> (String, String, String) {
|
||||
let cal_id = "http://my.calend.ar/id".parse().unwrap();
|
||||
let cal_url = "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_id
|
||||
String::from("This is a task with ÜTF-8 characters"), completed, &cal_url
|
||||
));
|
||||
|
||||
let ical = build_from(&task).unwrap();
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ pub use builder::build_from;
|
||||
use crate::config::{ORG_NAME, PRODUCT_NAME};
|
||||
|
||||
pub fn default_prod_id() -> String {
|
||||
format!("-//{}//{}//EN", *ORG_NAME, *PRODUCT_NAME)
|
||||
format!("-//{}//{}//EN", ORG_NAME.lock().unwrap(), PRODUCT_NAME.lock().unwrap())
|
||||
}
|
||||
|
||||
|
||||
|
||||
+17
-17
@@ -4,22 +4,22 @@ 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_id: ItemId, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> {
|
||||
pub fn parse(content: &str, item_url: Url, 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_id).into()),
|
||||
None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()),
|
||||
Some(item) => match item {
|
||||
Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_id, err).into()),
|
||||
Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()),
|
||||
Ok(item) => item,
|
||||
}
|
||||
};
|
||||
@@ -80,15 +80,15 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||
}
|
||||
let name = match name {
|
||||
Some(name) => name,
|
||||
None => return Err(format!("Missing name for item {}", item_id).into()),
|
||||
None => return Err(format!("Missing name for item {}", item_url).into()),
|
||||
};
|
||||
let uid = match uid {
|
||||
Some(uid) => uid,
|
||||
None => return Err(format!("Missing UID for item {}", item_id).into()),
|
||||
None => return Err(format!("Missing UID for item {}", item_url).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_id).into()),
|
||||
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_url).into()),
|
||||
};
|
||||
let completion_status = match completed {
|
||||
false => {
|
||||
@@ -100,7 +100,7 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||
true => CompletionStatus::Completed(completion_date),
|
||||
};
|
||||
|
||||
Item::Task(Task::new_with_parameters(name, uid, item_id, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters))
|
||||
Item::Task(Task::new_with_parameters(name, uid, item_url, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters))
|
||||
},
|
||||
};
|
||||
|
||||
@@ -244,13 +244,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_id: ItemId = "http://some.id/for/testing".parse().unwrap();
|
||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||
|
||||
let item = parse(EXAMPLE_ICAL, item_id.clone(), sync_status.clone()).unwrap();
|
||||
let item = parse(EXAMPLE_ICAL, item_url.clone(), sync_status.clone()).unwrap();
|
||||
let task = item.unwrap_task();
|
||||
|
||||
assert_eq!(task.name(), "Do not forget to do this");
|
||||
assert_eq!(task.id(), &item_id);
|
||||
assert_eq!(task.url(), &item_url);
|
||||
assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com");
|
||||
assert_eq!(task.completed(), false);
|
||||
assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted);
|
||||
@@ -262,9 +262,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_id: ItemId = "http://some.id/for/testing".parse().unwrap();
|
||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||
|
||||
let item = parse(EXAMPLE_ICAL_COMPLETED, item_id.clone(), sync_status.clone()).unwrap();
|
||||
let item = parse(EXAMPLE_ICAL_COMPLETED, item_url.clone(), sync_status.clone()).unwrap();
|
||||
let task = item.unwrap_task();
|
||||
|
||||
assert_eq!(task.completed(), true);
|
||||
@@ -275,9 +275,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_id: ItemId = "http://some.id/for/testing".parse().unwrap();
|
||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||
|
||||
let item = parse(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_id.clone(), sync_status.clone()).unwrap();
|
||||
let item = parse(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_url.clone(), sync_status.clone()).unwrap();
|
||||
let task = item.unwrap_task();
|
||||
|
||||
assert_eq!(task.completed(), true);
|
||||
@@ -288,9 +288,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_id: ItemId = "http://some.id/for/testing".parse().unwrap();
|
||||
let item_url: Url = "http://some.id/for/testing".parse().unwrap();
|
||||
|
||||
let item = parse(EXAMPLE_MULTIPLE_ICAL, item_id.clone(), sync_status.clone());
|
||||
let item = parse(EXAMPLE_MULTIPLE_ICAL, item_url.clone(), sync_status.clone());
|
||||
assert!(item.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+3
-71
@@ -1,17 +1,10 @@
|
||||
//! CalDAV items (todo, events, journals...)
|
||||
// TODO: move Event and Task to nest them in crate::items::calendar::Calendar?
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::resource::Resource;
|
||||
use crate::calendar::CalendarId;
|
||||
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Item {
|
||||
@@ -32,7 +25,7 @@ macro_rules! synthetise_common_getter {
|
||||
}
|
||||
|
||||
impl Item {
|
||||
synthetise_common_getter!(id, &ItemId);
|
||||
synthetise_common_getter!(url, &Url);
|
||||
synthetise_common_getter!(uid, &str);
|
||||
synthetise_common_getter!(name, &str);
|
||||
synthetise_common_getter!(creation_date, Option<&DateTime<Utc>>);
|
||||
@@ -94,67 +87,6 @@ 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.
|
||||
@@ -175,7 +107,7 @@ impl VersionTag {
|
||||
&self.tag
|
||||
}
|
||||
|
||||
/// Generate a random VesionTag
|
||||
/// Generate a random VersionTag
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
pub fn random() -> Self {
|
||||
let random = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
||||
|
||||
+12
-4
@@ -1,6 +1,8 @@
|
||||
//! This crate provides a way to manage CalDAV data.
|
||||
//! 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.
|
||||
//!
|
||||
//! 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. \
|
||||
//! This initial implementation only supports TODO events. This 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. \
|
||||
//! 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
|
||||
@@ -12,14 +14,20 @@
|
||||
//!
|
||||
//! 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.
|
||||
//! 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).
|
||||
//!
|
||||
//! 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 `tasklist`, a GUI app that uses `kitchen-fridge` under the hood.
|
||||
//! 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")]
|
||||
|
||||
pub mod traits;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ pub struct MockBehaviour {
|
||||
|
||||
// From the DavCalendar trait
|
||||
pub get_item_version_tags_behaviour: (u32, u32),
|
||||
pub get_item_by_id_behaviour: (u32, u32),
|
||||
pub get_item_by_url_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_id_behaviour: (0, n_fails),
|
||||
get_item_by_url_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_id(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
pub fn can_get_item_by_url(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
if self.is_suspended { return Ok(()) }
|
||||
decrement(&mut self.get_item_by_id_behaviour, "get_item_by_id")
|
||||
decrement(&mut self.get_item_by_url_behaviour, "get_item_by_url")
|
||||
}
|
||||
pub fn can_delete_item(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
if self.is_suspended { return Ok(()) }
|
||||
|
||||
+93
-93
@@ -6,11 +6,11 @@ use std::error::Error;
|
||||
use std::collections::HashSet;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use url::Url;
|
||||
|
||||
use crate::traits::{BaseCalendar, CalDavSource, DavCalendar};
|
||||
use crate::traits::CompleteCalendar;
|
||||
use crate::item::{ItemId, SyncStatus};
|
||||
use crate::calendar::CalendarId;
|
||||
use crate::item::SyncStatus;
|
||||
|
||||
pub mod sync_progress;
|
||||
use sync_progress::SyncProgress;
|
||||
@@ -102,39 +102,39 @@ where
|
||||
|
||||
// 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 {
|
||||
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_id, 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_id, err));
|
||||
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err));
|
||||
continue;
|
||||
}
|
||||
handled_calendars.insert(cal_id);
|
||||
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_id, cal_local) in cals_local {
|
||||
if handled_calendars.contains(&cal_id) {
|
||||
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_id, cal_local.clone()).await {
|
||||
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_id, 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_id, err));
|
||||
progress.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_url, err));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -145,11 +145,11 @@ where
|
||||
}
|
||||
|
||||
|
||||
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_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_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 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
|
||||
}
|
||||
|
||||
|
||||
@@ -179,52 +179,52 @@ where
|
||||
details: format!("{} remote items", remote_items.len()),
|
||||
});
|
||||
|
||||
let mut local_items_to_handle = cal_local.get_item_ids().await?;
|
||||
for (id, remote_tag) in remote_items {
|
||||
progress.trace(&format!("***** Considering remote item {}...", id));
|
||||
match cal_local.get_item_by_id(&id).await {
|
||||
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", id));
|
||||
remote_additions.insert(id);
|
||||
progress.debug(&format!("* {} is a remote addition", url));
|
||||
remote_additions.insert(url);
|
||||
},
|
||||
Some(local_item) => {
|
||||
if local_items_to_handle.remove(&id) == false {
|
||||
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", id));
|
||||
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!("ID reuse between remote and local sources ({}). Ignoring this item in the sync", id));
|
||||
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", id));
|
||||
remote_changes.insert(id);
|
||||
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", id));
|
||||
local_changes.insert(id);
|
||||
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.", id));
|
||||
progress.debug(&format!("* {} is considered a remote change", id));
|
||||
remote_changes.insert(id);
|
||||
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", id));
|
||||
local_del.insert(id);
|
||||
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.", id));
|
||||
progress.debug(&format!("* {} is a considered a remote change", id));
|
||||
remote_changes.insert(id);
|
||||
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);
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -233,11 +233,11 @@ where
|
||||
}
|
||||
|
||||
// Also iterate on the local tasks that are not on the remote
|
||||
for id in local_items_to_handle {
|
||||
progress.trace(&format!("##### Considering local item {}...", id));
|
||||
let local_item = match cal_local.get_item_by_id(&id).await {
|
||||
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", id));
|
||||
progress.error(&format!("Inconsistent state: missing task {} from the local tasks", url));
|
||||
continue;
|
||||
},
|
||||
Some(item) => item,
|
||||
@@ -246,22 +246,22 @@ where
|
||||
match local_item.sync_status() {
|
||||
SyncStatus::Synced(_) => {
|
||||
// This item has been removed from the remote
|
||||
progress.debug(&format!("# {} is a deletion from the server", id));
|
||||
remote_del.insert(id);
|
||||
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", id));
|
||||
local_additions.insert(id);
|
||||
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", id));
|
||||
remote_del.insert(id);
|
||||
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", id));
|
||||
remote_del.insert(id);
|
||||
progress.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", url));
|
||||
remote_del.insert(url);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -269,80 +269,80 @@ where
|
||||
|
||||
// Step 2 - commit changes
|
||||
progress.trace("Committing changes...");
|
||||
for id_del in local_del {
|
||||
progress.debug(&format!("> Pushing local deletion {} to the server", id_del));
|
||||
for url_del in local_del {
|
||||
progress.debug(&format!("> Pushing local deletion {} to the server", url_del));
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
details: Self::item_name(&cal_local, &id_del).await,
|
||||
details: Self::item_name(&cal_local, &url_del).await,
|
||||
});
|
||||
match cal_remote.delete_item(&id_del).await {
|
||||
match cal_remote.delete_item(&url_del).await {
|
||||
Err(err) => {
|
||||
progress.warn(&format!("Unable to delete remote item {}: {}", id_del, 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(&id_del).await {
|
||||
progress.error(&format!("Unable to permanently delete local item {}: {}", id_del, err));
|
||||
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 id_del in remote_del {
|
||||
progress.debug(&format!("> Applying remote deletion {} locally", id_del));
|
||||
for url_del in remote_del {
|
||||
progress.debug(&format!("> Applying remote deletion {} locally", url_del));
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
details: Self::item_name(&cal_local, &id_del).await,
|
||||
details: Self::item_name(&cal_local, &url_del).await,
|
||||
});
|
||||
if let Err(err) = cal_local.immediately_delete_item(&id_del).await {
|
||||
progress.warn(&format!("Unable to delete local item {}: {}", id_del, err));
|
||||
if let Err(err) = cal_local.immediately_delete_item(&url_del).await {
|
||||
progress.warn(&format!("Unable to delete local item {}: {}", url_del, err));
|
||||
}
|
||||
}
|
||||
|
||||
for id_add in remote_additions {
|
||||
progress.debug(&format!("> Applying remote addition {} locally", id_add));
|
||||
for url_add in remote_additions {
|
||||
progress.debug(&format!("> Applying remote addition {} locally", url_add));
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
details: Self::item_name(&cal_local, &id_add).await,
|
||||
details: Self::item_name(&cal_local, &url_add).await,
|
||||
});
|
||||
match cal_remote.get_item_by_id(&id_add).await {
|
||||
match cal_remote.get_item_by_url(&url_add).await {
|
||||
Err(err) => {
|
||||
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it.", id_add, err));
|
||||
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it.", url_add, err));
|
||||
continue;
|
||||
},
|
||||
Ok(item) => match item {
|
||||
None => {
|
||||
progress.error(&format!("Inconsistency: new item {} has vanished from the remote end", id_add));
|
||||
progress.error(&format!("Inconsistency: new item {} has vanished from the remote end", url_add));
|
||||
continue;
|
||||
},
|
||||
Some(new_item) => {
|
||||
if let Err(err) = cal_local.add_item(new_item.clone()).await {
|
||||
progress.error(&format!("Not able to add item {} to local calendar: {}", id_add, err));
|
||||
progress.error(&format!("Not able to add item {} to local calendar: {}", url_add, err));
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for id_change in remote_changes {
|
||||
progress.debug(&format!("> Applying remote change {} locally", id_change));
|
||||
for url_change in remote_changes {
|
||||
progress.debug(&format!("> Applying remote change {} locally", url_change));
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
details: Self::item_name(&cal_local, &id_change).await,
|
||||
details: Self::item_name(&cal_local, &url_change).await,
|
||||
});
|
||||
match cal_remote.get_item_by_id(&id_change).await {
|
||||
match cal_remote.get_item_by_url(&url_change).await {
|
||||
Err(err) => {
|
||||
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it", id_change, err));
|
||||
progress.warn(&format!("Unable to get remote item {}: {}. Skipping it", url_change, err));
|
||||
continue;
|
||||
},
|
||||
Ok(item) => match item {
|
||||
None => {
|
||||
progress.error(&format!("Inconsistency: modified item {} has vanished from the remote end", id_change));
|
||||
progress.error(&format!("Inconsistency: modified item {} has vanished from the remote end", url_change));
|
||||
continue;
|
||||
},
|
||||
Some(item) => {
|
||||
if let Err(err) = cal_local.update_item(item.clone()).await {
|
||||
progress.error(&format!("Unable to update item {} in local calendar: {}", id_change, err));
|
||||
progress.error(&format!("Unable to update item {} in local calendar: {}", url_change, err));
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -350,20 +350,20 @@ where
|
||||
}
|
||||
|
||||
|
||||
for id_add in local_additions {
|
||||
progress.debug(&format!("> Pushing local addition {} to the server", id_add));
|
||||
for url_add in local_additions {
|
||||
progress.debug(&format!("> Pushing local addition {} to the server", url_add));
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
details: Self::item_name(&cal_local, &id_add).await,
|
||||
details: Self::item_name(&cal_local, &url_add).await,
|
||||
});
|
||||
match cal_local.get_item_by_id_mut(&id_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", id_add));
|
||||
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: {}", id_add, err)),
|
||||
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);
|
||||
@@ -373,20 +373,20 @@ where
|
||||
};
|
||||
}
|
||||
|
||||
for id_change in local_changes {
|
||||
progress.debug(&format!("> Pushing local change {} to the server", id_change));
|
||||
for url_change in local_changes {
|
||||
progress.debug(&format!("> Pushing local change {} to the server", url_change));
|
||||
progress.feedback(SyncEvent::InProgress{
|
||||
calendar: cal_name.clone(),
|
||||
details: Self::item_name(&cal_local, &id_change).await,
|
||||
details: Self::item_name(&cal_local, &url_change).await,
|
||||
});
|
||||
match cal_local.get_item_by_id_mut(&id_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", id_change));
|
||||
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: {}", id_change, err)),
|
||||
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);
|
||||
@@ -400,14 +400,14 @@ where
|
||||
}
|
||||
|
||||
|
||||
async fn item_name(cal: &T, id: &ItemId) -> String {
|
||||
cal.get_item_by_id(id).await.map(|item| item.name()).unwrap_or_default().to_string()
|
||||
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 get_or_insert_counterpart_calendar<H, N, I>(haystack_descr: &str, haystack: &mut H, cal_id: &CalendarId, needle: Arc<Mutex<N>>)
|
||||
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>,
|
||||
@@ -415,18 +415,18 @@ where
|
||||
N: BaseCalendar,
|
||||
{
|
||||
loop {
|
||||
if let Some(cal) = haystack.get_calendar(&cal_id).await {
|
||||
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_id);
|
||||
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_id.clone(),
|
||||
cal_url.clone(),
|
||||
name,
|
||||
supported_comps,
|
||||
color.cloned(),
|
||||
|
||||
+13
-11
@@ -4,10 +4,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::calendar::CalendarId;
|
||||
use crate::utils::random_url;
|
||||
|
||||
/// 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,10 +33,11 @@ impl CompletionStatus {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
/// The task URL
|
||||
id: ItemId,
|
||||
url: Url,
|
||||
|
||||
/// 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, but UUID are even better
|
||||
/// 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.
|
||||
uid: String,
|
||||
|
||||
/// The sync status of this item
|
||||
@@ -65,8 +66,8 @@ pub struct Task {
|
||||
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_id: &CalendarId) -> Self {
|
||||
let new_item_id = ItemId::random(parent_calendar_id);
|
||||
pub fn new(name: String, completed: bool, parent_calendar_url: &Url) -> Self {
|
||||
let new_url = random_url(parent_calendar_url);
|
||||
let new_sync_status = SyncStatus::NotSynced;
|
||||
let new_uid = Uuid::new_v4().to_hyphenated().to_string();
|
||||
let new_creation_date = Some(Utc::now());
|
||||
@@ -76,18 +77,18 @@ impl Task {
|
||||
} 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_item_id, 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_url, new_completion_status, new_sync_status, new_creation_date, new_last_modified, ical_prod_id, extra_parameters)
|
||||
}
|
||||
|
||||
/// Create a new Task instance, that may be synced on the server already
|
||||
pub fn new_with_parameters(name: String, uid: String, id: ItemId,
|
||||
pub fn new_with_parameters(name: String, uid: String, new_url: Url,
|
||||
completion_status: CompletionStatus,
|
||||
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>,
|
||||
ical_prod_id: String, extra_parameters: Vec<Property>,
|
||||
) -> Self
|
||||
{
|
||||
Self {
|
||||
id,
|
||||
url: new_url,
|
||||
uid,
|
||||
name,
|
||||
completion_status,
|
||||
@@ -99,7 +100,7 @@ impl Task {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &ItemId { &self.id }
|
||||
pub fn url(&self) -> &Url { &self.url }
|
||||
pub fn uid(&self) -> &str { &self.uid }
|
||||
pub fn name(&self) -> &str { &self.name }
|
||||
pub fn completed(&self) -> bool { self.completion_status.is_completed() }
|
||||
@@ -112,7 +113,8 @@ impl Task {
|
||||
|
||||
#[cfg(any(test, feature = "integration_tests"))]
|
||||
pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
|
||||
self.id == other.id
|
||||
self.url == other.url
|
||||
&& self.uid == other.uid
|
||||
&& 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)
|
||||
|
||||
+32
-25
@@ -1,41 +1,46 @@
|
||||
//! 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<CalendarId, Arc<Mutex<T>>>, Box<dyn Error>>;
|
||||
/// Returns the calendar matching the ID
|
||||
async fn get_calendar(&self, id: &CalendarId) -> Option<Arc<Mutex<T>>>;
|
||||
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>>>;
|
||||
/// Create a calendar if it did not exist, and return it
|
||||
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents, color: Option<Color>)
|
||||
async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>)
|
||||
-> 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 unique ID
|
||||
fn id(&self) -> &CalendarId;
|
||||
/// Returns the calendar URL
|
||||
fn url(&self) -> &Url;
|
||||
|
||||
/// Returns the supported kinds of components for this calendar
|
||||
fn supported_components(&self) -> crate::calendar::SupportedComponents;
|
||||
@@ -65,25 +70,27 @@ 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;
|
||||
|
||||
/// 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>>;
|
||||
/// 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>>;
|
||||
|
||||
/// Returns a particular item
|
||||
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>>;
|
||||
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>>;
|
||||
|
||||
/// Delete an item
|
||||
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>>;
|
||||
async fn delete_item(&mut self, item_url: &Url) -> Result<(), 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>> {
|
||||
/// Get the URLs of all current items in this calendar
|
||||
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>> {
|
||||
let items = self.get_item_version_tags().await?;
|
||||
Ok(items.iter()
|
||||
.map(|(id, _tag)| id.clone())
|
||||
.map(|(url, _tag)| url.clone())
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -95,30 +102,30 @@ 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, id: CalendarId, supported_components: SupportedComponents, color: Option<Color>) -> Self;
|
||||
fn new(name: String, url: Url, supported_components: SupportedComponents, color: Option<Color>) -> Self;
|
||||
|
||||
/// Get the IDs of all current items in this calendar
|
||||
async fn get_item_ids(&self) -> Result<HashSet<ItemId>, Box<dyn Error>>;
|
||||
/// Get the URLs of all current items in this calendar
|
||||
async fn get_item_urls(&self) -> Result<HashSet<Url>, Box<dyn Error>>;
|
||||
|
||||
/// Returns all items that this calendar contains
|
||||
///
|
||||
/// See [`crate::utils::comparison`] for helper functions that help sorting the results
|
||||
async fn get_items(&self) -> Result<HashMap<ItemId, &Item>, Box<dyn Error>>;
|
||||
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>>;
|
||||
|
||||
/// Returns a particular item
|
||||
async fn get_item_by_id<'a>(&'a self, id: &ItemId) -> Option<&'a Item>;
|
||||
async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item>;
|
||||
|
||||
/// Returns a particular item
|
||||
async fn get_item_by_id_mut<'a>(&'a mut self, id: &ItemId) -> Option<&'a mut Item>;
|
||||
async fn get_item_by_url_mut<'a>(&'a mut self, url: &Url) -> 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: &ItemId) -> Result<(), Box<dyn Error>>;
|
||||
async fn mark_for_deletion(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>>;
|
||||
|
||||
/// Immediately remove an item. See [`CompleteCalendar::mark_for_deletion`]
|
||||
async fn immediately_delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>>;
|
||||
async fn immediately_delete_item(&mut self, item_id: &Url) -> Result<(), Box<dyn Error>>;
|
||||
}
|
||||
|
||||
+17
-10
@@ -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<CalendarId, Arc<Mutex<C>>>)
|
||||
pub async fn print_calendar_list<C>(cals: &HashMap<Url, Arc<Mutex<C>>>)
|
||||
where
|
||||
C: CompleteCalendar,
|
||||
{
|
||||
for (id, cal) in cals {
|
||||
println!("CAL {} ({})", cal.lock().unwrap().name(), id);
|
||||
for (url, cal) in cals {
|
||||
println!("CAL {} ({})", cal.lock().unwrap().name(), url);
|
||||
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<CalendarId, Arc<Mutex<C>>>)
|
||||
pub async fn print_dav_calendar_list<C>(cals: &HashMap<Url, Arc<Mutex<C>>>)
|
||||
where
|
||||
C: DavCalendar,
|
||||
{
|
||||
for (id, cal) in cals {
|
||||
println!("CAL {} ({})", cal.lock().unwrap().name(), id);
|
||||
for (url, cal) in cals {
|
||||
println!("CAL {} ({})", cal.lock().unwrap().name(), url);
|
||||
match cal.lock().unwrap().get_item_version_tags().await {
|
||||
Err(_err) => continue,
|
||||
Ok(map) => {
|
||||
for (id, version_tag) in map {
|
||||
println!(" * {} (version {:?})", id, version_tag);
|
||||
for (url, version_tag) in map {
|
||||
println!(" * {} (version {:?})", url, version_tag);
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -107,7 +107,7 @@ pub fn print_task(item: &Item) {
|
||||
SyncStatus::LocallyModified(_) => "~",
|
||||
SyncStatus::LocallyDeleted(_) => "x",
|
||||
};
|
||||
println!(" {}{} {}\t{}", completion, sync, task.name(), task.id());
|
||||
println!(" {}{} {}\t{}", completion, sync, task.name(), task.url());
|
||||
},
|
||||
_ => return,
|
||||
}
|
||||
@@ -148,3 +148,10 @@ 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 */)
|
||||
}
|
||||
|
||||
+73
-73
@@ -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::ItemId;
|
||||
use kitchen_fridge::item::SyncStatus;
|
||||
use kitchen_fridge::Task;
|
||||
use kitchen_fridge::task::CompletionStatus;
|
||||
use kitchen_fridge::calendar::cached_calendar::CachedCalendar;
|
||||
use kitchen_fridge::provider::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: CalendarId,
|
||||
calendar: Url,
|
||||
/// Its name
|
||||
name: String,
|
||||
/// Its completion status
|
||||
@@ -54,15 +54,15 @@ pub struct ItemState {
|
||||
pub enum ChangeToApply {
|
||||
Rename(String),
|
||||
SetCompletion(bool),
|
||||
Create(CalendarId, Item),
|
||||
Create(Url, Item),
|
||||
/// "remove" means "mark for deletion" in the local calendar, or "immediately delete" on the remote calendar
|
||||
Remove,
|
||||
// ChangeCalendar(CalendarId) is useless, as long as changing a calendar is implemented as "delete in one calendar and re-create it in another one"
|
||||
// ChangeCalendar(Url) 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 {
|
||||
id: ItemId,
|
||||
url: Url,
|
||||
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 = 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());
|
||||
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());
|
||||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
id: ItemId::random(&first_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&first_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&first_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&first_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&first_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&first_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&second_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&second_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&second_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&second_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&second_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&second_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&second_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&third_cal),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&third_cal),
|
||||
url: random_url(&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 id_p = ItemId::random(&third_cal);
|
||||
let url_p = random_url(&third_cal);
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
id: id_p.clone(),
|
||||
url: url_p.clone(),
|
||||
initial_state: LocatedState::BothSynced( ItemState{
|
||||
calendar: third_cal.clone(),
|
||||
name: String::from("Task P"),
|
||||
@@ -369,16 +369,16 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
||||
}
|
||||
);
|
||||
|
||||
let id_q = ItemId::random(&third_cal);
|
||||
let url_q = random_url(&third_cal);
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
id: id_q.clone(),
|
||||
url: url_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"),
|
||||
id_q.to_string(), id_q,
|
||||
url_q.to_string(), url_q,
|
||||
CompletionStatus::Uncompleted,
|
||||
SyncStatus::random_synced(), Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() )
|
||||
))],
|
||||
@@ -390,15 +390,15 @@ pub fn scenarii_basic() -> Vec<ItemScenario> {
|
||||
}
|
||||
);
|
||||
|
||||
let id_r = ItemId::random(&third_cal);
|
||||
let url_r = random_url(&third_cal);
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
id: id_r.clone(),
|
||||
url: url_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"),
|
||||
id_r.to_string(), id_r,
|
||||
url_r.to_string(), url_r,
|
||||
CompletionStatus::Uncompleted,
|
||||
SyncStatus::NotSynced, Some(Utc::now()), Utc::now(), "prod_id".to_string(), Vec::new() )
|
||||
))],
|
||||
@@ -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 = CalendarId::from("https://some.calend.ar/first/".parse().unwrap());
|
||||
let cal2 = CalendarId::from("https://some.calend.ar/second/".parse().unwrap());
|
||||
let cal1 = Url::from("https://some.calend.ar/first/".parse().unwrap());
|
||||
let cal2 = Url::from("https://some.calend.ar/second/".parse().unwrap());
|
||||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
id: ItemId::random(&cal1),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&cal2),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&cal1),
|
||||
url: random_url(&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 = CalendarId::from("https://some.calend.ar/third/".parse().unwrap());
|
||||
let cal4 = CalendarId::from("https://some.calend.ar/fourth/".parse().unwrap());
|
||||
let cal3 = Url::from("https://some.calend.ar/third/".parse().unwrap());
|
||||
let cal4 = Url::from("https://some.calend.ar/fourth/".parse().unwrap());
|
||||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
id: ItemId::random(&cal3),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&cal4),
|
||||
url: random_url(&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 {
|
||||
id: ItemId::random(&cal3),
|
||||
url: random_url(&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 = CalendarId::from("https://some.calend.ar/transient/".parse().unwrap());
|
||||
let cal = Url::from("https://some.calend.ar/transient/".parse().unwrap());
|
||||
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
id: ItemId::random(&cal),
|
||||
url: random_url(&cal),
|
||||
initial_state: LocatedState::Local( ItemState{
|
||||
calendar: cal.clone(),
|
||||
name: String::from("A task, so that the calendar actually exists"),
|
||||
@@ -567,16 +567,16 @@ pub fn scenarii_transient_task() -> Vec<ItemScenario> {
|
||||
}
|
||||
);
|
||||
|
||||
let id_transient = ItemId::random(&cal);
|
||||
let url_transient = random_url(&cal);
|
||||
tasks.push(
|
||||
ItemScenario {
|
||||
id: id_transient.clone(),
|
||||
url: url_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"),
|
||||
id_transient.to_string(), id_transient,
|
||||
url_transient.to_string(), url_transient,
|
||||
CompletionStatus::Uncompleted,
|
||||
SyncStatus::NotSynced, Some(Utc::now()), Utc::now(),
|
||||
"prod_id".to_string(), Vec::new() )
|
||||
@@ -637,8 +637,8 @@ async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc<M
|
||||
let new_item = Item::Task(
|
||||
Task::new_with_parameters(
|
||||
state.name.clone(),
|
||||
item.id.to_string(),
|
||||
item.id.clone(),
|
||||
item.url.to_string(),
|
||||
item.url.clone(),
|
||||
completion_status,
|
||||
sync_status,
|
||||
Some(now),
|
||||
@@ -667,37 +667,37 @@ 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_id = match &item.initial_state {
|
||||
let initial_calendar_url = 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_id = initial_calendar_id.clone();
|
||||
let mut calendar_url = initial_calendar_url.clone();
|
||||
for local_change in &item.local_changes_to_apply {
|
||||
calendar_id = Some(apply_change(provider.local(), calendar_id, &item.id, local_change, false).await);
|
||||
calendar_url = Some(apply_change(provider.local(), calendar_url, &item.url, local_change, false).await);
|
||||
}
|
||||
|
||||
let mut calendar_id = initial_calendar_id;
|
||||
let mut calendar_url = initial_calendar_url;
|
||||
for remote_change in &item.remote_changes_to_apply {
|
||||
calendar_id = Some(apply_change(provider.remote(), calendar_id, &item.id, remote_change, true).await);
|
||||
calendar_url = Some(apply_change(provider.remote(), calendar_url, &item.url, remote_change, true).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId)
|
||||
async fn get_or_insert_calendar(source: &mut Cache, url: &Url)
|
||||
-> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>>
|
||||
{
|
||||
match source.get_calendar(id).await {
|
||||
match source.get_calendar(url).await {
|
||||
Some(cal) => Ok(cal),
|
||||
None => {
|
||||
let new_name = format!("Test calendar for ID {}", id);
|
||||
let new_name = format!("Test calendar for URL {}", url);
|
||||
let supported_components = SupportedComponents::TODO;
|
||||
let color = csscolorparser::parse("#ff8000"); // TODO: we should rather have specific colors, depending on the calendars
|
||||
|
||||
source.create_calendar(
|
||||
id.clone(),
|
||||
url.clone(),
|
||||
new_name.to_string(),
|
||||
supported_components,
|
||||
None,
|
||||
@@ -706,15 +706,15 @@ async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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
|
||||
where
|
||||
S: CalDavSource<C>,
|
||||
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
||||
{
|
||||
match calendar_id {
|
||||
match calendar_url {
|
||||
Some(cal) => {
|
||||
apply_changes_on_an_existing_item(source, &cal, item_id, change, is_remote).await;
|
||||
apply_changes_on_an_existing_item(source, &cal, item_url, change, is_remote).await;
|
||||
cal
|
||||
},
|
||||
None => {
|
||||
@@ -723,14 +723,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_changes_on_an_existing_item<S, C>(source: &S, calendar_id: &CalendarId, item_id: &ItemId, change: &ChangeToApply, is_remote: bool)
|
||||
async fn apply_changes_on_an_existing_item<S, C>(source: &S, calendar_url: &Url, item_url: &Url, 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_id).await.unwrap();
|
||||
let cal = source.get_calendar(calendar_url).await.unwrap();
|
||||
let mut cal = cal.lock().unwrap();
|
||||
let task = cal.get_item_by_id_mut(item_id).await.unwrap().unwrap_task_mut();
|
||||
let task = cal.get_item_by_url_mut(item_url).await.unwrap().unwrap_task_mut();
|
||||
|
||||
match change {
|
||||
ChangeToApply::Rename(new_name) => {
|
||||
@@ -753,18 +753,18 @@ where
|
||||
},
|
||||
ChangeToApply::Remove => {
|
||||
match is_remote {
|
||||
false => cal.mark_for_deletion(item_id).await.unwrap(),
|
||||
true => cal.delete_item(item_id).await.unwrap(),
|
||||
false => cal.mark_for_deletion(item_url).await.unwrap(),
|
||||
true => cal.delete_item(item_url).await.unwrap(),
|
||||
};
|
||||
},
|
||||
ChangeToApply::Create(_calendar_id, _item) => {
|
||||
ChangeToApply::Create(_calendar_url, _item) => {
|
||||
panic!("This function only handles already existing items");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an item, and returns the calendar ID it was inserted in
|
||||
async fn create_test_item<S, C>(source: &S, change: &ChangeToApply) -> CalendarId
|
||||
/// 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
|
||||
where
|
||||
S: CalDavSource<C>,
|
||||
C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds
|
||||
@@ -775,10 +775,10 @@ where
|
||||
ChangeToApply::Remove => {
|
||||
panic!("This function only creates items that do not exist yet");
|
||||
}
|
||||
ChangeToApply::Create(calendar_id, item) => {
|
||||
let cal = source.get_calendar(calendar_id).await.unwrap();
|
||||
ChangeToApply::Create(calendar_url, item) => {
|
||||
let cal = source.get_calendar(calendar_url).await.unwrap();
|
||||
cal.lock().unwrap().add_item(item.clone()).await.unwrap();
|
||||
calendar_id.clone()
|
||||
calendar_url.clone()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
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
|
||||
}
|
||||
|
||||
+3
-3
@@ -124,7 +124,7 @@ impl TestFlavour {
|
||||
Self {
|
||||
scenarii: scenarii::scenarii_basic(),
|
||||
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
|
||||
get_item_by_id_behaviour: (3,2),
|
||||
get_item_by_url_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_id_behaviour: (1,4),
|
||||
get_item_by_url_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_id_behaviour: (0,41),
|
||||
get_item_by_url_behaviour: (0,41),
|
||||
..MockBehaviour::default()
|
||||
})),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user