20 Commits
Author SHA1 Message Date
Ilya 644800907d Add events 2022-04-13 12:43:45 +02:00
daladim 2281c34529 Version 0.4.0 2022-01-19 09:22:24 +01:00
daladim c36c06ba94 Upgrade dependencies 2022-01-19 09:22:24 +01:00
daladim fd216ff7c3 [doc] improvements and rewordings 2022-01-19 09:17:09 +01:00
daladim 511f2123e6 [minor] Examples have different data folders 2022-01-19 09:16:25 +01:00
daladim 7cc1e778bb Added a progress counter 2021-12-24 15:36:34 +01:00
daladim 7b13d74edc [optim] Download 30 changed items at once in a single HTTP request from the server 2021-12-24 14:56:23 +01:00
daladim 159b6199fa Poor man's generic function to work around passing an async closure 2021-12-24 14:56:22 +01:00
daladim 313f2ed0e9 [minor] Unused variable 2021-12-24 14:56:22 +01:00
daladim 6c64bcce8e [optim] Download 30 added items at once in a single HTTP request from the server 2021-12-24 14:56:22 +01:00
daladim 56484a6d4d Split a function 2021-12-24 11:47:56 +01:00
daladim a973146366 Added get_items_by_url, to fetch multiple items at once 2021-12-24 11:47:56 +01:00
daladim f7110c254f Better timestamp parsing 2021-12-24 11:47:56 +01:00
daladim ac7d6952eb Better support for iCal DTSTAMP/LAST-MODIFIED 2021-12-24 11:47:56 +01:00
daladim 5505a17081 [doc] 2021-12-24 11:47:55 +01:00
daladim 039965966f Added a second example 2021-12-24 11:47:55 +01:00
daladim 94b3bb27ba Added a function 2021-12-19 18:50:00 +01:00
daladim 3d142edf3e Merge remote-tracking branch 'daladim/master' 2021-12-07 21:36:10 +01:00
daladim 7f8529b837 [fmt] 2021-12-07 21:35:55 +01:00
daladim ea6cd47b5d Fixed icon trasnparency 2021-11-26 15:25:48 +01:00
21 changed files with 910 additions and 337 deletions
Generated
+133 -131
View File
@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.18" version = "0.7.18"
@@ -11,9 +13,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.51" version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -51,9 +53,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.8.0" version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]] [[package]]
name = "bytes" name = "bytes"
@@ -114,10 +116,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "encoding_rs" name = "either"
version = "0.8.29" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encoding_rs"
version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@@ -135,6 +143,15 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "fastrand"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2"
dependencies = [
"instant",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -168,36 +185,36 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
dependencies = [ dependencies = [
"futures-core", "futures-core",
] ]
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@@ -218,20 +235,20 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi 0.10.2+wasi-snapshot-preview1", "wasi 0.10.3+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.7" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -263,13 +280,13 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
"itoa", "itoa 1.0.1",
] ]
[[package]] [[package]]
@@ -303,9 +320,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.15" version = "0.14.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@@ -316,7 +333,7 @@ dependencies = [
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa 0.4.8",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
@@ -367,26 +384,50 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.7.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
] ]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.3.1" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.8" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.55" version = "0.3.55"
@@ -398,7 +439,7 @@ dependencies = [
[[package]] [[package]]
name = "kitchen-fridge" name = "kitchen-fridge"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bitflags", "bitflags",
@@ -407,6 +448,7 @@ dependencies = [
"env_logger", "env_logger",
"ical-daladim", "ical-daladim",
"ics", "ics",
"itertools",
"log", "log",
"minidom", "minidom",
"once_cell", "once_cell",
@@ -427,9 +469,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.108" version = "0.2.112"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
[[package]] [[package]]
name = "log" name = "log"
@@ -537,9 +579,9 @@ dependencies = [
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
@@ -547,9 +589,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]] [[package]]
name = "openssl" name = "openssl"
@@ -567,15 +609,15 @@ dependencies = [
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.71" version = "0.9.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cc", "cc",
@@ -608,7 +650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [ dependencies = [
"phf_shared", "phf_shared",
"rand 0.7.3", "rand",
] ]
[[package]] [[package]]
@@ -636,9 +678,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@@ -648,15 +690,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.22" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
@@ -666,9 +708,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.32" version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@@ -684,9 +726,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.10" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -699,24 +741,12 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [ dependencies = [
"getrandom 0.1.16", "getrandom 0.1.16",
"libc", "libc",
"rand_chacha 0.2.2", "rand_chacha",
"rand_core 0.5.1", "rand_core",
"rand_hc 0.2.0", "rand_hc",
"rand_pcg", "rand_pcg",
] ]
[[package]]
name = "rand"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.3",
"rand_hc 0.3.1",
]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.2.2" version = "0.2.2"
@@ -724,17 +754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core 0.5.1", "rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.3",
] ]
[[package]] [[package]]
@@ -746,31 +766,13 @@ dependencies = [
"getrandom 0.1.16", "getrandom 0.1.16",
] ]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom 0.2.3",
]
[[package]] [[package]]
name = "rand_hc" name = "rand_hc"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [ dependencies = [
"rand_core 0.5.1", "rand_core",
]
[[package]]
name = "rand_hc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core 0.6.3",
] ]
[[package]] [[package]]
@@ -779,7 +781,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [ dependencies = [
"rand_core 0.5.1", "rand_core",
] ]
[[package]] [[package]]
@@ -819,15 +821,16 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.6" version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280" checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2",
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
@@ -854,9 +857,9 @@ dependencies = [
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]] [[package]]
name = "sanitize-filename" name = "sanitize-filename"
@@ -880,9 +883,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.4.2" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"core-foundation", "core-foundation",
@@ -893,9 +896,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.4.2" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -903,18 +906,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.130" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.130" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -923,32 +926,32 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.71" version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" checksum = "c059c05b48c5c0067d4b4b2b4f0732dd65feb52daf7e0ea09cd87e7dadc1af79"
dependencies = [ dependencies = [
"itoa", "itoa 1.0.1",
"ryu", "ryu",
"serde", "serde",
] ]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"itoa", "itoa 1.0.1",
"ryu", "ryu",
"serde", "serde",
] ]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "0.3.7" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e"
[[package]] [[package]]
name = "slab" name = "slab"
@@ -968,9 +971,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.81" version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -979,13 +982,13 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.2.0" version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand",
"libc", "libc",
"rand 0.8.4",
"redox_syscall", "redox_syscall",
"remove_dir_all", "remove_dir_all",
"winapi", "winapi",
@@ -1047,11 +1050,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
dependencies = [ dependencies = [
"autocfg",
"bytes", "bytes",
"libc", "libc",
"memchr", "memchr",
@@ -1064,9 +1066,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1169,7 +1171,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [ dependencies = [
"getrandom 0.2.3", "getrandom 0.2.4",
] ]
[[package]] [[package]]
@@ -1196,9 +1198,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.2+wasi-snapshot-preview1" version = "0.10.3+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" checksum = "46a2e384a3f170b0c7543787a91411175b71afd56ba4d3a0ae5678d4e2243c0e"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "kitchen-fridge" name = "kitchen-fridge"
version = "0.3.0" version = "0.4.0"
authors = ["daladim"] authors = ["daladim"]
edition = "2018" edition = "2018"
description = "A CalDAV (ical file management over WebDAV) library" description = "A CalDAV (ical file management over WebDAV) library"
@@ -35,3 +35,4 @@ ics = "0.5"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
csscolorparser = { version = "0.5", features = ["serde"] } csscolorparser = { version = "0.5", features = ["serde"] }
once_cell = "1.8" once_cell = "1.8"
itertools = "0.10"
+10 -45
View File
@@ -1,40 +1,32 @@
//! This is an example of how kitchen-fridge can be used //! This is an example of how kitchen-fridge can be used
use std::path::Path;
use chrono::{Utc}; use chrono::{Utc};
use url::Url; use url::Url;
use kitchen_fridge::{client::Client, traits::CalDavSource}; use kitchen_fridge::traits::CalDavSource;
use kitchen_fridge::calendar::SupportedComponents; use kitchen_fridge::calendar::SupportedComponents;
use kitchen_fridge::Item; use kitchen_fridge::Item;
use kitchen_fridge::Task; use kitchen_fridge::Task;
use kitchen_fridge::Event;
use kitchen_fridge::task::CompletionStatus; use kitchen_fridge::task::CompletionStatus;
use kitchen_fridge::cache::Cache;
use kitchen_fridge::CalDavProvider; use kitchen_fridge::CalDavProvider;
use kitchen_fridge::traits::BaseCalendar; use kitchen_fridge::traits::BaseCalendar;
use kitchen_fridge::traits::CompleteCalendar; use kitchen_fridge::traits::CompleteCalendar;
use kitchen_fridge::utils::pause; use kitchen_fridge::utils::pause;
mod shared;
use shared::initial_sync;
use shared::{URL, USERNAME, EXAMPLE_EXISTING_CALENDAR_URL, EXAMPLE_CREATED_CALENDAR_URL};
const CACHE_FOLDER: &str = "test_cache/provider_sync"; const CACHE_FOLDER: &str = "test_cache/provider_sync";
// TODO: change these values with yours
pub const URL: &str = "https://my.server.com/remote.php/dav/files/john";
pub const USERNAME: &str = "username";
pub const PASSWORD: &str = "secret_password";
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/";
pub const EXAMPLE_CREATED_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_that_we_have_created/";
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); env_logger::init();
println!("This examples show how to sync a remote server with a local cache, using a Provider."); println!("This example show how to sync a remote server with a local cache, using a Provider.");
println!("Make sure you have edited the constants in this file to include correct URLs and credentials."); println!("Make sure you have edited the constants in the 'shared.rs' file to include correct URLs and credentials.");
println!("You can also set the RUST_LOG environment variable to display more info about the sync."); println!("You can also set the RUST_LOG environment variable to display more info about the sync.");
println!(""); println!("");
println!("This will use the following settings:"); println!("This will use the following settings:");
@@ -44,39 +36,12 @@ async fn main() {
println!(" * EXAMPLE_CREATED_CALENDAR_URL = {}", EXAMPLE_CREATED_CALENDAR_URL); println!(" * EXAMPLE_CREATED_CALENDAR_URL = {}", EXAMPLE_CREATED_CALENDAR_URL);
pause(); pause();
let cache_path = Path::new(CACHE_FOLDER); let mut provider = initial_sync(CACHE_FOLDER).await;
let client = Client::new(URL, USERNAME, PASSWORD).unwrap();
let cache = match Cache::from_folder(&cache_path) {
Ok(cache) => cache,
Err(err) => {
log::warn!("Invalid cache file: {}. Using a default cache", err);
Cache::new(&cache_path)
}
};
let mut provider = CalDavProvider::new(client, cache);
let cals = provider.local().get_calendars().await.unwrap();
println!("---- Local items, before sync -----");
kitchen_fridge::utils::print_calendar_list(&cals).await;
println!("Starting a sync...");
println!("Depending on your RUST_LOG value, you may see more or less details about the progress.");
// Note that we could use sync_with_feedback() to have better and formatted feedback
if provider.sync().await == false {
log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync.");
}
provider.local().save_to_folder().unwrap();
println!("---- Local items, after sync -----");
let cals = provider.local().get_calendars().await.unwrap();
kitchen_fridge::utils::print_calendar_list(&cals).await;
add_items_and_sync_again(&mut provider).await; add_items_and_sync_again(&mut provider).await;
} }
async fn add_items_and_sync_again(provider: &mut CalDavProvider) async fn add_items_and_sync_again(provider: &mut CalDavProvider) {
{
println!("\nNow, we'll add a calendar and a few tasks and run the sync again."); println!("\nNow, we'll add a calendar and a few tasks and run the sync again.");
pause(); pause();
+58
View File
@@ -0,0 +1,58 @@
use std::path::Path;
use kitchen_fridge::cache::Cache;
use kitchen_fridge::client::Client;
use kitchen_fridge::traits::CalDavSource;
use kitchen_fridge::CalDavProvider;
// TODO: change these values with yours
pub const URL: &str = "https://next.anshorei.me/remote.php/dav/files/test";
pub const USERNAME: &str = "test";
pub const PASSWORD: &str = "1ETsJD6BPTIMLhmVQcCK";
// 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://next.anshorei.me/remote.php/dav/calendars/test/a_calendar_that_we_have_created/";
// https://next.anshorei.me/remote.php/dav
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str =
"https://next.anshorei.me/remote.php/dav/calendars/test/personal/";
fn main() {
panic!("This file is not supposed to be executed");
}
/// Initializes a Provider, and run an initial sync from the server
pub async fn initial_sync(cache_folder: &str) -> CalDavProvider {
let cache_path = Path::new(cache_folder);
let client = Client::new(URL, USERNAME, PASSWORD).unwrap();
let cache = match Cache::from_folder(&cache_path) {
Ok(cache) => cache,
Err(err) => {
log::warn!("Invalid cache file: {}. Using a default cache", err);
Cache::new(&cache_path)
}
};
let mut provider = CalDavProvider::new(client, cache);
let cals = provider.local().get_calendars().await.unwrap();
println!("---- Local items, before sync -----");
kitchen_fridge::utils::print_calendar_list(&cals).await;
println!("Starting a sync...");
println!(
"Depending on your RUST_LOG value, you may see more or less details about the progress."
);
// Note that we could use sync_with_feedback() to have better and formatted feedback
if provider.sync().await == false {
log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync.");
}
provider.local().save_to_folder().unwrap();
println!("---- Local items, after sync -----");
let cals = provider.local().get_calendars().await.unwrap();
kitchen_fridge::utils::print_calendar_list(&cals).await;
provider
}
+65
View File
@@ -0,0 +1,65 @@
//! This is an example of how kitchen-fridge can be used.
//! This binary simply toggles all completion statuses of the tasks it finds.
use std::error::Error;
use chrono::Utc;
use kitchen_fridge::item::Item;
use kitchen_fridge::task::CompletionStatus;
use kitchen_fridge::CalDavProvider;
use kitchen_fridge::utils::pause;
mod shared;
use shared::initial_sync;
use shared::{URL, USERNAME};
const CACHE_FOLDER: &str = "test_cache/toggle_completion";
#[tokio::main]
async fn main() {
env_logger::init();
println!("This example show how to sync a remote server with a local cache, using a Provider.");
println!("Make sure you have edited the constants in the 'shared.rs' file to include correct URLs and credentials.");
println!("You can also set the RUST_LOG environment variable to display more info about the sync.");
println!("");
println!("This will use the following settings:");
println!(" * URL = {}", URL);
println!(" * USERNAME = {}", USERNAME);
pause();
let mut provider = initial_sync(CACHE_FOLDER).await;
toggle_all_tasks_and_sync_again(&mut provider).await.unwrap();
}
async fn toggle_all_tasks_and_sync_again(provider: &mut CalDavProvider) -> Result<(), Box<dyn Error>> {
let mut n_toggled = 0;
for (_url, cal) in provider.local().get_calendars_sync()?.iter() {
for (_url, item) in cal.lock().unwrap().get_items_mut_sync()?.iter_mut() {
match item {
Item::Task(task) => {
match task.completed() {
false => task.set_completion_status(CompletionStatus::Completed(Some(Utc::now()))),
true => task.set_completion_status(CompletionStatus::Uncompleted),
};
n_toggled += 1;
}
Item::Event(_) => {
// Not doing anything with calendar events
},
}
}
}
println!("{} items toggled.", n_toggled);
println!("Syncing...");
provider.sync().await;
println!("Syncing complete.");
Ok(())
}
+18 -5
View File
@@ -23,11 +23,11 @@
inkscape:collect="always" inkscape:collect="always"
id="linearGradient850-3"> id="linearGradient850-3">
<stop <stop
style="stop-color:#7a7a7a;stop-opacity:1" style="stop-color:#555555;stop-opacity:1"
offset="0" offset="0"
id="stop846" /> id="stop846" />
<stop <stop
style="stop-color:#000000;stop-opacity:0;" style="stop-color:#ffffff;stop-opacity:1"
offset="1" offset="1"
id="stop848" /> id="stop848" />
</linearGradient> </linearGradient>
@@ -49,10 +49,10 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="0.7" inkscape:zoom="0.7"
inkscape:cx="730.92011" inkscape:cx="732.34868"
inkscape:cy="461.7807" inkscape:cy="461.7807"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:current-layer="layer2" inkscape:current-layer="layer1"
showgrid="false" showgrid="false"
inkscape:window-width="2510" inkscape:window-width="2510"
inkscape:window-height="1376" inkscape:window-height="1376"
@@ -71,10 +71,23 @@
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:type <dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title> <dc:title />
</cc:Work> </cc:Work>
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="Fond"
style="display:none">
<rect
style="fill:#102335;fill-opacity:1;stroke:#b40f00;stroke-width:0.26499999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect867"
width="213.55655"
height="212.42261"
x="0"
y="0.37030393" />
</g>
<g <g
inkscape:label="Fridge" inkscape:label="Fridge"
inkscape:groupmode="layer" inkscape:groupmode="layer"

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+2 -1
View File
@@ -27,7 +27,8 @@ const MAIN_FILE: &str = "data.json";
/// ///
/// It automatically updates the content of the folder when dropped (see its `Drop` implementation), but you can also manually call [`Cache::save_to_folder`] /// It automatically updates the content of the folder when dropped (see its `Drop` implementation), but you can also manually call [`Cache::save_to_folder`]
/// ///
/// Most of its methods are part of the `CalDavSource` trait implementation /// Most of its functionality is provided by the `CalDavSource` async trait it implements.
/// However, since these functions do not _need_ to be actually async, non-async versions of them are also provided for better convenience. See [`Cache::get_calendar_sync`] for example
#[derive(Debug)] #[derive(Debug)]
pub struct Cache { pub struct Cache {
backing_folder: PathBuf, backing_folder: PathBuf,
+22 -1
View File
@@ -19,7 +19,8 @@ use crate::mock_behaviour::MockBehaviour;
/// A calendar used by the [`cache`](crate::cache) module /// A calendar used by the [`cache`](crate::cache) module
/// ///
/// Most of its methods are part of traits implementations /// Most of its functionality is provided by the async traits it implements.
/// However, since these functions do not _need_ to be actually async, non-async versions of them are also provided for better convenience. See [`CachedCalendar::add_item_sync`] for example
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CachedCalendar { pub struct CachedCalendar {
name: String, name: String,
@@ -134,6 +135,14 @@ impl CachedCalendar {
) )
} }
/// The non-async version of [`Self::get_items_mut`]
pub fn get_items_mut_sync(&mut self) -> Result<HashMap<Url, &mut Item>, Box<dyn Error>> {
Ok(self.items.iter_mut()
.map(|(url, item)| (url.clone(), item))
.collect()
)
}
/// The non-async version of [`Self::get_item_by_url`] /// 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> { pub fn get_item_by_url_sync<'a>(&'a self, url: &Url) -> Option<&'a Item> {
self.items.get(url) self.items.get(url)
@@ -253,6 +262,10 @@ impl CompleteCalendar for CachedCalendar {
self.get_items_sync() self.get_items_sync()
} }
async fn get_items_mut(&mut self) -> Result<HashMap<Url, &mut Item>, Box<dyn Error>> {
self.get_items_mut_sync()
}
async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item> { async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item> {
self.get_item_by_url_sync(url) self.get_item_by_url_sync(url)
} }
@@ -314,6 +327,14 @@ impl DavCalendar for CachedCalendar {
Ok(self.items.get(url).cloned()) Ok(self.items.get(url).cloned())
} }
async fn get_items_by_url(&self, urls: &[Url]) -> Result<Vec<Option<Item>>, Box<dyn Error>> {
let mut v = Vec::new();
for url in urls {
v.push(DavCalendar::get_item_by_url(self, url).await?);
}
Ok(v)
}
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> { async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_delete_item())?; self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_delete_item())?;
+1 -1
View File
@@ -69,7 +69,7 @@ pub enum SearchFilter {
// /// Return only completed tasks // /// Return only completed tasks
// CompletedTasks, // CompletedTasks,
// /// Return only calendar events // /// Return only calendar events
// Events, Events,
} }
impl Default for SearchFilter { impl Default for SearchFilter {
+57 -2
View File
@@ -14,6 +14,20 @@ use crate::item::Item;
use crate::item::VersionTag; use crate::item::VersionTag;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::resource::Resource; use crate::resource::Resource;
use crate::utils::find_elem;
// static TASKS_BODY: &str = r#"
// <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
// <d:prop>
// <d:getetag />
// </d:prop>
// <c:filter>
// <c:comp-filter name="VCALENDAR">
// <c:comp-filter name="VTODO" />
// </c:comp-filter>
// </c:filter>
// </c:calendar-query>
// "#;
static TASKS_BODY: &str = r#" static TASKS_BODY: &str = r#"
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"> <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
@@ -22,12 +36,20 @@ static TASKS_BODY: &str = r#"
</d:prop> </d:prop>
<c:filter> <c:filter>
<c:comp-filter name="VCALENDAR"> <c:comp-filter name="VCALENDAR">
<c:comp-filter name="VTODO" />
</c:comp-filter> </c:comp-filter>
</c:filter> </c:filter>
</c:calendar-query> </c:calendar-query>
"#; "#;
static MULTIGET_BODY_PREFIX: &str = r#"
<c:calendar-multiget xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<c:calendar-data />
</d:prop>
"#;
static MULTIGET_BODY_SUFFIX: &str = r#"
</c:calendar-multiget>
"#;
@@ -191,6 +213,40 @@ impl DavCalendar for RemoteCalendar {
Ok(Some(item)) Ok(Some(item))
} }
async fn get_items_by_url(&self, urls: &[Url]) -> Result<Vec<Option<Item>>, Box<dyn Error>> {
// Build the request body
let mut hrefs = String::new();
for url in urls {
hrefs.push_str(&format!(" <d:href>{}</d:href>\n", url.path()));
}
let body = format!("{}{}{}", MULTIGET_BODY_PREFIX, hrefs, MULTIGET_BODY_SUFFIX);
// Send the request
let xml_replies = crate::client::sub_request_and_extract_elems(&self.resource, "REPORT", body, "response").await?;
// This is supposed to be cached
let version_tags = self.get_item_version_tags().await?;
// Parse the results
let mut results = Vec::new();
for xml_reply in xml_replies {
let href = find_elem(&xml_reply, "href").ok_or("Missing HREF")?.text();
let mut url = self.resource.url().clone();
url.set_path(&href);
let ical_data = find_elem(&xml_reply, "calendar-data").ok_or("Missing calendar-data")?.text();
let vt = match version_tags.get(&url) {
None => return Err(format!("Inconsistent data: {} has no version tag", url).into()),
Some(vt) => vt,
};
let item = crate::ical::parse(&ical_data, url.clone(), SyncStatus::Synced(vt.clone()))?;
results.push(Some(item));
}
Ok(results)
}
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> { async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>> {
let del_response = reqwest::Client::new() let del_response = reqwest::Client::new()
.delete(item_url.clone()) .delete(item_url.clone())
@@ -205,4 +261,3 @@ impl DavCalendar for RemoteCalendar {
Ok(()) Ok(())
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
//! Support for compile-time configuration options //! Support for library configuration options
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
+92 -11
View File
@@ -1,27 +1,100 @@
//! Calendar events (iCal `VEVENT` items) //! Calendar events (iCal `VEVENT` items)
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ical::property::Property;
use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use uuid::Uuid;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::utils::random_url;
/// TODO: implement `Event` one day. /// An event
/// This crate currently only supports tasks, not calendar events. #[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Event { pub struct Event {
/// The event URL
url: Url,
/// Persistent, globally unique identifier for the calendar component.
uid: String, uid: String,
name: String,
/// The sync status of this item
sync_status: SyncStatus, sync_status: SyncStatus,
/// The time this event was created.
/// This is not required by RFC5545. This will be populated in events created by this crate, but can be None for events coming from a server
creation_date: Option<DateTime<Utc>>,
last_modified: DateTime<Utc>,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
/// The display name of the event
name: String,
/// The PRODID, as defined in iCal files
ical_prod_id: String,
/// Extra parameters that have not been parsed from the iCal file (because they're not supported (yet) by this crate).
/// They are needed to serialize this item into an equivalent iCal file
extra_parameters: Vec<Property>,
} }
impl Event { impl Event {
pub fn new() -> Self { pub fn new(
unimplemented!(); name: String,
parent_calendar_url: &Url,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
) -> 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());
let new_last_modified = Utc::now();
let ical_prod_id = crate::ical::default_prod_id();
let extra_parameters = Vec::new();
Self::new_with_parameters(
name,
new_uid,
new_url,
new_sync_status,
new_creation_date,
new_last_modified,
start_date,
end_date,
ical_prod_id,
extra_parameters,
)
}
pub fn new_with_parameters(
name: String,
uid: String,
new_url: Url,
sync_status: SyncStatus,
creation_date: Option<DateTime<Utc>>,
last_modified: DateTime<Utc>,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
ical_prod_id: String,
extra_parameters: Vec<Property>,
) -> Self {
Self {
url: new_url,
uid,
name,
sync_status,
creation_date,
last_modified,
start_date,
end_date,
ical_prod_id,
extra_parameters,
}
} }
pub fn url(&self) -> &Url { pub fn url(&self) -> &Url {
unimplemented!(); &self.url
} }
pub fn uid(&self) -> &str { pub fn uid(&self) -> &str {
@@ -33,15 +106,23 @@ impl Event {
} }
pub fn ical_prod_id(&self) -> &str { pub fn ical_prod_id(&self) -> &str {
unimplemented!() &self.ical_prod_id
} }
pub fn creation_date(&self) -> Option<&DateTime<Utc>> { pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
unimplemented!() self.creation_date.as_ref()
} }
pub fn last_modified(&self) -> &DateTime<Utc> { pub fn last_modified(&self) -> &DateTime<Utc> {
unimplemented!() &self.last_modified
}
pub fn start_date(&self) -> &DateTime<Utc> {
&self.start_date
}
pub fn end_date(&self) -> &DateTime<Utc> {
&self.end_date
} }
pub fn sync_status(&self) -> &SyncStatus { pub fn sync_status(&self) -> &SyncStatus {
+183 -34
View File
@@ -2,26 +2,33 @@
use std::error::Error; use std::error::Error;
use chrono::{Date, DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo}; use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
use chrono::{DateTime, TimeZone, Utc};
use url::Url; use url::Url;
use crate::Item;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::Task;
use crate::task::CompletionStatus; use crate::task::CompletionStatus;
use crate::Event; use crate::Event;
use crate::Item;
use crate::Task;
/// Parse an iCal file into the internal representation [`crate::Item`] /// Parse an iCal file into the internal representation [`crate::Item`]
pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> { pub fn parse(
content: &str,
item_url: Url,
sync_status: SyncStatus,
) -> Result<Item, Box<dyn Error>> {
let mut reader = ical::IcalParser::new(content.as_bytes()); let mut reader = ical::IcalParser::new(content.as_bytes());
let parsed_item = match reader.next() { let parsed_item = match reader.next() {
None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()), None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()),
Some(item) => match item { Some(item) => match item {
Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()), Err(err) => {
Ok(item) => item, return Err(
format!("Unable to parse iCal data for item {}: {}", item_url, err).into(),
)
} }
Ok(item) => item,
},
}; };
let ical_prod_id = extract_ical_prod_id(&parsed_item) let ical_prod_id = extract_ical_prod_id(&parsed_item)
@@ -29,9 +36,93 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
.unwrap_or_else(|| super::default_prod_id()); .unwrap_or_else(|| super::default_prod_id());
let item = match assert_single_type(&parsed_item)? { let item = match assert_single_type(&parsed_item)? {
CurrentType::Event(_) => { CurrentType::Event(event) => {
Item::Event(Event::new()) let mut name = None;
}, let mut uid = None;
let mut last_modified = None;
let mut creation_date = None;
let mut start_date = None;
let mut end_date = None;
let mut extra_parameters = Vec::new();
for prop in &event.properties {
match prop.name.as_str() {
"SUMMARY" => name = prop.value.clone(),
"UID" => uid = prop.value.clone(),
"DTSTART" => {
start_date = parse_date_time_from_property(&prop.value);
}
"DTEND" => {
end_date = parse_date_time_from_property(&prop.value);
}
"DTSTAMP" => {
last_modified = parse_date_time_from_property(&prop.value);
}
"LAST-MODIFIED" => {
last_modified = parse_date_time_from_property(&prop.value);
}
"CREATED" => {
// The property can be specified once, but is not mandatory
creation_date = parse_date_time_from_property(&prop.value)
}
_ => {
// This field is not supported. Let's store it anyway, so that we are able to re-create an identical iCal file
extra_parameters.push(prop.clone());
}
}
}
let name = match name {
Some(name) => name,
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_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_url
)
.into())
}
};
let start_date = match start_date {
Some(dt) => dt,
None => {
return Err(format!(
"Missing DTSTART for item {}, but this is required by RFC5545",
item_url
)
.into())
}
};
let end_date = match end_date {
Some(dt) => dt,
None => {
return Err(format!(
"Missing DTEND for item {}, but this is required by RFC5545",
item_url
)
.into())
}
};
Item::Event(Event::new_with_parameters(
name,
uid,
item_url,
sync_status,
creation_date,
last_modified,
start_date,
end_date,
ical_prod_id,
extra_parameters,
))
}
CurrentType::Todo(todo) => { CurrentType::Todo(todo) => {
let mut name = None; let mut name = None;
@@ -39,29 +130,44 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
let mut completed = false; let mut completed = false;
let mut last_modified = None; let mut last_modified = None;
let mut completion_date = None; let mut completion_date = None;
let mut completion_percent = None;
let mut creation_date = None; let mut creation_date = None;
let mut extra_parameters = Vec::new(); let mut extra_parameters = Vec::new();
for prop in &todo.properties { for prop in &todo.properties {
match prop.name.as_str() { match prop.name.as_str() {
"SUMMARY" => { name = prop.value.clone() }, "SUMMARY" => name = prop.value.clone(),
"UID" => { uid = prop.value.clone() }, "UID" => uid = prop.value.clone(),
"DTSTAMP" => { "DTSTAMP" => {
// The property can be specified once, but is not mandatory // The property can be specified once, but is not mandatory
// "This property specifies the date and time that the information associated with // "This property specifies the date and time that the information associated with
// the calendar component was last revised in the calendar store." // the calendar component was last revised in the calendar store."
last_modified = parse_date_time_from_property(&prop.value) // "In the case of an iCalendar object that doesn't specify a "METHOD"
}, // property [e.g.: VTODO and VEVENT], this property is equivalent to the "LAST-MODIFIED" property".
last_modified = parse_date_time_from_property(&prop.value);
}
"LAST-MODIFIED" => {
// The property can be specified once, but is not mandatory
// "This property specifies the date and time that the information associated with
// the calendar component was last revised in the calendar store."
// In practise, for VEVENT and VTODO, this is generally the same value as DTSTAMP.
last_modified = parse_date_time_from_property(&prop.value);
}
"COMPLETED" => { "COMPLETED" => {
// The property can be specified once, but is not mandatory // The property can be specified once, but is not mandatory
// "This property defines the date and time that a to-do was // "This property defines the date and time that a to-do was
// actually completed." // actually completed."
completion_date = parse_date_time_from_property(&prop.value) completion_date = parse_date_time_from_property(&prop.value)
}, }
"PERCENT-COMPLETE" => {
if let Some(value) = &prop.value {
completion_percent = value.parse().ok()
}
}
"CREATED" => { "CREATED" => {
// The property can be specified once, but is not mandatory // The property can be specified once, but is not mandatory
creation_date = parse_date_time_from_property(&prop.value) creation_date = parse_date_time_from_property(&prop.value)
}, }
"STATUS" => { "STATUS" => {
// Possible values: // Possible values:
// "NEEDS-ACTION" ;Indicates to-do needs action. // "NEEDS-ACTION" ;Indicates to-do needs action.
@@ -88,7 +194,13 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
}; };
let last_modified = match last_modified { let last_modified = match last_modified {
Some(dt) => dt, Some(dt) => dt,
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_url).into()), None => {
return Err(format!(
"Missing DTSTAMP for item {}, but this is required by RFC5545",
item_url
)
.into())
}
}; };
let completion_status = match completed { let completion_status = match completed {
false => { false => {
@@ -96,15 +208,29 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
log::warn!("Task {:?} has an inconsistent content: its STATUS is not completed, yet it has a COMPLETED timestamp at {:?}", uid, completion_date); log::warn!("Task {:?} has an inconsistent content: its STATUS is not completed, yet it has a COMPLETED timestamp at {:?}", uid, completion_date);
} }
CompletionStatus::Uncompleted CompletionStatus::Uncompleted
}, }
true => CompletionStatus::Completed(completion_date), true => CompletionStatus::Completed(completion_date),
}; };
let completion_percent = completion_percent.unwrap_or(match completed {
false => 0f32,
true => 100f32,
});
Item::Task(Task::new_with_parameters(name, uid, item_url, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters)) Item::Task(Task::new_with_parameters(
}, name,
uid,
item_url,
completion_status,
completion_percent,
sync_status,
creation_date,
last_modified,
ical_prod_id,
extra_parameters,
))
}
}; };
// What to do with multiple items? // What to do with multiple items?
if reader.next().map(|r| r.is_ok()) == Some(true) { if reader.next().map(|r| r.is_ok()) == Some(true) {
return Err("Parsing multiple items are not supported".into()); return Err("Parsing multiple items are not supported".into());
@@ -113,33 +239,38 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
Ok(item) Ok(item)
} }
fn parse_date(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
NaiveDate::parse_from_str("20220413", "%Y%m%d")
.map(|dt| dt.and_time(NaiveTime::from_hms(0, 0, 0)))
.map(|dt| DateTime::from_utc(dt, Utc))
}
fn parse_date_time(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> { fn parse_date_time(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S") Utc.datetime_from_str(dt, "%Y%m%dT%H%M%SZ")
.or_else(|_err| Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S"))
.or_else(|_err| parse_date(dt))
} }
fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> { fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> {
value.as_ref() value.as_ref().and_then(|s| {
.and_then(|s| {
parse_date_time(s) parse_date_time(s)
.map_err(|err| { .map_err(|err| {
log::warn!("Invalid timestamp: {}", s); log::warn!("Invalid timestamp: '{}'", s);
err err
}) })
.ok() .ok()
}) })
} }
fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> { fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> {
for prop in &item.properties { for prop in &item.properties {
if &prop.name == "PRODID" { if &prop.name == "PRODID" {
return prop.value.as_ref().map(|s| s.as_str()) return prop.value.as_ref().map(|s| s.as_str());
} }
} }
None None
} }
enum CurrentType<'a> { enum CurrentType<'a> {
Event(&'a IcalEvent), Event(&'a IcalEvent),
Todo(&'a IcalTodo), Todo(&'a IcalTodo),
@@ -169,7 +300,6 @@ fn assert_single_type<'a>(item: &'a IcalCalendar) -> Result<CurrentType<'a>, Box
return Err("Only a single TODO or a single EVENT is supported".into()); return Err("Only a single TODO or a single EVENT is supported".into());
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR
@@ -251,11 +381,17 @@ END:VCALENDAR
assert_eq!(task.name(), "Do not forget to do this"); assert_eq!(task.name(), "Do not forget to do this");
assert_eq!(task.url(), &item_url); assert_eq!(task.url(), &item_url);
assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com"); assert_eq!(
task.uid(),
"0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com"
);
assert_eq!(task.completed(), false); assert_eq!(task.completed(), false);
assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted); assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted);
assert_eq!(task.sync_status(), &sync_status); assert_eq!(task.sync_status(), &sync_status);
assert_eq!(task.last_modified(), &Utc.ymd(2021, 03, 21).and_hms(0, 16, 0)); assert_eq!(
task.last_modified(),
&Utc.ymd(2021, 03, 21).and_hms(0, 16, 0)
);
} }
#[test] #[test]
@@ -264,11 +400,19 @@ END:VCALENDAR
let sync_status = SyncStatus::Synced(version_tag); let sync_status = SyncStatus::Synced(version_tag);
let item_url: Url = "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_url.clone(), sync_status.clone()).unwrap(); let item = parse(
EXAMPLE_ICAL_COMPLETED,
item_url.clone(),
sync_status.clone(),
)
.unwrap();
let task = item.unwrap_task(); let task = item.unwrap_task();
assert_eq!(task.completed(), true); assert_eq!(task.completed(), true);
assert_eq!(task.completion_status(), &CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57)))); assert_eq!(
task.completion_status(),
&CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57)))
);
} }
#[test] #[test]
@@ -277,7 +421,12 @@ END:VCALENDAR
let sync_status = SyncStatus::Synced(version_tag); let sync_status = SyncStatus::Synced(version_tag);
let item_url: Url = "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_url.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(); let task = item.unwrap_task();
assert_eq!(task.completed(), true); assert_eq!(task.completed(), true);
+1 -1
View File
@@ -2,7 +2,7 @@
//! 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). \ //! CalDAV is described as "Calendaring Extensions to WebDAV" in [RFC 4791](https://datatracker.ietf.org/doc/html/rfc4791) and [RFC 7986](https://datatracker.ietf.org/doc/html/rfc7986) and the underlying iCal format is described at least in [RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545). \
//! This library has been intensivley tested with Nextcloud servers. It should support Owncloud and iCloud as well, since they use the very same CalDAV protocol. //! This library has been intensivley tested with Nextcloud servers. It should support Owncloud and iCloud as well, since they use the very same CalDAV protocol.
//! //!
//! This 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. \ //! This initial implementation only supports TODO events. Thus it can fetch and update a CalDAV-hosted todo-list...just like [sticky notes on a kitchen fridge](https://www.google.com/search?q=kitchen+fridge+todo+list&tbm=isch) would. \
//! 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 :-) //! 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 //! ## Possible uses
+125 -48
View File
@@ -6,7 +6,10 @@ use std::error::Error;
use std::collections::HashSet; use std::collections::HashSet;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::fmt::{Display, Formatter};
use url::Url; use url::Url;
use itertools::Itertools;
use crate::traits::{BaseCalendar, CalDavSource, DavCalendar}; use crate::traits::{BaseCalendar, CalDavSource, DavCalendar};
use crate::traits::CompleteCalendar; use crate::traits::CompleteCalendar;
@@ -16,6 +19,30 @@ pub mod sync_progress;
use sync_progress::SyncProgress; use sync_progress::SyncProgress;
use sync_progress::{FeedbackSender, SyncEvent}; use sync_progress::{FeedbackSender, SyncEvent};
/// How many items will be batched in a single HTTP request when downloading from the server
#[cfg(not(test))]
const DOWNLOAD_BATCH_SIZE: usize = 30;
/// How many items will be batched in a single HTTP request when downloading from the server
#[cfg(test)]
const DOWNLOAD_BATCH_SIZE: usize = 3;
// I am too lazy to actually make `fetch_and_apply` generic over an async closure.
// Let's work around by passing an enum, so that `fetch_and_apply` will know what to do
enum BatchDownloadType {
RemoteAdditions,
RemoteChanges,
}
impl Display for BatchDownloadType {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
Self::RemoteAdditions => write!(f, "remote additions"),
Self::RemoteChanges => write!(f, "remote changes"),
}
}
}
/// A data source that combines two `CalDavSource`s, which is able to sync both sources. /// A data source that combines two `CalDavSource`s, which is able to sync both sources.
/// ///
/// Usually, you will only need to use a provider between a server and a local cache, that is to say a [`CalDavProvider`](crate::CalDavProvider), i.e. a `Provider<Cache, CachedCalendar, Client, RemoteCalendar>`. \ /// Usually, you will only need to use a provider between a server and a local cache, that is to say a [`CalDavProvider`](crate::CalDavProvider), i.e. a `Provider<Cache, CachedCalendar, Client, RemoteCalendar>`. \
@@ -159,8 +186,10 @@ where
let cal_name = cal_local.name().to_string(); let cal_name = cal_local.name().to_string();
progress.info(&format!("Syncing calendar {}", cal_name)); progress.info(&format!("Syncing calendar {}", cal_name));
progress.reset_counter();
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
items_done_already: 0,
details: "started".to_string() details: "started".to_string()
}); });
@@ -176,6 +205,7 @@ where
let remote_items = cal_remote.get_item_version_tags().await?; let remote_items = cal_remote.get_item_version_tags().await?;
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
items_done_already: 0,
details: format!("{} remote items", remote_items.len()), details: format!("{} remote items", remote_items.len()),
}); });
@@ -271,10 +301,13 @@ where
progress.trace("Committing changes..."); progress.trace("Committing changes...");
for url_del in local_del { for url_del in local_del {
progress.debug(&format!("> Pushing local deletion {} to the server", url_del)); progress.debug(&format!("> Pushing local deletion {} to the server", url_del));
progress.increment_counter(1);
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
items_done_already: progress.counter(),
details: Self::item_name(&cal_local, &url_del).await, details: Self::item_name(&cal_local, &url_del).await,
}); });
match cal_remote.delete_item(&url_del).await { match cal_remote.delete_item(&url_del).await {
Err(err) => { Err(err) => {
progress.warn(&format!("Unable to delete remote item {}: {}", url_del, err)); progress.warn(&format!("Unable to delete remote item {}: {}", url_del, err));
@@ -290,8 +323,10 @@ where
for url_del in remote_del { for url_del in remote_del {
progress.debug(&format!("> Applying remote deletion {} locally", url_del)); progress.debug(&format!("> Applying remote deletion {} locally", url_del));
progress.increment_counter(1);
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
items_done_already: progress.counter(),
details: Self::item_name(&cal_local, &url_del).await, details: Self::item_name(&cal_local, &url_del).await,
}); });
if let Err(err) = cal_local.immediately_delete_item(&url_del).await { if let Err(err) = cal_local.immediately_delete_item(&url_del).await {
@@ -299,61 +334,29 @@ where
} }
} }
for url_add in remote_additions { Self::apply_remote_additions(
progress.debug(&format!("> Applying remote addition {} locally", url_add)); remote_additions,
progress.feedback(SyncEvent::InProgress{ &mut *cal_local,
calendar: cal_name.clone(), &mut *cal_remote,
details: Self::item_name(&cal_local, &url_add).await, progress,
}); &cal_name
match cal_remote.get_item_by_url(&url_add).await { ).await;
Err(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", 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: {}", url_add, err));
}
},
},
}
}
for url_change in remote_changes { Self::apply_remote_changes(
progress.debug(&format!("> Applying remote change {} locally", url_change)); remote_changes,
progress.feedback(SyncEvent::InProgress{ &mut *cal_local,
calendar: cal_name.clone(), &mut *cal_remote,
details: Self::item_name(&cal_local, &url_change).await, progress,
}); &cal_name
match cal_remote.get_item_by_url(&url_change).await { ).await;
Err(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", 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: {}", url_change, err));
}
},
}
}
}
for url_add in local_additions { for url_add in local_additions {
progress.debug(&format!("> Pushing local addition {} to the server", url_add)); progress.debug(&format!("> Pushing local addition {} to the server", url_add));
progress.increment_counter(1);
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
items_done_already: progress.counter(),
details: Self::item_name(&cal_local, &url_add).await, details: Self::item_name(&cal_local, &url_add).await,
}); });
match cal_local.get_item_by_url_mut(&url_add).await { match cal_local.get_item_by_url_mut(&url_add).await {
@@ -375,8 +378,10 @@ where
for url_change in local_changes { for url_change in local_changes {
progress.debug(&format!("> Pushing local change {} to the server", url_change)); progress.debug(&format!("> Pushing local change {} to the server", url_change));
progress.increment_counter(1);
progress.feedback(SyncEvent::InProgress{ progress.feedback(SyncEvent::InProgress{
calendar: cal_name.clone(), calendar: cal_name.clone(),
items_done_already: progress.counter(),
details: Self::item_name(&cal_local, &url_change).await, details: Self::item_name(&cal_local, &url_change).await,
}); });
match cal_local.get_item_by_url_mut(&url_change).await { match cal_local.get_item_by_url_mut(&url_change).await {
@@ -404,6 +409,78 @@ where
cal.get_item_by_url(url).await.map(|item| item.name()).unwrap_or_default().to_string() cal.get_item_by_url(url).await.map(|item| item.name()).unwrap_or_default().to_string()
} }
async fn apply_remote_additions(
mut remote_additions: HashSet<Url>,
cal_local: &mut T,
cal_remote: &mut U,
progress: &mut SyncProgress,
cal_name: &str
) {
for batch in remote_additions.drain().chunks(DOWNLOAD_BATCH_SIZE).into_iter() {
Self::fetch_batch_and_apply(BatchDownloadType::RemoteAdditions, batch, cal_local, cal_remote, progress, cal_name).await;
}
}
async fn apply_remote_changes(
mut remote_changes: HashSet<Url>,
cal_local: &mut T,
cal_remote: &mut U,
progress: &mut SyncProgress,
cal_name: &str
) {
for batch in remote_changes.drain().chunks(DOWNLOAD_BATCH_SIZE).into_iter() {
Self::fetch_batch_and_apply(BatchDownloadType::RemoteChanges, batch, cal_local, cal_remote, progress, cal_name).await;
}
}
async fn fetch_batch_and_apply<I: Iterator<Item = Url>>(
batch_type: BatchDownloadType,
remote_additions: I,
cal_local: &mut T,
cal_remote: &mut U,
progress: &mut SyncProgress,
cal_name: &str
) {
progress.debug(&format!("> Applying a batch of {} locally", batch_type) /* too bad Chunks does not implement ExactSizeIterator, that could provide useful debug info. See https://github.com/rust-itertools/itertools/issues/171 */);
let list_of_additions: Vec<Url> = remote_additions.map(|url| url.clone()).collect();
match cal_remote.get_items_by_url(&list_of_additions).await {
Err(err) => {
progress.warn(&format!("Unable to get the batch of {} {:?}: {}. Skipping them.", batch_type, list_of_additions, err));
},
Ok(items) => {
for item in items {
match item {
None => {
progress.error(&format!("Inconsistency: an item from the batch has vanished from the remote end"));
continue;
},
Some(new_item) => {
let local_update_result = match batch_type {
BatchDownloadType::RemoteAdditions => cal_local.add_item(new_item.clone()).await,
BatchDownloadType::RemoteChanges => cal_local.update_item(new_item.clone()).await,
};
if let Err(err) = local_update_result {
progress.error(&format!("Not able to add item {} to local calendar: {}", new_item.url(), err));
}
},
}
}
// Notifying every item at the same time would not make sense. Let's notify only one of them
let one_item_name = match list_of_additions.get(0) {
Some(url) => Self::item_name(&cal_local, &url).await,
None => String::from("<unable to get the name of the first batched item>"),
};
progress.increment_counter(list_of_additions.len());
progress.feedback(SyncEvent::InProgress{
calendar: cal_name.to_string(),
items_done_already: progress.counter(),
details: one_item_name,
});
},
}
}
} }
+22 -5
View File
@@ -10,7 +10,7 @@ pub enum SyncEvent {
/// Sync has just started but no calendar is handled yet /// Sync has just started but no calendar is handled yet
Started, Started,
/// Sync is in progress. /// Sync is in progress.
InProgress{ calendar: String, details: String}, InProgress{ calendar: String, items_done_already: usize, details: String},
/// Sync is finished /// Sync is finished
Finished{ success: bool }, Finished{ success: bool },
} }
@@ -20,7 +20,7 @@ impl Display for SyncEvent {
match self { match self {
SyncEvent::NotStarted => write!(f, "Not started"), SyncEvent::NotStarted => write!(f, "Not started"),
SyncEvent::Started => write!(f, "Sync has started..."), SyncEvent::Started => write!(f, "Sync has started..."),
SyncEvent::InProgress{calendar, details} => write!(f, "[{}] {}...", calendar, details), SyncEvent::InProgress{calendar, items_done_already, details} => write!(f, "{} [{}/?] {}...", calendar, items_done_already, details),
SyncEvent::Finished{success} => match success { SyncEvent::Finished{success} => match success {
true => write!(f, "Sync successfully finished"), true => write!(f, "Sync successfully finished"),
false => write!(f, "Sync finished with errors"), false => write!(f, "Sync finished with errors"),
@@ -53,16 +53,33 @@ pub fn feedback_channel() -> (FeedbackSender, FeedbackReceiver) {
/// A structure that tracks the progression and the errors that happen during a sync /// A structure that tracks the progression and the errors that happen during a sync
pub struct SyncProgress { pub struct SyncProgress {
n_errors: u32, n_errors: u32,
feedback_channel: Option<FeedbackSender> feedback_channel: Option<FeedbackSender>,
counter: usize,
} }
impl SyncProgress { impl SyncProgress {
pub fn new() -> Self { pub fn new() -> Self {
Self { n_errors: 0, feedback_channel: None } Self { n_errors: 0, feedback_channel: None, counter: 0 }
} }
pub fn new_with_feedback_channel(channel: FeedbackSender) -> Self { pub fn new_with_feedback_channel(channel: FeedbackSender) -> Self {
Self { n_errors: 0, feedback_channel: Some(channel) } Self { n_errors: 0, feedback_channel: Some(channel), counter: 0 }
} }
/// Reset the user-info counter
pub fn reset_counter(&mut self) {
self.counter = 0;
}
/// Increments the user-info counter.
pub fn increment_counter(&mut self, increment: usize) {
self.counter += increment;
}
/// Retrieves the current user-info counter.
/// This counts "arbitrary things", that's provided as a convenience but it is not used internally
/// (e.g. that can be used to keep track of the items handled for the current calendar)
pub fn counter(&self) -> usize {
self.counter
}
pub fn is_success(&self) -> bool { pub fn is_success(&self) -> bool {
self.n_errors == 0 self.n_errors == 0
+69 -24
View File
@@ -1,10 +1,10 @@
//! To-do tasks (iCal `VTODO` item) //! To-do tasks (iCal `VTODO` item)
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ical::property::Property; use ical::property::Property;
use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use uuid::Uuid;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::utils::random_url; use crate::utils::random_url;
@@ -49,11 +49,11 @@ pub struct Task {
last_modified: DateTime<Utc>, last_modified: DateTime<Utc>,
/// The completion status of this task /// The completion status of this task
completion_status: CompletionStatus, completion_status: CompletionStatus,
completion_percent: f32,
/// The display name of the task /// The display name of the task
name: String, name: String,
/// The PRODID, as defined in iCal files /// The PRODID, as defined in iCal files
ical_prod_id: String, ical_prod_id: String,
@@ -62,7 +62,6 @@ pub struct Task {
extra_parameters: Vec<Property>, extra_parameters: Vec<Property>,
} }
impl Task { impl Task {
/// Create a brand new Task that is not on a server yet. /// Create a brand new Task that is not on a server yet.
/// This will pick a new (random) task ID. /// This will pick a new (random) task ID.
@@ -74,24 +73,45 @@ impl Task {
let new_last_modified = Utc::now(); let new_last_modified = Utc::now();
let new_completion_status = if completed { let new_completion_status = if completed {
CompletionStatus::Completed(Some(Utc::now())) CompletionStatus::Completed(Some(Utc::now()))
} else { CompletionStatus::Uncompleted }; } else {
CompletionStatus::Uncompleted
};
let new_completion_percent = if completed { 100f32 } else { 0f32 };
let ical_prod_id = crate::ical::default_prod_id(); let ical_prod_id = crate::ical::default_prod_id();
let extra_parameters = Vec::new(); let extra_parameters = Vec::new();
Self::new_with_parameters(name, new_uid, new_url, new_completion_status, new_sync_status, new_creation_date, new_last_modified, ical_prod_id, extra_parameters) Self::new_with_parameters(
name,
new_uid,
new_url,
new_completion_status,
new_completion_percent,
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 /// Create a new Task instance, that may be synced on the server already
pub fn new_with_parameters(name: String, uid: String, new_url: Url, pub fn new_with_parameters(
name: String,
uid: String,
new_url: Url,
completion_status: CompletionStatus, completion_status: CompletionStatus,
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>, completion_percent: f32,
ical_prod_id: String, extra_parameters: Vec<Property>, sync_status: SyncStatus,
) -> Self creation_date: Option<DateTime<Utc>>,
{ last_modified: DateTime<Utc>,
ical_prod_id: String,
extra_parameters: Vec<Property>,
) -> Self {
Self { Self {
url: new_url, url: new_url,
uid, uid,
name, name,
completion_status, completion_status,
completion_percent,
sync_status, sync_status,
creation_date, creation_date,
last_modified, last_modified,
@@ -100,16 +120,39 @@ impl Task {
} }
} }
pub fn url(&self) -> &Url { &self.url } pub fn url(&self) -> &Url {
pub fn uid(&self) -> &str { &self.uid } &self.url
pub fn name(&self) -> &str { &self.name } }
pub fn completed(&self) -> bool { self.completion_status.is_completed() } pub fn uid(&self) -> &str {
pub fn ical_prod_id(&self) -> &str { &self.ical_prod_id } &self.uid
pub fn sync_status(&self) -> &SyncStatus { &self.sync_status } }
pub fn last_modified(&self) -> &DateTime<Utc> { &self.last_modified } pub fn name(&self) -> &str {
pub fn creation_date(&self) -> Option<&DateTime<Utc>> { self.creation_date.as_ref() } &self.name
pub fn completion_status(&self) -> &CompletionStatus { &self.completion_status } }
pub fn extra_parameters(&self) -> &[Property] { &self.extra_parameters } pub fn completed(&self) -> bool {
self.completion_status.is_completed()
}
pub fn completion_percent(&self) -> f32 {
self.completion_percent
}
pub fn ical_prod_id(&self) -> &str {
&self.ical_prod_id
}
pub fn sync_status(&self) -> &SyncStatus {
&self.sync_status
}
pub fn last_modified(&self) -> &DateTime<Utc> {
&self.last_modified
}
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
self.creation_date.as_ref()
}
pub fn completion_status(&self) -> &CompletionStatus {
&self.completion_status
}
pub fn extra_parameters(&self) -> &[Property] {
&self.extra_parameters
}
#[cfg(any(test, feature = "integration_tests"))] #[cfg(any(test, feature = "integration_tests"))]
pub fn has_same_observable_content_as(&self, other: &Task) -> bool { pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
@@ -137,7 +180,7 @@ impl Task {
SyncStatus::LocallyDeleted(_) => { SyncStatus::LocallyDeleted(_) => {
log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync."); log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync.");
return; return;
}, }
} }
} }
@@ -145,7 +188,6 @@ impl Task {
self.last_modified = Utc::now(); self.last_modified = Utc::now();
} }
/// Rename a task. /// Rename a task.
/// This updates its "last modified" field /// This updates its "last modified" field
pub fn set_name(&mut self, new_name: String) { pub fn set_name(&mut self, new_name: String) {
@@ -169,7 +211,10 @@ impl Task {
} }
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
/// Set the completion status, but forces a "master" SyncStatus, just like CalDAV servers are always "masters" /// Set the completion status, but forces a "master" SyncStatus, just like CalDAV servers are always "masters"
pub fn mock_remote_calendar_set_completion_status(&mut self, new_completion_status: CompletionStatus) { pub fn mock_remote_calendar_set_completion_status(
&mut self,
new_completion_status: CompletionStatus,
) {
self.sync_status = SyncStatus::random_synced(); self.sync_status = SyncStatus::random_synced();
self.completion_status = new_completion_status; self.completion_status = new_completion_status;
} }
+7
View File
@@ -83,6 +83,10 @@ pub trait DavCalendar : BaseCalendar {
/// Returns a particular item /// Returns a particular item
async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>>; async fn get_item_by_url(&self, url: &Url) -> Result<Option<Item>, Box<dyn Error>>;
/// Returns a set of items.
/// This is usually faster than calling multiple consecutive [`DavCalendar::get_item_by_url`], since it only issues one HTTP request.
async fn get_items_by_url(&self, urls: &[Url]) -> Result<Vec<Option<Item>>, Box<dyn Error>>;
/// Delete an item /// Delete an item
async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>>; async fn delete_item(&mut self, item_url: &Url) -> Result<(), Box<dyn Error>>;
@@ -115,6 +119,9 @@ pub trait CompleteCalendar : BaseCalendar {
/// Returns all items that this calendar contains /// Returns all items that this calendar contains
async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>>; async fn get_items(&self) -> Result<HashMap<Url, &Item>, Box<dyn Error>>;
/// Returns all items that this calendar contains
async fn get_items_mut(&mut self) -> Result<HashMap<Url, &mut Item>, Box<dyn Error>>;
/// Returns a particular item /// Returns a particular item
async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item>; async fn get_item_by_url<'a>(&'a self, url: &Url) -> Option<&'a Item>;
+31 -15
View File
@@ -1,17 +1,17 @@
///! Some utility functions //! Some utility functions
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::hash::Hash; use std::hash::Hash;
use std::io::{stdin, stdout, Read, Write}; use std::io::{stdin, stdout, Read, Write};
use std::sync::{Arc, Mutex};
use minidom::Element; use minidom::Element;
use url::Url; use url::Url;
use crate::item::SyncStatus;
use crate::traits::CompleteCalendar; use crate::traits::CompleteCalendar;
use crate::traits::DavCalendar; use crate::traits::DavCalendar;
use crate::Item; use crate::Item;
use crate::item::SyncStatus;
/// Walks an XML tree and returns every element that has the given name /// Walks an XML tree and returns every element that has the given name
pub fn find_elems<S: AsRef<str>>(root: &Element, searched_name: S) -> Vec<&Element> { pub fn find_elems<S: AsRef<str>>(root: &Element, searched_name: S) -> Vec<&Element> {
@@ -49,14 +49,10 @@ pub fn find_elem<S: AsRef<str>>(root: &Element, searched_name: S) -> Option<&Ele
None None
} }
pub fn print_xml(element: &Element) { pub fn print_xml(element: &Element) {
let mut writer = std::io::stdout(); let mut writer = std::io::stdout();
let mut xml_writer = minidom::quick_xml::Writer::new_with_indent( let mut xml_writer = minidom::quick_xml::Writer::new_with_indent(std::io::stdout(), 0x20, 4);
std::io::stdout(),
0x20, 4
);
let _ = element.to_writer(&mut xml_writer); let _ = element.to_writer(&mut xml_writer);
let _ = writer.write(&[0x0a]); let _ = writer.write(&[0x0a]);
} }
@@ -74,7 +70,7 @@ where
for (_, item) in map { for (_, item) in map {
print_task(item); print_task(item);
} }
}, }
} }
} }
} }
@@ -92,7 +88,7 @@ where
for (url, version_tag) in map { for (url, version_tag) in map {
println!(" * {} (version {:?})", url, version_tag); println!(" * {} (version {:?})", url, version_tag);
} }
}, }
} }
} }
} }
@@ -100,7 +96,14 @@ where
pub fn print_task(item: &Item) { pub fn print_task(item: &Item) {
match item { match item {
Item::Task(task) => { Item::Task(task) => {
let completion = if task.completed() { "✓" } else { " " }; let completion = if task.completed() {
String::from("✓")
} else if task.completion_percent() > 0f32 {
format!("{}%", task.completion_percent())
} else {
String::new()
};
let completion = format!("{completion:>width$}", completion = completion, width = 4);
let sync = match task.sync_status() { let sync = match task.sync_status() {
SyncStatus::NotSynced => ".", SyncStatus::NotSynced => ".",
SyncStatus::Synced(_) => "=", SyncStatus::Synced(_) => "=",
@@ -108,12 +111,27 @@ pub fn print_task(item: &Item) {
SyncStatus::LocallyDeleted(_) => "x", SyncStatus::LocallyDeleted(_) => "x",
}; };
println!(" {}{} {}\t{}", completion, sync, task.name(), task.url()); println!(" {}{} {}\t{}", completion, sync, task.name(), task.url());
}, }
Item::Event(event) => {
let sync = match event.sync_status() {
SyncStatus::NotSynced => ".",
SyncStatus::Synced(_) => "=",
SyncStatus::LocallyModified(_) => "~",
SyncStatus::LocallyDeleted(_) => "x",
};
println!(
" {}-{}{} {}\t{}",
event.start_date(),
event.end_date(),
sync,
event.name(),
event.url()
)
}
_ => return, _ => return,
} }
} }
/// Compare keys of two hashmaps for equality /// Compare keys of two hashmaps for equality
pub fn keys_are_the_same<T, U, V>(left: &HashMap<T, U>, right: &HashMap<T, V>) -> bool pub fn keys_are_the_same<T, U, V>(left: &HashMap<T, U>, right: &HashMap<T, V>) -> bool
where where
@@ -140,7 +158,6 @@ where
result result
} }
/// Wait for the user to press enter /// Wait for the user to press enter
pub fn pause() { pub fn pause() {
let mut stdout = stdout(); let mut stdout = stdout();
@@ -149,7 +166,6 @@ pub fn pause() {
stdin().read_exact(&mut [0]).unwrap(); stdin().read_exact(&mut [0]).unwrap();
} }
/// Generate a random URL with a given prefix /// Generate a random URL with a given prefix
pub fn random_url(parent_calendar: &Url) -> Url { pub fn random_url(parent_calendar: &Url) -> Url {
let random = uuid::Uuid::new_v4().to_hyphenated().to_string(); let random = uuid::Uuid::new_v4().to_hyphenated().to_string();
+2 -2
View File
@@ -694,13 +694,13 @@ async fn get_or_insert_calendar(source: &mut Cache, url: &Url)
None => { None => {
let new_name = format!("Test calendar for URL {}", url); let new_name = format!("Test calendar for URL {}", url);
let supported_components = SupportedComponents::TODO; let supported_components = SupportedComponents::TODO;
let color = csscolorparser::parse("#ff8000"); // TODO: we should rather have specific colors, depending on the calendars let color = csscolorparser::parse("#ff8000").unwrap(); // TODO: we should rather have specific colors, depending on the calendars
source.create_calendar( source.create_calendar(
url.clone(), url.clone(),
new_name.to_string(), new_name.to_string(),
supported_components, supported_components,
None, Some(color),
).await ).await
} }
} }
+1 -1
View File
@@ -145,7 +145,7 @@ impl TestFlavour {
scenarii: scenarii::scenarii_basic(), scenarii: scenarii::scenarii_basic(),
mock_behaviour: Arc::new(Mutex::new(MockBehaviour{ mock_behaviour: Arc::new(Mutex::new(MockBehaviour{
add_item_behaviour: (2,3), add_item_behaviour: (2,3),
get_item_by_url_behaviour: (1,4), get_item_by_url_behaviour: (1,12),
..MockBehaviour::default() ..MockBehaviour::default()
})), })),
} }