mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-29 10:49:15 +00:00
Compare commits
16 Commits
v0.11.4
...
feature/ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
968a5e931c | ||
|
|
2e89b63cd2 | ||
|
|
1cfc8e7c23 | ||
|
|
b0fdca1b64 | ||
|
|
b65cca9d17 | ||
|
|
55ecbdcd41 | ||
|
|
89d3d3b7a4 | ||
|
|
a74b74369c | ||
|
|
85c49a0bdf | ||
|
|
f2e4e2c1a7 | ||
|
|
28c925301e | ||
|
|
b50ea478db | ||
|
|
2c7748255c | ||
|
|
f40a23a1f1 | ||
|
|
2a4ba33e45 | ||
|
|
6bc4bd3fa3 |
346
Cargo.lock
generated
346
Cargo.lock
generated
@@ -26,6 +26,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alloca"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allocator-api2"
|
name = "allocator-api2"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -41,6 +50,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anes"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.21"
|
version = "0.6.21"
|
||||||
@@ -156,9 +171,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_web"
|
name = "askama_web"
|
||||||
version = "0.14.6"
|
version = "0.14.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50dcd7d2caaff31b91ef5d112ed10416344e23a33db9e7eea7ba695d2a97a88a"
|
checksum = "e1acadd534892f9ef8c3809b47997e3cd857fad735edceff77a88be1c8236920"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_web_derive",
|
"askama_web_derive",
|
||||||
@@ -260,9 +275,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-lock"
|
name = "async-lock"
|
||||||
version = "3.4.1"
|
version = "3.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
|
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener 5.4.1",
|
"event-listener 5.4.1",
|
||||||
"event-listener-strategy",
|
"event-listener-strategy",
|
||||||
@@ -345,9 +360,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.7"
|
version = "0.8.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
|
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -397,9 +412,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-extra"
|
name = "axum-extra"
|
||||||
version = "0.12.2"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbfe9f610fe4e99cf0cfcd03ccf8c63c28c616fe714d80475ef731f3b13dd21b"
|
checksum = "6dfbd6109d91702d55fc56df06aae7ed85c465a7a451db6c0e54a4b9ca5983d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
@@ -541,10 +556,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cast"
|
||||||
version = "1.2.49"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
|
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -595,6 +616,33 @@ dependencies = [
|
|||||||
"phf",
|
"phf",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||||
|
dependencies = [
|
||||||
|
"ciborium-io",
|
||||||
|
"ciborium-ll",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium-io"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium-ll"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||||
|
dependencies = [
|
||||||
|
"ciborium-io",
|
||||||
|
"half",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.53"
|
version = "4.5.53"
|
||||||
@@ -718,6 +766,61 @@ version = "2.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "criterion"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
|
||||||
|
dependencies = [
|
||||||
|
"alloca",
|
||||||
|
"anes",
|
||||||
|
"cast",
|
||||||
|
"ciborium",
|
||||||
|
"clap",
|
||||||
|
"criterion-plot",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"num-traits",
|
||||||
|
"oorandom",
|
||||||
|
"page_size",
|
||||||
|
"plotters",
|
||||||
|
"rayon",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tinytemplate",
|
||||||
|
"tokio",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "criterion-plot"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
|
||||||
|
dependencies = [
|
||||||
|
"cast",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-queue"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.12"
|
version = "0.3.12"
|
||||||
@@ -733,6 +836,12 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-bigint"
|
name = "crypto-bigint"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -874,18 +983,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "2.1.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
|
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_more-impl",
|
"derive_more-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more-impl"
|
name = "derive_more-impl"
|
||||||
version = "2.1.0"
|
version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
|
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1382,6 +1491,17 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "half"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crunchy",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1641,14 +1761,14 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ical"
|
name = "ical"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
source = "git+https://github.com/lennart-k/ical-rs#211ce20acf3ecc7831eecf78d9a23232b83d7a6c"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
|
"derive_more",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"rrule",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1791,14 +1911,15 @@ checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.44.3"
|
version = "1.45.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698"
|
checksum = "b76866be74d68b1595eb8060cb9191dca9c021db2316558e52ddc5d55d41b66c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console",
|
"console",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
"similar",
|
"similar",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1832,6 +1953,15 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1843,9 +1973,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
@@ -2133,6 +2263,12 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oorandom"
|
||||||
|
version = "11.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openidconnect"
|
name = "openidconnect"
|
||||||
version = "4.0.1"
|
version = "4.0.1"
|
||||||
@@ -2327,6 +2463,16 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "page_size"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@@ -2538,6 +2684,34 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plotters"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"plotters-backend",
|
||||||
|
"plotters-svg",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plotters-backend"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plotters-svg"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||||
|
dependencies = [
|
||||||
|
"plotters-backend",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.11.0"
|
version = "3.11.0"
|
||||||
@@ -2777,6 +2951,26 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -3068,7 +3262,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical"
|
name = "rustical"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -3101,7 +3295,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.9.9+spec-1.0.0",
|
"toml 0.9.10+spec-1.1.0",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-sessions",
|
"tower-sessions",
|
||||||
@@ -3113,7 +3307,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_caldav"
|
name = "rustical_caldav"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3155,7 +3349,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_carddav"
|
name = "rustical_carddav"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3188,7 +3382,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav"
|
name = "rustical_dav"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3197,6 +3391,7 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
|
"ical",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
"matchit 0.9.0",
|
"matchit 0.9.0",
|
||||||
@@ -3213,7 +3408,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav_push"
|
name = "rustical_dav_push"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3238,7 +3433,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_frontend"
|
name = "rustical_frontend"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_web",
|
"askama_web",
|
||||||
@@ -3274,7 +3469,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_ical"
|
name = "rustical_ical"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3291,7 +3486,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_oidc"
|
name = "rustical_oidc"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3307,7 +3502,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store"
|
name = "rustical_store"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3340,11 +3535,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store_sqlite"
|
name = "rustical_store_sqlite"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"criterion",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
"ical",
|
||||||
"password-auth",
|
"password-auth",
|
||||||
"password-hash",
|
"password-hash",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
@@ -3362,7 +3559,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_xml"
|
name = "rustical_xml"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
@@ -3425,9 +3622,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.20"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
@@ -3530,9 +3727,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.145"
|
version = "1.0.146"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -4033,6 +4230,19 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"syn 2.0.111",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -4123,6 +4333,16 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinytemplate"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
@@ -4215,14 +4435,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.9.9+spec-1.0.0"
|
version = "0.9.10+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb5238e643fc34a1d5d7e753e1532a91912d74b63b92b3ea51fde8d1b7bc79dd"
|
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.12.1",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned 1.0.4",
|
"serde_spanned 1.0.4",
|
||||||
"toml_datetime 0.7.4+spec-1.0.0",
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow",
|
"winnow",
|
||||||
@@ -4239,9 +4459,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.7.4+spec-1.0.0"
|
version = "0.7.5+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6"
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -4267,16 +4487,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
|
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.12.1",
|
"indexmap 2.12.1",
|
||||||
"toml_datetime 0.7.4+spec-1.0.0",
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.0.5+spec-1.0.0"
|
version = "1.0.6+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c"
|
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
@@ -4289,9 +4509,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.0.5+spec-1.0.0"
|
version = "1.0.6+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9cd6190959dce0994aa8970cd32ab116d1851ead27e866039acaf2524ce44fa"
|
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tonic"
|
name = "tonic"
|
||||||
@@ -4458,9 +4678,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.43"
|
version = "0.1.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -4481,9 +4701,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.35"
|
version = "0.1.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"valuable",
|
"valuable",
|
||||||
@@ -4817,6 +5037,22 @@ dependencies = [
|
|||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
@@ -4826,6 +5062,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
@@ -5139,7 +5381,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xml_derive"
|
name = "xml_derive"
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.23.0",
|
"darling 0.23.0",
|
||||||
"heck",
|
"heck",
|
||||||
|
|||||||
13
Cargo.toml
13
Cargo.toml
@@ -2,7 +2,7 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.11.4"
|
version = "0.11.6"
|
||||||
rust-version = "1.91"
|
rust-version = "1.91"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A CalDAV server"
|
description = "A CalDAV server"
|
||||||
@@ -32,12 +32,11 @@ opentelemetry = [
|
|||||||
"dep:tracing-opentelemetry",
|
"dep:tracing-opentelemetry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
rustical_dav = { path = "./crates/dav/" }
|
rustical_dav = { path = "./crates/dav/", features = ["ical"] }
|
||||||
rustical_dav_push = { path = "./crates/dav_push/" }
|
rustical_dav_push = { path = "./crates/dav_push/" }
|
||||||
rustical_store = { path = "./crates/store/" }
|
rustical_store = { path = "./crates/store/" }
|
||||||
rustical_store_sqlite = { path = "./crates/store_sqlite/" }
|
rustical_store_sqlite = { path = "./crates/store_sqlite/" }
|
||||||
@@ -108,11 +107,12 @@ strum = "0.27"
|
|||||||
strum_macros = "0.27"
|
strum_macros = "0.27"
|
||||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||||
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
||||||
ical = { git = "https://github.com/lennart-k/ical-rs", features = [
|
ical = { path = "../ical-rs/", features = [
|
||||||
"generator",
|
|
||||||
"serde",
|
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
] }
|
] }
|
||||||
|
# ical = { git = "https://github.com/lennart-k/ical-rs", features = [
|
||||||
|
# "chrono-tz",
|
||||||
|
# ] }
|
||||||
toml = "0.9"
|
toml = "0.9"
|
||||||
tower = "0.5"
|
tower = "0.5"
|
||||||
tower-http = { version = "0.6", features = [
|
tower-http = { version = "0.6", features = [
|
||||||
@@ -150,6 +150,7 @@ openssl = { version = "0.10", features = ["vendored"] }
|
|||||||
async-std = { version = "1.13", features = ["attributes"] }
|
async-std = { version = "1.13", features = ["attributes"] }
|
||||||
similar-asserts = "1.7"
|
similar-asserts = "1.7"
|
||||||
insta = { version = "1.44", features = ["filters"] }
|
insta = { version = "1.44", features = ["filters"] }
|
||||||
|
criterion = { version = "0.8", features = ["async_tokio"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest.workspace = true
|
rstest.workspace = true
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ base64.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
rustical_dav.workspace = true
|
rustical_dav = { workspace = true, features = ["ical"] }
|
||||||
rustical_store.workspace = true
|
rustical_store.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
chrono-tz.workspace = true
|
chrono-tz.workspace = true
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ use axum::extract::State;
|
|||||||
use axum::{extract::Path, response::Response};
|
use axum::{extract::Path, response::Response};
|
||||||
use headers::{ContentType, HeaderMapExt};
|
use headers::{ContentType, HeaderMapExt};
|
||||||
use http::{HeaderValue, Method, StatusCode, header};
|
use http::{HeaderValue, Method, StatusCode, header};
|
||||||
use ical::generator::{Emitter, IcalCalendarBuilder};
|
use ical::builder::calendar::IcalCalendarBuilder;
|
||||||
|
use ical::component::CalendarInnerData;
|
||||||
|
use ical::generator::Emitter;
|
||||||
use ical::property::Property;
|
use ical::property::Property;
|
||||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||||
use rustical_ical::{CalendarObjectComponent, EventObject};
|
|
||||||
use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
|
use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -35,7 +36,7 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
let mut vtimezones = HashMap::new();
|
let mut vtimezones = HashMap::new();
|
||||||
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
|
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
|
||||||
|
|
||||||
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
|
let mut ical_calendar_builder = IcalCalendarBuilder::version("2.0")
|
||||||
.gregorian()
|
.gregorian()
|
||||||
.prodid("RustiCal");
|
.prodid("RustiCal");
|
||||||
if let Some(displayname) = calendar.meta.displayname {
|
if let Some(displayname) = calendar.meta.displayname {
|
||||||
@@ -60,34 +61,28 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for object in &objects {
|
for (_object_id, object) in &objects {
|
||||||
vtimezones.extend(object.get_vtimezones());
|
vtimezones.extend(object.get_inner().get_vtimezones());
|
||||||
match object.get_data() {
|
match object.get_inner().get_inner() {
|
||||||
CalendarObjectComponent::Event(EventObject { event, .. }, overrides) => {
|
CalendarInnerData::Event(main, overrides) => {
|
||||||
ical_calendar_builder = ical_calendar_builder.add_event(event.clone());
|
ical_calendar_builder = ical_calendar_builder
|
||||||
for ev_override in overrides {
|
.add_event(main.clone())
|
||||||
ical_calendar_builder =
|
.add_events(overrides.iter().cloned());
|
||||||
ical_calendar_builder.add_event(ev_override.event.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
CalendarObjectComponent::Todo(todo, overrides) => {
|
CalendarInnerData::Todo(main, overrides) => {
|
||||||
ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
|
ical_calendar_builder = ical_calendar_builder
|
||||||
for ev_override in overrides {
|
.add_todo(main.clone())
|
||||||
ical_calendar_builder = ical_calendar_builder.add_todo(ev_override.clone());
|
.add_todos(overrides.iter().cloned());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
CalendarObjectComponent::Journal(journal, overrides) => {
|
CalendarInnerData::Journal(main, overrides) => {
|
||||||
ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
|
ical_calendar_builder = ical_calendar_builder
|
||||||
for ev_override in overrides {
|
.add_journal(main.clone())
|
||||||
ical_calendar_builder = ical_calendar_builder.add_journal(ev_override.clone());
|
.add_journals(overrides.iter().cloned());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for vtimezone in vtimezones.into_values() {
|
ical_calendar_builder = ical_calendar_builder.add_timezones(vtimezones.into_values().cloned());
|
||||||
ical_calendar_builder = ical_calendar_builder.add_tz(vtimezone.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
let ical_calendar = ical_calendar_builder
|
let ical_calendar = ical_calendar_builder
|
||||||
.build()
|
.build()
|
||||||
@@ -95,7 +90,7 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
|
|
||||||
let mut resp = Response::builder().status(StatusCode::OK);
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap());
|
||||||
|
|
||||||
let filename = format!("{}_{}.ics", calendar.principal, calendar.id);
|
let filename = format!("{}_{}.ics", calendar.principal, calendar.id);
|
||||||
let filename = utf8_percent_encode(&filename, CONTROLS);
|
let filename = utf8_percent_encode(&filename, CONTROLS);
|
||||||
|
|||||||
@@ -82,7 +82,10 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
let objects = expanded_cals
|
let objects = expanded_cals
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|cal| cal.generate())
|
.map(|cal| cal.generate())
|
||||||
.map(|ics| CalendarObject::from_ics(ics, None))
|
.map(|ics| {
|
||||||
|
CalendarObject::from_ics(ics)
|
||||||
|
.map(|object| (object.get_inner().get_uid().to_owned(), object))
|
||||||
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
let new_cal = Calendar {
|
let new_cal = Calendar {
|
||||||
principal,
|
principal,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
store: &C,
|
store: &C,
|
||||||
) -> Result<(Vec<CalendarObject>, Vec<String>), Error> {
|
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>), Error> {
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
|||||||
let filename = filename.trim_start_matches('/');
|
let filename = filename.trim_start_matches('/');
|
||||||
if let Some(object_id) = filename.strip_suffix(".ics") {
|
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||||
match store.get_object(principal, cal_id, object_id, false).await {
|
match store.get_object(principal, cal_id, object_id, false).await {
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push((object_id.to_owned(), object)),
|
||||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
|
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use crate::calendar::methods::report::calendar_query::{
|
|||||||
TimeRangeElement,
|
TimeRangeElement,
|
||||||
prop_filter::{PropFilterElement, PropFilterable},
|
prop_filter::{PropFilterElement, PropFilterable},
|
||||||
};
|
};
|
||||||
use ical::parser::ical::component::IcalTimeZone;
|
use ical::{component::IcalCalendarObject, parser::ical::component::IcalTimeZone};
|
||||||
use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType};
|
use rustical_ical::{CalendarObject, CalendarObjectType};
|
||||||
use rustical_xml::XmlDeserialize;
|
use rustical_xml::XmlDeserialize;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
@@ -80,10 +80,11 @@ impl CompFilterable for CalendarObject {
|
|||||||
|
|
||||||
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool {
|
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool {
|
||||||
let mut matches = self
|
let mut matches = self
|
||||||
|
.get_inner()
|
||||||
.get_vtimezones()
|
.get_vtimezones()
|
||||||
.values()
|
.values()
|
||||||
.map(|tz| tz.matches(comp_filter))
|
.map(|tz| tz.matches(comp_filter))
|
||||||
.chain([self.get_data().matches(comp_filter)]);
|
.chain([self.matches(comp_filter)]);
|
||||||
|
|
||||||
if comp_filter.is_not_defined.is_some() {
|
if comp_filter.is_not_defined.is_some() {
|
||||||
matches.all(|x| x)
|
matches.all(|x| x)
|
||||||
@@ -107,7 +108,7 @@ impl CompFilterable for IcalTimeZone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompFilterable for CalendarObjectComponent {
|
impl CompFilterable for IcalCalendarObject {
|
||||||
fn get_comp_name(&self) -> &'static str {
|
fn get_comp_name(&self) -> &'static str {
|
||||||
CalendarObjectType::from(self).as_str()
|
CalendarObjectType::from(self).as_str()
|
||||||
}
|
}
|
||||||
@@ -120,7 +121,7 @@ impl CompFilterable for CalendarObjectComponent {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(end) = &time_range.end
|
if let Some(end) = &time_range.end
|
||||||
&& let Some(first_occurence) = self.get_first_occurence().unwrap_or(None)
|
&& let Some(first_occurence) = self.get_dtstart().unwrap_or(None)
|
||||||
&& **end < first_occurence.utc()
|
&& **end < first_occurence.utc()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -137,13 +138,11 @@ impl CompFilterable for CalendarObjectComponent {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
|
use rustical_dav::xml::{NegateCondition, TextCollation, TextMatchElement};
|
||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
|
|
||||||
use crate::calendar::methods::report::calendar_query::{
|
use crate::calendar::methods::report::calendar_query::{
|
||||||
CompFilterable, TextMatchElement, TimeRangeElement,
|
CompFilterElement, CompFilterable, PropFilterElement, TimeRangeElement,
|
||||||
comp_filter::CompFilterElement,
|
|
||||||
prop_filter::PropFilterElement,
|
|
||||||
text_match::{NegateCondition, TextCollation},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ICS: &str = r"BEGIN:VCALENDAR
|
const ICS: &str = r"BEGIN:VCALENDAR
|
||||||
@@ -168,7 +167,7 @@ END:VCALENDAR";
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_comp_filter_matching() {
|
fn test_comp_filter_matching() {
|
||||||
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
let object = CalendarObject::from_ics(ICS.to_string()).unwrap();
|
||||||
|
|
||||||
let comp_filter = CompFilterElement {
|
let comp_filter = CompFilterElement {
|
||||||
is_not_defined: Some(()),
|
is_not_defined: Some(()),
|
||||||
@@ -258,7 +257,7 @@ END:VCALENDAR";
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_comp_filter_time_range() {
|
fn test_comp_filter_time_range() {
|
||||||
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
let object = CalendarObject::from_ics(ICS.to_string()).unwrap();
|
||||||
|
|
||||||
let comp_filter = CompFilterElement {
|
let comp_filter = CompFilterElement {
|
||||||
is_not_defined: None,
|
is_not_defined: None,
|
||||||
@@ -313,7 +312,7 @@ END:VCALENDAR";
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_match_timezone() {
|
fn test_match_timezone() {
|
||||||
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
let object = CalendarObject::from_ics(ICS.to_string()).unwrap();
|
||||||
|
|
||||||
let comp_filter = CompFilterElement {
|
let comp_filter = CompFilterElement {
|
||||||
is_not_defined: None,
|
is_not_defined: None,
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
use crate::{
|
use super::comp_filter::{CompFilterElement, CompFilterable};
|
||||||
calendar::methods::report::calendar_query::{
|
use crate::calendar_object::CalendarObjectPropWrapperName;
|
||||||
TextMatchElement,
|
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
||||||
comp_filter::{CompFilterElement, CompFilterable},
|
|
||||||
},
|
|
||||||
calendar_object::CalendarObjectPropWrapperName,
|
|
||||||
};
|
|
||||||
use rustical_dav::xml::PropfindType;
|
|
||||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
use rustical_store::calendar_store::CalendarQuery;
|
use rustical_store::calendar_store::CalendarQuery;
|
||||||
use rustical_xml::XmlDeserialize;
|
use rustical_xml::XmlDeserialize;
|
||||||
|
|||||||
@@ -5,47 +5,39 @@ use rustical_store::CalendarStore;
|
|||||||
mod comp_filter;
|
mod comp_filter;
|
||||||
mod elements;
|
mod elements;
|
||||||
mod prop_filter;
|
mod prop_filter;
|
||||||
pub mod text_match;
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use comp_filter::{CompFilterElement, CompFilterable};
|
pub use comp_filter::{CompFilterElement, CompFilterable};
|
||||||
pub use elements::*;
|
pub use elements::*;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use prop_filter::{PropFilterElement, PropFilterable};
|
pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub use text_match::TextMatchElement;
|
|
||||||
|
|
||||||
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||||
cal_query: &CalendarQueryRequest,
|
cal_query: &CalendarQueryRequest,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
store: &C,
|
store: &C,
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
) -> Result<Vec<(String, CalendarObject)>, Error> {
|
||||||
let mut objects = store
|
let mut objects = store
|
||||||
.calendar_query(principal, cal_id, cal_query.into())
|
.calendar_query(principal, cal_id, cal_query.into())
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(filter) = &cal_query.filter {
|
if let Some(filter) = &cal_query.filter {
|
||||||
objects.retain(|object| filter.matches(object));
|
objects.retain(|(_, object)| filter.matches(object));
|
||||||
}
|
}
|
||||||
Ok(objects)
|
Ok(objects)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use rustical_dav::xml::PropElement;
|
use super::{
|
||||||
use rustical_xml::XmlDocument;
|
CalendarQueryRequest, FilterElement, ParamFilterElement, comp_filter::CompFilterElement,
|
||||||
|
prop_filter::PropFilterElement,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
calendar::methods::report::{
|
calendar::methods::report::ReportRequest,
|
||||||
ReportRequest,
|
|
||||||
calendar_query::{
|
|
||||||
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
|
|
||||||
comp_filter::CompFilterElement,
|
|
||||||
prop_filter::PropFilterElement,
|
|
||||||
text_match::{NegateCondition, TextCollation},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||||
};
|
};
|
||||||
|
use rustical_dav::xml::{NegateCondition, PropElement, TextCollation, TextMatchElement};
|
||||||
|
use rustical_xml::XmlDocument;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn calendar_query_7_8_7() {
|
fn calendar_query_7_8_7() {
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
use std::collections::HashMap;
|
use super::{ParamFilterElement, TimeRangeElement};
|
||||||
|
|
||||||
use ical::{
|
use ical::{
|
||||||
|
component::{CalendarInnerData, IcalCalendarObject},
|
||||||
generator::{IcalCalendar, IcalEvent},
|
generator::{IcalCalendar, IcalEvent},
|
||||||
parser::{
|
parser::{
|
||||||
Component,
|
Component,
|
||||||
ical::component::{IcalJournal, IcalTimeZone, IcalTodo},
|
ical::component::{IcalJournal, IcalTimeZone, IcalTodo},
|
||||||
},
|
},
|
||||||
property::Property,
|
property::Property,
|
||||||
|
types::CalDateTime,
|
||||||
};
|
};
|
||||||
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectComponent, UtcDateTime};
|
use rustical_dav::xml::TextMatchElement;
|
||||||
|
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||||
use rustical_xml::XmlDeserialize;
|
use rustical_xml::XmlDeserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
use crate::calendar::methods::report::calendar_query::{
|
|
||||||
ParamFilterElement, TextMatchElement, TimeRangeElement,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -82,7 +81,7 @@ pub trait PropFilterable {
|
|||||||
|
|
||||||
impl PropFilterable for CalendarObject {
|
impl PropFilterable for CalendarObject {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
Self::get_property(self, name)
|
self.get_property(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +115,12 @@ impl PropFilterable for IcalTimeZone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropFilterable for CalendarObjectComponent {
|
impl PropFilterable for IcalCalendarObject {
|
||||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
match self {
|
match self.get_inner() {
|
||||||
Self::Event(event, _) => PropFilterable::get_property(&event.event, name),
|
CalendarInnerData::Event(event, _) => PropFilterable::get_property(event, name),
|
||||||
Self::Todo(todo, _) => PropFilterable::get_property(todo, name),
|
CalendarInnerData::Todo(todo, _) => PropFilterable::get_property(todo, name),
|
||||||
Self::Journal(journal, _) => PropFilterable::get_property(journal, name),
|
CalendarInnerData::Journal(journal, _) => PropFilterable::get_property(journal, name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl ReportRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn objects_response(
|
fn objects_response(
|
||||||
objects: Vec<CalendarObject>,
|
objects: Vec<(String, CalendarObject)>,
|
||||||
not_found: Vec<String>,
|
not_found: Vec<String>,
|
||||||
path: &str,
|
path: &str,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -60,11 +60,12 @@ fn objects_response(
|
|||||||
prop: &PropfindType<CalendarObjectPropWrapperName>,
|
prop: &PropfindType<CalendarObjectPropWrapperName>,
|
||||||
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
|
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
|
||||||
let mut responses = Vec::new();
|
let mut responses = Vec::new();
|
||||||
for object in objects {
|
for (object_id, object) in objects {
|
||||||
let path = format!("{}/{}.ics", path, object.get_id());
|
let path = format!("{}/{}.ics", path, &object_id);
|
||||||
responses.push(
|
responses.push(
|
||||||
CalendarObjectResource {
|
CalendarObjectResource {
|
||||||
object,
|
object,
|
||||||
|
object_id,
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
}
|
}
|
||||||
.propfind(&path, prop, None, puri, user)?,
|
.propfind(&path, prop, None, puri, user)?,
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ pub async fn handle_sync_collection<C: CalendarStore>(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut responses = Vec::new();
|
let mut responses = Vec::new();
|
||||||
for object in new_objects {
|
for (object_id, object) in new_objects {
|
||||||
let path = format!("{}/{}.ics", path, object.get_id());
|
let path = format!("{}/{}.ics", path, &object_id);
|
||||||
responses.push(
|
responses.push(
|
||||||
CalendarObjectResource {
|
CalendarObjectResource {
|
||||||
object,
|
object,
|
||||||
|
object_id,
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
}
|
}
|
||||||
.propfind(&path, &sync_collection.prop, None, puri, user)?,
|
.propfind(&path, &sync_collection.prop, None, puri, user)?,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
|
use rustical_dav::xml::TextCollation;
|
||||||
use rustical_ical::CalendarObjectType;
|
use rustical_ical::CalendarObjectType;
|
||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
use strum_macros::VariantArray;
|
use strum_macros::VariantArray;
|
||||||
|
|
||||||
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
|
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
|
||||||
pub struct SupportedCalendarComponent {
|
pub struct SupportedCalendarComponent {
|
||||||
#[xml(ty = "attr")]
|
#[xml(ty = "attr")]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::calendar::prop::{ReportMethod, SupportedCollationSet};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
use ical::IcalParser;
|
use ical::IcalParser;
|
||||||
|
use ical::types::CalDateTime;
|
||||||
use rustical_dav::extensions::{
|
use rustical_dav::extensions::{
|
||||||
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
||||||
};
|
};
|
||||||
@@ -11,7 +12,6 @@ use rustical_dav::privileges::UserPrivilegeSet;
|
|||||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
|
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
|
||||||
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
||||||
use rustical_ical::CalDateTime;
|
|
||||||
use rustical_store::Calendar;
|
use rustical_store::Calendar;
|
||||||
use rustical_store::auth::Principal;
|
use rustical_store::auth::Principal;
|
||||||
use rustical_xml::{EnumVariants, PropName};
|
use rustical_xml::{EnumVariants, PropName};
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
|
|||||||
.get_objects(principal, cal_id)
|
.get_objects(principal, cal_id)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|object| CalendarObjectResource {
|
.map(|(object_id, object)| CalendarObjectResource {
|
||||||
object,
|
object,
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
|
object_id,
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub async fn get_event<C: CalendarStore>(
|
|||||||
let mut resp = Response::builder().status(StatusCode::OK);
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
|
hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
|
||||||
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap());
|
||||||
if matches!(method, Method::HEAD) {
|
if matches!(method, Method::HEAD) {
|
||||||
Ok(resp.body(Body::empty()).unwrap())
|
Ok(resp.body(Body::empty()).unwrap())
|
||||||
} else {
|
} else {
|
||||||
@@ -78,12 +78,12 @@ pub async fn put_event<C: CalendarStore>(
|
|||||||
true
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(object) = CalendarObject::from_ics(body.clone(), Some(object_id)) else {
|
let Ok(object) = CalendarObject::from_ics(body.clone()) else {
|
||||||
debug!("invalid calendar data:\n{body}");
|
debug!("invalid calendar data:\n{body}");
|
||||||
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
||||||
};
|
};
|
||||||
cal_store
|
cal_store
|
||||||
.put_object(principal, calendar_id, object, overwrite)
|
.put_object(principal, calendar_id, (object_id, object), overwrite)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(StatusCode::CREATED.into_response())
|
Ok(StatusCode::CREATED.into_response())
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use super::prop::{
|
|||||||
};
|
};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
|
use ical::generator::Emitter;
|
||||||
use rustical_dav::{
|
use rustical_dav::{
|
||||||
extensions::CommonPropertiesExtension,
|
extensions::CommonPropertiesExtension,
|
||||||
privileges::UserPrivilegeSet,
|
privileges::UserPrivilegeSet,
|
||||||
@@ -16,12 +17,13 @@ use rustical_store::auth::Principal;
|
|||||||
#[derive(Clone, From, Into)]
|
#[derive(Clone, From, Into)]
|
||||||
pub struct CalendarObjectResource {
|
pub struct CalendarObjectResource {
|
||||||
pub object: CalendarObject,
|
pub object: CalendarObject,
|
||||||
|
pub object_id: String,
|
||||||
pub principal: String,
|
pub principal: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for CalendarObjectResource {
|
impl ResourceName for CalendarObjectResource {
|
||||||
fn get_name(&self) -> String {
|
fn get_name(&self) -> String {
|
||||||
format!("{}.ics", self.object.get_id())
|
format!("{}.ics", self.object_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,10 +54,14 @@ impl Resource for CalendarObjectResource {
|
|||||||
}
|
}
|
||||||
CalendarObjectPropName::CalendarData(CalendarData { expand, .. }) => {
|
CalendarObjectPropName::CalendarData(CalendarData { expand, .. }) => {
|
||||||
CalendarObjectProp::CalendarData(if let Some(expand) = expand.as_ref() {
|
CalendarObjectProp::CalendarData(if let Some(expand) = expand.as_ref() {
|
||||||
self.object.expand_recurrence(
|
self.object
|
||||||
Some(expand.start.to_utc()),
|
.get_inner()
|
||||||
Some(expand.end.to_utc()),
|
.expand_recurrence(
|
||||||
)?
|
Some(expand.start.to_utc()),
|
||||||
|
Some(expand.end.to_utc()),
|
||||||
|
)
|
||||||
|
.map_err(rustical_ical::Error::ParserError)?
|
||||||
|
.generate()
|
||||||
} else {
|
} else {
|
||||||
self.object.get_ics().to_owned()
|
self.object.get_ics().to_owned()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
|||||||
Ok(CalendarObjectResource {
|
Ok(CalendarObjectResource {
|
||||||
object,
|
object,
|
||||||
principal: principal.to_owned(),
|
principal: principal.to_owned(),
|
||||||
|
object_id: object_id.to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ base64.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
rustical_dav.workspace = true
|
rustical_dav = { workspace = true, features = ["ical"] }
|
||||||
rustical_store.workspace = true
|
rustical_store.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ pub async fn get_object<AS: AddressbookStore>(
|
|||||||
let mut resp = Response::builder().status(StatusCode::OK);
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
|
hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
|
||||||
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
hdrs.typed_insert(ContentType::from_str("text/vcard; charset=utf-8").unwrap());
|
||||||
if matches!(method, Method::HEAD) {
|
if matches!(method, Method::HEAD) {
|
||||||
Ok(resp.body(Body::empty()).unwrap())
|
Ok(resp.body(Body::empty()).unwrap())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
|||||||
|
|
||||||
let mut resp = Response::builder().status(StatusCode::OK);
|
let mut resp = Response::builder().status(StatusCode::OK);
|
||||||
let hdrs = resp.headers_mut().unwrap();
|
let hdrs = resp.headers_mut().unwrap();
|
||||||
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
hdrs.typed_insert(ContentType::from_str("text/vcard; charset=utf-8").unwrap());
|
||||||
let filename = format!("{principal}_{addressbook_id}.vcf");
|
let filename = format!("{principal}_{addressbook_id}.vcf");
|
||||||
let filename = utf8_percent_encode(&filename, CONTROLS);
|
let filename = utf8_percent_encode(&filename, CONTROLS);
|
||||||
hdrs.insert(
|
hdrs.insert(
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
use crate::{
|
||||||
|
address_object::AddressObjectPropWrapperName,
|
||||||
|
addressbook::methods::report::addressbook_query::PropFilterElement,
|
||||||
|
};
|
||||||
|
use rustical_dav::xml::{PropfindType, TextMatchElement};
|
||||||
|
use rustical_ical::{AddressObject, UtcDateTime};
|
||||||
|
use rustical_xml::XmlDeserialize;
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct TimeRangeElement {
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub(crate) start: Option<UtcDateTime>,
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub(crate) end: Option<UtcDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.3
|
||||||
|
pub struct ParamFilterElement {
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
pub(crate) is_not_defined: Option<()>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
pub(crate) text_match: Option<TextMatchElement>,
|
||||||
|
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub(crate) name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
// <!ELEMENT filter (prop-filter*)>
|
||||||
|
// <!ATTLIST filter test (anyof | allof) "anyof">
|
||||||
|
// <!-- test value:
|
||||||
|
// anyof logical OR for prop-filter matches
|
||||||
|
// allof logical AND for prop-filter matches -->
|
||||||
|
pub struct FilterElement {
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub anyof: Option<String>,
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub allof: Option<String>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||||
|
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterElement {
|
||||||
|
#[must_use]
|
||||||
|
pub fn matches(&self, addr_object: &AddressObject) -> bool {
|
||||||
|
let allof = match (self.allof.is_some(), self.anyof.is_some()) {
|
||||||
|
(true, false) => true,
|
||||||
|
(false, _) => false,
|
||||||
|
(true, true) => panic!("wat"),
|
||||||
|
};
|
||||||
|
let mut results = self
|
||||||
|
.prop_filter
|
||||||
|
.iter()
|
||||||
|
.map(|prop_filter| prop_filter.match_component(addr_object));
|
||||||
|
if allof {
|
||||||
|
results.all(|x| x)
|
||||||
|
} else {
|
||||||
|
results.any(|x| x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
// <!ELEMENT addressbook-query ((DAV:allprop |
|
||||||
|
// DAV:propname |
|
||||||
|
// DAV:prop)?, filter, limit?)>
|
||||||
|
pub struct AddressbookQueryRequest {
|
||||||
|
#[xml(ty = "untagged")]
|
||||||
|
pub prop: PropfindType<AddressObjectPropWrapperName>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
pub(crate) filter: FilterElement,
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use crate::Error;
|
||||||
|
mod elements;
|
||||||
|
mod prop_filter;
|
||||||
|
pub use elements::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||||
|
use rustical_ical::AddressObject;
|
||||||
|
use rustical_store::AddressbookStore;
|
||||||
|
|
||||||
|
pub async fn get_objects_addressbook_query<AS: AddressbookStore>(
|
||||||
|
addr_query: &AddressbookQueryRequest,
|
||||||
|
principal: &str,
|
||||||
|
addressbook_id: &str,
|
||||||
|
store: &AS,
|
||||||
|
) -> Result<Vec<AddressObject>, Error> {
|
||||||
|
let mut objects = store.get_objects(principal, addressbook_id).await?;
|
||||||
|
objects.retain(|object| addr_query.filter.matches(object));
|
||||||
|
Ok(objects)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use super::ParamFilterElement;
|
||||||
|
use ical::{parser::Component, property::Property};
|
||||||
|
use rustical_dav::xml::TextMatchElement;
|
||||||
|
use rustical_ical::AddressObject;
|
||||||
|
use rustical_xml::XmlDeserialize;
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
// <!ELEMENT prop-filter (is-not-defined |
|
||||||
|
// (text-match*, param-filter*))>
|
||||||
|
//
|
||||||
|
// <!ATTLIST prop-filter name CDATA #REQUIRED
|
||||||
|
// test (anyof | allof) "anyof">
|
||||||
|
// <!-- name value: a vCard property name (e.g., "NICKNAME")
|
||||||
|
// test value:
|
||||||
|
// anyof logical OR for text-match/param-filter matches
|
||||||
|
// allof logical AND for text-match/param-filter matches -->
|
||||||
|
pub struct PropFilterElement {
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
pub(crate) is_not_defined: Option<()>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||||
|
pub(crate) text_match: Vec<TextMatchElement>,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
||||||
|
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||||
|
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub(crate) name: String,
|
||||||
|
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub anyof: Option<String>,
|
||||||
|
#[xml(ty = "attr")]
|
||||||
|
pub allof: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterElement {
|
||||||
|
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||||
|
let property = comp.get_property(&self.name);
|
||||||
|
let _property = match (self.is_not_defined.is_some(), property) {
|
||||||
|
// We are the component that's not supposed to be defined
|
||||||
|
(true, Some(_))
|
||||||
|
// We don't match
|
||||||
|
| (false, None) => return false,
|
||||||
|
// We shall not be and indeed we aren't
|
||||||
|
(true, None) => return true,
|
||||||
|
(false, Some(property)) => property
|
||||||
|
};
|
||||||
|
|
||||||
|
let _allof = match (self.allof.is_some(), self.anyof.is_some()) {
|
||||||
|
(true, false) => true,
|
||||||
|
(false, _) => false,
|
||||||
|
(true, true) => panic!("wat"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: IMPLEMENT
|
||||||
|
// if let Some(text_match) = &self.text_match
|
||||||
|
// && !text_match.match_property(property)
|
||||||
|
// {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: param-filter
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PropFilterable {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropFilterable for AddressObject {
|
||||||
|
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
|
self.get_vcard().get_property(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
CardDavPrincipalUri, Error, address_object::AddressObjectPropWrapperName,
|
CardDavPrincipalUri, Error,
|
||||||
addressbook::AddressbookResourceService,
|
address_object::{
|
||||||
|
AddressObjectPropWrapper, AddressObjectPropWrapperName, resource::AddressObjectResource,
|
||||||
|
},
|
||||||
|
addressbook::{
|
||||||
|
AddressbookResourceService,
|
||||||
|
methods::report::addressbook_query::{
|
||||||
|
AddressbookQueryRequest, get_objects_addressbook_query,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget};
|
use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -8,19 +16,30 @@ use axum::{
|
|||||||
extract::{OriginalUri, Path, State},
|
extract::{OriginalUri, Path, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest};
|
use http::StatusCode;
|
||||||
|
use rustical_dav::{
|
||||||
|
resource::{PrincipalUri, Resource},
|
||||||
|
xml::{
|
||||||
|
MultistatusElement, PropfindType, multistatus::ResponseElement,
|
||||||
|
sync_collection::SyncCollectionRequest,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use rustical_ical::AddressObject;
|
||||||
use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
|
use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
|
||||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||||
use sync_collection::handle_sync_collection;
|
use sync_collection::handle_sync_collection;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
mod addressbook_multiget;
|
mod addressbook_multiget;
|
||||||
|
mod addressbook_query;
|
||||||
mod sync_collection;
|
mod sync_collection;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
||||||
pub(crate) enum ReportRequest {
|
pub(crate) enum ReportRequest {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
AddressbookMultiget(AddressbookMultigetRequest),
|
AddressbookMultiget(AddressbookMultigetRequest),
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
|
AddressbookQuery(AddressbookQueryRequest),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
SyncCollection(SyncCollectionRequest<AddressObjectPropWrapperName>),
|
SyncCollection(SyncCollectionRequest<AddressObjectPropWrapperName>),
|
||||||
}
|
}
|
||||||
@@ -29,11 +48,49 @@ impl ReportRequest {
|
|||||||
const fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
|
const fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
|
||||||
match self {
|
match self {
|
||||||
Self::AddressbookMultiget(AddressbookMultigetRequest { prop, .. })
|
Self::AddressbookMultiget(AddressbookMultigetRequest { prop, .. })
|
||||||
| Self::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
|
| Self::SyncCollection(SyncCollectionRequest { prop, .. })
|
||||||
|
| Self::AddressbookQuery(AddressbookQueryRequest { prop, .. }) => prop,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn objects_response(
|
||||||
|
objects: Vec<AddressObject>,
|
||||||
|
not_found: Vec<String>,
|
||||||
|
path: &str,
|
||||||
|
principal: &str,
|
||||||
|
puri: &impl PrincipalUri,
|
||||||
|
user: &Principal,
|
||||||
|
prop: &PropfindType<AddressObjectPropWrapperName>,
|
||||||
|
) -> Result<MultistatusElement<AddressObjectPropWrapper, String>, Error> {
|
||||||
|
let mut responses = Vec::new();
|
||||||
|
for object in objects {
|
||||||
|
let path = format!("{}/{}.vcf", path, object.get_id());
|
||||||
|
responses.push(
|
||||||
|
AddressObjectResource {
|
||||||
|
object,
|
||||||
|
principal: principal.to_owned(),
|
||||||
|
}
|
||||||
|
.propfind(&path, prop, None, puri, user)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let not_found_responses = not_found
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| ResponseElement {
|
||||||
|
href: path,
|
||||||
|
status: Some(StatusCode::NOT_FOUND),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(MultistatusElement {
|
||||||
|
responses,
|
||||||
|
member_responses: not_found_responses,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(addr_store))]
|
#[instrument(skip(addr_store))]
|
||||||
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
|
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
|
||||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||||
@@ -75,13 +132,34 @@ pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
ReportRequest::AddressbookQuery(addr_query) => {
|
||||||
|
let objects = get_objects_addressbook_query(
|
||||||
|
addr_query,
|
||||||
|
&principal,
|
||||||
|
&addressbook_id,
|
||||||
|
addr_store.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
objects_response(
|
||||||
|
objects,
|
||||||
|
vec![],
|
||||||
|
uri.path(),
|
||||||
|
&principal,
|
||||||
|
&puri,
|
||||||
|
&user,
|
||||||
|
&addr_query.prop,
|
||||||
|
)?
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::address_object::AddressObjectPropName;
|
use crate::{
|
||||||
|
address_object::AddressObjectPropName,
|
||||||
|
addressbook::methods::report::addressbook_query::{FilterElement, PropFilterElement},
|
||||||
|
};
|
||||||
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
|
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -144,4 +222,46 @@ mod tests {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xml_addressbook_query() {
|
||||||
|
let report_request = ReportRequest::parse_str(
|
||||||
|
r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<card:addressbook-query xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:d="DAV:">
|
||||||
|
<d:prop>
|
||||||
|
<d:getetag/>
|
||||||
|
</d:prop>
|
||||||
|
<card:filter>
|
||||||
|
<card:prop-filter name="FN"/>
|
||||||
|
</card:filter>
|
||||||
|
</card:addressbook-query>
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
report_request,
|
||||||
|
ReportRequest::AddressbookQuery(AddressbookQueryRequest {
|
||||||
|
prop: rustical_dav::xml::PropfindType::Prop(PropElement(
|
||||||
|
vec![AddressObjectPropWrapperName::AddressObject(
|
||||||
|
AddressObjectPropName::Getetag
|
||||||
|
),],
|
||||||
|
vec![]
|
||||||
|
)),
|
||||||
|
filter: FilterElement {
|
||||||
|
anyof: None,
|
||||||
|
allof: None,
|
||||||
|
prop_filter: vec![PropFilterElement {
|
||||||
|
name: "FN".to_owned(),
|
||||||
|
is_not_defined: None,
|
||||||
|
text_match: vec![],
|
||||||
|
param_filter: vec![],
|
||||||
|
allof: None,
|
||||||
|
anyof: None
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,3 +28,7 @@ headers.workspace = true
|
|||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
matchit.workspace = true
|
matchit.workspace = true
|
||||||
matchit-serde.workspace = true
|
matchit-serde.workspace = true
|
||||||
|
ical = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
ical = ["dep:ical"]
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ mod report_set;
|
|||||||
pub use report_set::SupportedReportSet;
|
pub use report_set::SupportedReportSet;
|
||||||
mod group;
|
mod group;
|
||||||
pub use group::*;
|
pub use group::*;
|
||||||
|
#[cfg(feature = "ical")]
|
||||||
|
mod text_match;
|
||||||
|
#[cfg(feature = "ical")]
|
||||||
|
pub use text_match::*;
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ pub struct TextMatchElement {
|
|||||||
#[xml(ty = "attr", default = "Default::default")]
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
pub collation: TextCollation,
|
pub collation: TextCollation,
|
||||||
#[xml(ty = "attr", default = "Default::default")]
|
#[xml(ty = "attr", default = "Default::default")]
|
||||||
pub(crate) negate_condition: NegateCondition,
|
pub negate_condition: NegateCondition,
|
||||||
#[xml(ty = "text")]
|
#[xml(ty = "text")]
|
||||||
pub(crate) needle: String,
|
pub needle: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextMatchElement {
|
impl TextMatchElement {
|
||||||
@@ -90,7 +90,7 @@ impl TextMatchElement {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
|
use super::TextCollation;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_collation() {
|
fn test_collation() {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.5",
|
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.6",
|
||||||
"lit": "npm:lit@^3.3.1",
|
"lit": "npm:lit@^3.3.1",
|
||||||
"vite": "npm:vite@^7.3.0"
|
"vite": "npm:vite@^7.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
8
crates/frontend/js-components/deno.lock
generated
8
crates/frontend/js-components/deno.lock
generated
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.3.0__picomatch@4.0.3",
|
"npm:@deno/vite-plugin@^1.0.6": "1.0.6_vite@7.3.0__picomatch@4.0.3",
|
||||||
"npm:lit@^3.3.1": "3.3.1",
|
"npm:lit@^3.3.1": "3.3.1",
|
||||||
"npm:vite@*": "7.3.0_picomatch@4.0.3",
|
"npm:vite@*": "7.3.0_picomatch@4.0.3",
|
||||||
"npm:vite@^7.3.0": "7.3.0_picomatch@4.0.3"
|
"npm:vite@^7.3.0": "7.3.0_picomatch@4.0.3"
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"@deno/vite-plugin@1.0.5_vite@7.3.0__picomatch@4.0.3": {
|
"@deno/vite-plugin@1.0.6_vite@7.3.0__picomatch@4.0.3": {
|
||||||
"integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==",
|
"integrity": "sha512-Sh5XqvFuKAwjARTesi0n6xRpEXm1V0UeqKh+SxIrexCofxOaieNDMqXZD02RiZCg0mrJ43V8eCMuVrDfq6mLmg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"vite"
|
"vite"
|
||||||
]
|
]
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:@deno/vite-plugin@^1.0.5",
|
"npm:@deno/vite-plugin@^1.0.6",
|
||||||
"npm:lit@^3.3.1",
|
"npm:lit@^3.3.1",
|
||||||
"npm:vite@^7.3.0"
|
"npm:vite@^7.3.0"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::{CalDateTime, LOCAL_DATE};
|
|
||||||
use crate::{CalendarObject, Error};
|
use crate::{CalendarObject, Error};
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use ical::generator::Emitter;
|
use ical::generator::Emitter;
|
||||||
@@ -6,8 +5,9 @@ use ical::parser::{
|
|||||||
Component,
|
Component,
|
||||||
vcard::{self, component::VcardContact},
|
vcard::{self, component::VcardContact},
|
||||||
};
|
};
|
||||||
|
use ical::types::CalDate;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{collections::HashMap, io::BufReader};
|
use std::io::BufReader;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AddressObject {
|
pub struct AddressObject {
|
||||||
@@ -64,15 +64,15 @@ impl AddressObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> {
|
pub fn get_anniversary(&self) -> Option<(CalDate, bool)> {
|
||||||
let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
|
let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
|
||||||
CalDateTime::parse_vcard(prop).ok()
|
CalDate::parse_vcard(prop).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> {
|
pub fn get_birthday(&self) -> Option<(CalDate, bool)> {
|
||||||
let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
|
let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
|
||||||
CalDateTime::parse_vcard(prop).ok()
|
CalDate::parse_vcard(prop).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -87,19 +87,14 @@ impl AddressObject {
|
|||||||
let Some(fullname) = self.get_full_name() else {
|
let Some(fullname) = self.get_full_name() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let anniversary = anniversary.date();
|
|
||||||
let year = contains_year.then_some(anniversary.year());
|
let year = contains_year.then_some(anniversary.year());
|
||||||
let anniversary_start = anniversary.format(LOCAL_DATE);
|
let anniversary_start = anniversary.format();
|
||||||
let anniversary_end = anniversary
|
let anniversary_end = anniversary.succ_opt().unwrap_or(anniversary).format();
|
||||||
.succ_opt()
|
|
||||||
.unwrap_or(anniversary)
|
|
||||||
.format(LOCAL_DATE);
|
|
||||||
let uid = format!("{}-anniversary", self.get_id());
|
let uid = format!("{}-anniversary", self.get_id());
|
||||||
|
|
||||||
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
|
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
|
||||||
Some(CalendarObject::from_ics(
|
Some(CalendarObject::from_ics(format!(
|
||||||
format!(
|
r"BEGIN:VCALENDAR
|
||||||
r"BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
||||||
@@ -117,9 +112,7 @@ DESCRIPTION:💍 {fullname}{year_suffix}
|
|||||||
END:VALARM
|
END:VALARM
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR",
|
END:VCALENDAR",
|
||||||
),
|
))?)
|
||||||
None,
|
|
||||||
)?)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
@@ -132,16 +125,14 @@ END:VCALENDAR",
|
|||||||
let Some(fullname) = self.get_full_name() else {
|
let Some(fullname) = self.get_full_name() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let birthday = birthday.date();
|
|
||||||
let year = contains_year.then_some(birthday.year());
|
let year = contains_year.then_some(birthday.year());
|
||||||
let birthday_start = birthday.format(LOCAL_DATE);
|
let birthday_start = birthday.format();
|
||||||
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
|
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format();
|
||||||
let uid = format!("{}-birthday", self.get_id());
|
let uid = format!("{}-birthday", self.get_id());
|
||||||
|
|
||||||
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
|
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
|
||||||
Some(CalendarObject::from_ics(
|
Some(CalendarObject::from_ics(format!(
|
||||||
format!(
|
r"BEGIN:VCALENDAR
|
||||||
r"BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
||||||
@@ -159,9 +150,7 @@ DESCRIPTION:🎂 {fullname}{year_suffix}
|
|||||||
END:VALARM
|
END:VALARM
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR",
|
END:VCALENDAR",
|
||||||
),
|
))?)
|
||||||
None,
|
|
||||||
)?)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
@@ -169,14 +158,19 @@ END:VCALENDAR",
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get significant dates associated with this address object
|
/// Get significant dates associated with this address object
|
||||||
pub fn get_significant_dates(&self) -> Result<HashMap<&'static str, CalendarObject>, Error> {
|
pub fn get_significant_dates(&self) -> Result<Vec<(String, CalendarObject)>, Error> {
|
||||||
let mut out = HashMap::new();
|
let mut out = vec![];
|
||||||
if let Some(birthday) = self.get_birthday_object()? {
|
if let Some(birthday) = self.get_birthday_object()? {
|
||||||
out.insert("birthday", birthday);
|
out.push((birthday.get_inner().get_uid().to_owned(), birthday));
|
||||||
}
|
}
|
||||||
if let Some(anniversary) = self.get_anniversary_object()? {
|
if let Some(anniversary) = self.get_anniversary_object()? {
|
||||||
out.insert("anniversary", anniversary);
|
out.push((anniversary.get_inner().get_uid().to_owned(), anniversary));
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn get_vcard(&self) -> &VcardContact {
|
||||||
|
&self.vcard
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use axum::{http::StatusCode, response::IntoResponse};
|
use axum::{http::StatusCode, response::IntoResponse};
|
||||||
|
use ical::types::CalDateTimeError;
|
||||||
use crate::CalDateTimeError;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|||||||
@@ -20,22 +20,6 @@ impl EventObject {
|
|||||||
self.event.get_uid()
|
self.event.get_uid()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_dtstart(&self) -> Result<Option<CalDateTime>, Error> {
|
|
||||||
if let Some(dtstart) = self.event.get_dtstart() {
|
|
||||||
Ok(Some(CalDateTime::parse_prop(dtstart, &self.timezones)?))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dtend(&self) -> Result<Option<CalDateTime>, Error> {
|
|
||||||
if let Some(dtend) = self.event.get_dtend() {
|
|
||||||
Ok(Some(CalDateTime::parse_prop(dtend, &self.timezones)?))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||||
if self.event.get_rrule().is_some() {
|
if self.event.get_rrule().is_some() {
|
||||||
// TODO: understand recurrence rules
|
// TODO: understand recurrence rules
|
||||||
@@ -51,134 +35,6 @@ impl EventObject {
|
|||||||
let first_occurence = self.get_dtstart()?;
|
let first_occurence = self.get_dtstart()?;
|
||||||
Ok(first_occurence.map(|first_occurence| first_occurence + duration))
|
Ok(first_occurence.map(|first_occurence| first_occurence + duration))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recurrence_ruleset(&self) -> Result<Option<rrule::RRuleSet>, Error> {
|
|
||||||
let dtstart: DateTime<rrule::Tz> = if let Some(dtstart) = self.get_dtstart()? {
|
|
||||||
if let Some(dtend) = self.get_dtend()? {
|
|
||||||
// DTSTART and DTEND MUST have the same timezone
|
|
||||||
assert_eq!(dtstart.timezone(), dtend.timezone());
|
|
||||||
}
|
|
||||||
|
|
||||||
dtstart
|
|
||||||
.as_datetime()
|
|
||||||
.with_timezone(&dtstart.timezone().into())
|
|
||||||
} else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut rrule_set = RRuleSet::new(dtstart);
|
|
||||||
|
|
||||||
for prop in &self.event.properties {
|
|
||||||
rrule_set = match prop.name.as_str() {
|
|
||||||
"RRULE" => {
|
|
||||||
let rrule = RRule::from_str(prop.value.as_ref().ok_or_else(|| {
|
|
||||||
Error::RRuleError(rrule::ParseError::MissingDateGenerationRules.into())
|
|
||||||
})?)?
|
|
||||||
.validate(dtstart)
|
|
||||||
.unwrap();
|
|
||||||
rrule_set.rrule(rrule)
|
|
||||||
}
|
|
||||||
"RDATE" => {
|
|
||||||
let rdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
|
|
||||||
rrule_set.rdate(rdate)
|
|
||||||
}
|
|
||||||
"EXDATE" => {
|
|
||||||
let exdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
|
|
||||||
rrule_set.exdate(exdate)
|
|
||||||
}
|
|
||||||
_ => rrule_set,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(rrule_set))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expand_recurrence(
|
|
||||||
&self,
|
|
||||||
start: Option<DateTime<Utc>>,
|
|
||||||
end: Option<DateTime<Utc>>,
|
|
||||||
overrides: &[Self],
|
|
||||||
) -> Result<Vec<IcalEvent>, Error> {
|
|
||||||
if let Some(mut rrule_set) = self.recurrence_ruleset()? {
|
|
||||||
if let Some(start) = start {
|
|
||||||
rrule_set = rrule_set.after(start.with_timezone(&rrule::Tz::UTC));
|
|
||||||
}
|
|
||||||
if let Some(end) = end {
|
|
||||||
rrule_set = rrule_set.before(end.with_timezone(&rrule::Tz::UTC));
|
|
||||||
}
|
|
||||||
let mut events = vec![];
|
|
||||||
let dates = rrule_set.all(2048).dates;
|
|
||||||
let dtstart = self.get_dtstart()?.expect("We must have a DTSTART here");
|
|
||||||
let computed_duration = self
|
|
||||||
.get_dtend()?
|
|
||||||
.map(|dtend| dtend.as_datetime().into_owned() - dtstart.as_datetime().into_owned());
|
|
||||||
|
|
||||||
'recurrence: for date in dates {
|
|
||||||
let date = CalDateTime::from(date);
|
|
||||||
let dateformat = if dtstart.is_date() {
|
|
||||||
date.format_date()
|
|
||||||
} else {
|
|
||||||
date.format()
|
|
||||||
};
|
|
||||||
|
|
||||||
for ev_override in overrides {
|
|
||||||
if let Some(override_id) = &ev_override
|
|
||||||
.event
|
|
||||||
.get_recurrence_id()
|
|
||||||
.as_ref()
|
|
||||||
.expect("overrides have a recurrence id")
|
|
||||||
.value
|
|
||||||
&& override_id == &dateformat
|
|
||||||
{
|
|
||||||
// We have an override for this occurence
|
|
||||||
//
|
|
||||||
events.push(ev_override.event.clone());
|
|
||||||
continue 'recurrence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ev = self.event.clone().mutable();
|
|
||||||
ev.remove_property("RRULE");
|
|
||||||
ev.remove_property("RDATE");
|
|
||||||
ev.remove_property("EXDATE");
|
|
||||||
ev.remove_property("EXRULE");
|
|
||||||
let dtstart_prop = ev
|
|
||||||
.get_property("DTSTART")
|
|
||||||
.expect("We must have a DTSTART here")
|
|
||||||
.clone();
|
|
||||||
ev.remove_property("DTSTART");
|
|
||||||
ev.remove_property("DTEND");
|
|
||||||
|
|
||||||
ev.set_property(Property {
|
|
||||||
name: "RECURRENCE-ID".to_string(),
|
|
||||||
value: Some(dateformat.clone()),
|
|
||||||
params: vec![],
|
|
||||||
});
|
|
||||||
ev.set_property(Property {
|
|
||||||
name: "DTSTART".to_string(),
|
|
||||||
value: Some(dateformat),
|
|
||||||
params: dtstart_prop.params.clone(),
|
|
||||||
});
|
|
||||||
if let Some(duration) = computed_duration {
|
|
||||||
let dtend = date + duration;
|
|
||||||
let dtendformat = if dtstart.is_date() {
|
|
||||||
dtend.format_date()
|
|
||||||
} else {
|
|
||||||
dtend.format()
|
|
||||||
};
|
|
||||||
ev.set_property(Property {
|
|
||||||
name: "DTEND".to_string(),
|
|
||||||
value: Some(dtendformat),
|
|
||||||
params: dtstart_prop.params,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
events.push(ev.verify()?);
|
|
||||||
}
|
|
||||||
Ok(events)
|
|
||||||
} else {
|
|
||||||
Ok(vec![self.event.clone()])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mod event;
|
|
||||||
mod object;
|
mod object;
|
||||||
|
mod object_type;
|
||||||
|
|
||||||
pub use event::*;
|
|
||||||
pub use object::*;
|
pub use object::*;
|
||||||
|
pub use object_type::*;
|
||||||
|
|||||||
@@ -1,195 +1,20 @@
|
|||||||
use super::EventObject;
|
use crate::CalendarObjectType;
|
||||||
use crate::CalDateTime;
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use chrono::DateTime;
|
use ical::component::IcalCalendarObject;
|
||||||
use chrono::Utc;
|
use ical::parser::Component;
|
||||||
use derive_more::Display;
|
|
||||||
use ical::generator::{Emitter, IcalCalendar};
|
|
||||||
use ical::parser::ical::component::IcalJournal;
|
|
||||||
use ical::parser::ical::component::IcalTimeZone;
|
|
||||||
use ical::parser::ical::component::IcalTodo;
|
|
||||||
use ical::property::Property;
|
use ical::property::Property;
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{collections::HashMap, io::BufReader};
|
use std::io::BufReader;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display)]
|
|
||||||
// specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6
|
|
||||||
pub enum CalendarObjectType {
|
|
||||||
#[serde(rename = "VEVENT")]
|
|
||||||
Event = 0,
|
|
||||||
#[serde(rename = "VTODO")]
|
|
||||||
Todo = 1,
|
|
||||||
#[serde(rename = "VJOURNAL")]
|
|
||||||
Journal = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CalendarObjectType {
|
|
||||||
#[must_use]
|
|
||||||
pub const fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Event => "VEVENT",
|
|
||||||
Self::Todo => "VTODO",
|
|
||||||
Self::Journal => "VJOURNAL",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rustical_xml::ValueSerialize for CalendarObjectType {
|
|
||||||
fn serialize(&self) -> String {
|
|
||||||
self.as_str().to_owned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rustical_xml::ValueDeserialize for CalendarObjectType {
|
|
||||||
fn deserialize(val: &str) -> std::result::Result<Self, rustical_xml::XmlError> {
|
|
||||||
match <String as rustical_xml::ValueDeserialize>::deserialize(val)?.as_str() {
|
|
||||||
"VEVENT" => Ok(Self::Event),
|
|
||||||
"VTODO" => Ok(Self::Todo),
|
|
||||||
"VJOURNAL" => Ok(Self::Journal),
|
|
||||||
_ => Err(rustical_xml::XmlError::InvalidValue(
|
|
||||||
rustical_xml::ParseValueError::Other(format!(
|
|
||||||
"Invalid value '{val}', must be VEVENT, VTODO, or VJOURNAL"
|
|
||||||
)),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum CalendarObjectComponent {
|
|
||||||
Event(EventObject, Vec<EventObject>),
|
|
||||||
Todo(IcalTodo, Vec<IcalTodo>),
|
|
||||||
Journal(IcalJournal, Vec<IcalJournal>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CalendarObjectComponent {
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_uid(&self) -> &str {
|
|
||||||
match &self {
|
|
||||||
// We've made sure before that the first component exists and all components share the
|
|
||||||
// same UID
|
|
||||||
Self::Todo(todo, _) => todo.get_uid(),
|
|
||||||
Self::Event(event, _) => event.event.get_uid(),
|
|
||||||
Self::Journal(journal, _) => journal.get_uid(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&CalendarObjectComponent> for CalendarObjectType {
|
|
||||||
fn from(value: &CalendarObjectComponent) -> Self {
|
|
||||||
match value {
|
|
||||||
CalendarObjectComponent::Event(..) => Self::Event,
|
|
||||||
CalendarObjectComponent::Todo(..) => Self::Todo,
|
|
||||||
CalendarObjectComponent::Journal(..) => Self::Journal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CalendarObjectComponent {
|
|
||||||
fn from_events(mut events: Vec<EventObject>) -> Result<Self, Error> {
|
|
||||||
let main_event = events
|
|
||||||
.extract_if(.., |event| event.event.get_recurrence_id().is_none())
|
|
||||||
.next()
|
|
||||||
.expect("there must be one main event");
|
|
||||||
let overrides = events;
|
|
||||||
for event in &overrides {
|
|
||||||
if event.get_uid() != main_event.get_uid() {
|
|
||||||
return Err(Error::InvalidData(
|
|
||||||
"Calendar object contains multiple UIDs".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if event.event.get_recurrence_id().is_none() {
|
|
||||||
return Err(Error::InvalidData(
|
|
||||||
"Calendar object can only contain one main component".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Self::Event(main_event, overrides))
|
|
||||||
}
|
|
||||||
fn from_todos(mut todos: Vec<IcalTodo>) -> Result<Self, Error> {
|
|
||||||
let main_todo = todos
|
|
||||||
.extract_if(.., |todo| todo.get_recurrence_id().is_none())
|
|
||||||
.next()
|
|
||||||
.expect("there must be one main event");
|
|
||||||
let overrides = todos;
|
|
||||||
for todo in &overrides {
|
|
||||||
if todo.get_uid() != main_todo.get_uid() {
|
|
||||||
return Err(Error::InvalidData(
|
|
||||||
"Calendar object contains multiple UIDs".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if todo.get_recurrence_id().is_none() {
|
|
||||||
return Err(Error::InvalidData(
|
|
||||||
"Calendar object can only contain one main component".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Self::Todo(main_todo, overrides))
|
|
||||||
}
|
|
||||||
fn from_journals(mut journals: Vec<IcalJournal>) -> Result<Self, Error> {
|
|
||||||
let main_journal = journals
|
|
||||||
.extract_if(.., |journal| journal.get_recurrence_id().is_none())
|
|
||||||
.next()
|
|
||||||
.expect("there must be one main event");
|
|
||||||
let overrides = journals;
|
|
||||||
for journal in &overrides {
|
|
||||||
if journal.get_uid() != main_journal.get_uid() {
|
|
||||||
return Err(Error::InvalidData(
|
|
||||||
"Calendar object contains multiple UIDs".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if journal.get_recurrence_id().is_none() {
|
|
||||||
return Err(Error::InvalidData(
|
|
||||||
"Calendar object can only contain one main component".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Self::Journal(main_journal, overrides))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
|
||||||
match &self {
|
|
||||||
Self::Event(main_event, overrides) => Ok(overrides
|
|
||||||
.iter()
|
|
||||||
.chain(std::iter::once(main_event))
|
|
||||||
.map(super::event::EventObject::get_dtstart)
|
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.min()),
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
|
||||||
match &self {
|
|
||||||
Self::Event(main_event, overrides) => Ok(overrides
|
|
||||||
.iter()
|
|
||||||
.chain(std::iter::once(main_event))
|
|
||||||
.map(super::event::EventObject::get_last_occurence)
|
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.max()),
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CalendarObject {
|
pub struct CalendarObject {
|
||||||
data: CalendarObjectComponent,
|
|
||||||
properties: Vec<Property>,
|
|
||||||
id: String,
|
|
||||||
ics: String,
|
ics: String,
|
||||||
vtimezones: HashMap<String, IcalTimeZone>,
|
inner: IcalCalendarObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CalendarObject {
|
impl CalendarObject {
|
||||||
pub fn from_ics(ics: String, id: Option<String>) -> Result<Self, Error> {
|
pub fn from_ics(ics: String) -> Result<Self, Error> {
|
||||||
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
|
let mut parser = ical::IcalObjectParser::new(BufReader::new(ics.as_bytes()));
|
||||||
let cal = parser.next().ok_or(Error::MissingCalendar)??;
|
let cal = parser.next().ok_or(Error::MissingCalendar)??;
|
||||||
if parser.next().is_some() {
|
if parser.next().is_some() {
|
||||||
return Err(Error::InvalidData(
|
return Err(Error::InvalidData(
|
||||||
@@ -197,85 +22,18 @@ impl CalendarObject {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if u8::from(!cal.events.is_empty())
|
Ok(Self { ics, inner: cal })
|
||||||
+ u8::from(!cal.todos.is_empty())
|
|
||||||
+ u8::from(!cal.journals.is_empty())
|
|
||||||
+ u8::from(!cal.free_busys.is_empty())
|
|
||||||
!= 1
|
|
||||||
{
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-4.1
|
|
||||||
return Err(Error::InvalidData(
|
|
||||||
"iCalendar object must have exactly one component type".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let timezones: HashMap<String, Option<chrono_tz::Tz>> = cal
|
|
||||||
.timezones
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|timezone| (timezone.get_tzid().to_owned(), (&timezone).try_into().ok()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let vtimezones = cal
|
|
||||||
.timezones
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|timezone| (timezone.get_tzid().to_owned(), timezone))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let data = if !cal.events.is_empty() {
|
|
||||||
CalendarObjectComponent::from_events(
|
|
||||||
cal.events
|
|
||||||
.into_iter()
|
|
||||||
.map(|event| EventObject {
|
|
||||||
event,
|
|
||||||
timezones: timezones.clone(),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)?
|
|
||||||
} else if !cal.todos.is_empty() {
|
|
||||||
CalendarObjectComponent::from_todos(cal.todos)?
|
|
||||||
} else if !cal.journals.is_empty() {
|
|
||||||
CalendarObjectComponent::from_journals(cal.journals)?
|
|
||||||
} else {
|
|
||||||
return Err(Error::InvalidData(
|
|
||||||
"iCalendar component type not supported :(".to_owned(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
id: id.unwrap_or_else(|| data.get_uid().to_owned()),
|
|
||||||
data,
|
|
||||||
properties: cal.properties,
|
|
||||||
ics,
|
|
||||||
vtimezones,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn get_vtimezones(&self) -> &HashMap<String, IcalTimeZone> {
|
pub const fn get_inner(&self) -> &IcalCalendarObject {
|
||||||
&self.vtimezones
|
&self.inner
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn get_data(&self) -> &CalendarObjectComponent {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_uid(&self) -> &str {
|
|
||||||
self.data.get_uid()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_id(&self) -> &str {
|
|
||||||
&self.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_etag(&self) -> String {
|
pub fn get_etag(&self) -> String {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(self.get_uid());
|
hasher.update(self.inner.get_uid());
|
||||||
hasher.update(self.get_ics());
|
hasher.update(self.get_ics());
|
||||||
format!("\"{:x}\"", hasher.finalize())
|
format!("\"{:x}\"", hasher.finalize())
|
||||||
}
|
}
|
||||||
@@ -285,47 +43,13 @@ impl CalendarObject {
|
|||||||
&self.ics
|
&self.ics
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_component_name(&self) -> &str {
|
|
||||||
self.get_object_type().as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_object_type(&self) -> CalendarObjectType {
|
pub fn get_object_type(&self) -> CalendarObjectType {
|
||||||
(&self.data).into()
|
(&self.inner).into()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
|
||||||
self.data.get_first_occurence()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
|
||||||
self.data.get_last_occurence()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expand_recurrence(
|
|
||||||
&self,
|
|
||||||
start: Option<DateTime<Utc>>,
|
|
||||||
end: Option<DateTime<Utc>>,
|
|
||||||
) -> Result<String, Error> {
|
|
||||||
// Only events can be expanded
|
|
||||||
match &self.data {
|
|
||||||
CalendarObjectComponent::Event(main_event, overrides) => {
|
|
||||||
let cal = IcalCalendar {
|
|
||||||
properties: self.properties.clone(),
|
|
||||||
events: main_event.expand_recurrence(start, end, overrides)?,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
Ok(cal.generate())
|
|
||||||
}
|
|
||||||
_ => Ok(self.get_ics().to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_property(&self, name: &str) -> Option<&Property> {
|
pub fn get_property(&self, name: &str) -> Option<&Property> {
|
||||||
self.properties
|
self.inner.get_property(name)
|
||||||
.iter()
|
|
||||||
.find(|property| property.name == name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
crates/ical/src/icalendar/object_type.rs
Normal file
56
crates/ical/src/icalendar/object_type.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use derive_more::Display;
|
||||||
|
use ical::component::{CalendarInnerData, IcalCalendarObject};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display)]
|
||||||
|
// specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6
|
||||||
|
pub enum CalendarObjectType {
|
||||||
|
#[serde(rename = "VEVENT")]
|
||||||
|
Event = 0,
|
||||||
|
#[serde(rename = "VTODO")]
|
||||||
|
Todo = 1,
|
||||||
|
#[serde(rename = "VJOURNAL")]
|
||||||
|
Journal = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalendarObjectType {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Event => "VEVENT",
|
||||||
|
Self::Todo => "VTODO",
|
||||||
|
Self::Journal => "VJOURNAL",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&IcalCalendarObject> for CalendarObjectType {
|
||||||
|
fn from(value: &IcalCalendarObject) -> Self {
|
||||||
|
match value.get_inner() {
|
||||||
|
CalendarInnerData::Event(..) => Self::Event,
|
||||||
|
CalendarInnerData::Todo(..) => Self::Todo,
|
||||||
|
CalendarInnerData::Journal(..) => Self::Journal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rustical_xml::ValueSerialize for CalendarObjectType {
|
||||||
|
fn serialize(&self) -> String {
|
||||||
|
self.as_str().to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rustical_xml::ValueDeserialize for CalendarObjectType {
|
||||||
|
fn deserialize(val: &str) -> std::result::Result<Self, rustical_xml::XmlError> {
|
||||||
|
match <String as rustical_xml::ValueDeserialize>::deserialize(val)?.as_str() {
|
||||||
|
"VEVENT" => Ok(Self::Event),
|
||||||
|
"VTODO" => Ok(Self::Todo),
|
||||||
|
"VJOURNAL" => Ok(Self::Journal),
|
||||||
|
_ => Err(rustical_xml::XmlError::InvalidValue(
|
||||||
|
rustical_xml::ParseValueError::Other(format!(
|
||||||
|
"Invalid value '{val}', must be VEVENT, VTODO, or VJOURNAL"
|
||||||
|
)),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
||||||
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
|
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
|
||||||
mod timestamp;
|
mod timestamp;
|
||||||
mod timezone;
|
|
||||||
pub use timestamp::*;
|
pub use timestamp::*;
|
||||||
pub use timezone::*;
|
|
||||||
|
|
||||||
mod icalendar;
|
mod icalendar;
|
||||||
pub use icalendar::*;
|
pub use icalendar::*;
|
||||||
|
|||||||
@@ -1,35 +1,8 @@
|
|||||||
use super::timezone::ICalTimezone;
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
use derive_more::derive::Deref;
|
use derive_more::derive::Deref;
|
||||||
use ical::property::Property;
|
|
||||||
use rustical_xml::{ValueDeserialize, ValueSerialize};
|
use rustical_xml::{ValueDeserialize, ValueSerialize};
|
||||||
use std::{borrow::Cow, collections::HashMap, ops::Add, sync::LazyLock};
|
|
||||||
|
|
||||||
static RE_VCARD_DATE_MM_DD: LazyLock<regex::Regex> =
|
|
||||||
LazyLock::new(|| regex::Regex::new(r"^--(?<m>\d{2})(?<d>\d{2})$").unwrap());
|
|
||||||
|
|
||||||
const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S";
|
|
||||||
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
|
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
|
||||||
pub const LOCAL_DATE: &str = "%Y%m%d";
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
|
||||||
pub enum CalDateTimeError {
|
|
||||||
#[error(
|
|
||||||
"Timezone has X-LIC-LOCATION property to specify a timezone from the Olson database, however its value {0} is invalid"
|
|
||||||
)]
|
|
||||||
InvalidOlson(String),
|
|
||||||
#[error("TZID {0} does not refer to a valid timezone")]
|
|
||||||
InvalidTZID(String),
|
|
||||||
#[error("Timestamp doesn't exist because of gap in local time")]
|
|
||||||
LocalTimeGap,
|
|
||||||
#[error("Datetime string {0} has an invalid format")]
|
|
||||||
InvalidDatetimeFormat(String),
|
|
||||||
#[error("Could not parse datetime {0}")]
|
|
||||||
ParseError(String),
|
|
||||||
#[error("Duration string {0} has an invalid format")]
|
|
||||||
InvalidDurationFormat(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deref, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Deref, PartialEq, Eq, Hash)]
|
||||||
pub struct UtcDateTime(pub DateTime<Utc>);
|
pub struct UtcDateTime(pub DateTime<Utc>);
|
||||||
@@ -54,367 +27,3 @@ impl ValueSerialize for UtcDateTime {
|
|||||||
format!("{}", self.0.format(UTC_DATE_TIME))
|
format!("{}", self.0.format(UTC_DATE_TIME))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum CalDateTime {
|
|
||||||
// Form 1, example: 19980118T230000 -> Local
|
|
||||||
// Form 2, example: 19980119T070000Z -> UTC
|
|
||||||
// Form 3, example: TZID=America/New_York:19980119T020000 -> Olson
|
|
||||||
// https://en.wikipedia.org/wiki/Tz_database
|
|
||||||
DateTime(DateTime<ICalTimezone>),
|
|
||||||
Date(NaiveDate, ICalTimezone),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CalDateTime> for DateTime<rrule::Tz> {
|
|
||||||
fn from(value: CalDateTime) -> Self {
|
|
||||||
value
|
|
||||||
.as_datetime()
|
|
||||||
.into_owned()
|
|
||||||
.with_timezone(&value.timezone().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DateTime<rrule::Tz>> for CalDateTime {
|
|
||||||
fn from(value: DateTime<rrule::Tz>) -> Self {
|
|
||||||
Self::DateTime(value.with_timezone(&value.timezone().into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for CalDateTime {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for CalDateTime {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
match (&self, &other) {
|
|
||||||
(Self::DateTime(a), Self::DateTime(b)) => a.cmp(b),
|
|
||||||
(Self::DateTime(a), Self::Date(..)) => a.cmp(&other.as_datetime()),
|
|
||||||
(Self::Date(..), Self::DateTime(b)) => self.as_datetime().as_ref().cmp(b),
|
|
||||||
(Self::Date(..), Self::Date(..)) => self.as_datetime().cmp(&other.as_datetime()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DateTime<Local>> for CalDateTime {
|
|
||||||
fn from(value: DateTime<Local>) -> Self {
|
|
||||||
Self::DateTime(value.with_timezone(&ICalTimezone::Local))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DateTime<Utc>> for CalDateTime {
|
|
||||||
fn from(value: DateTime<Utc>) -> Self {
|
|
||||||
Self::DateTime(value.with_timezone(&ICalTimezone::Olson(chrono_tz::UTC)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Add<Duration> for CalDateTime {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn add(self, duration: Duration) -> Self::Output {
|
|
||||||
match self {
|
|
||||||
Self::DateTime(datetime) => Self::DateTime(datetime + duration),
|
|
||||||
Self::Date(date, tz) => Self::DateTime(
|
|
||||||
date.and_time(NaiveTime::default())
|
|
||||||
.and_local_timezone(tz)
|
|
||||||
.earliest()
|
|
||||||
.expect("Local timezone has constant offset")
|
|
||||||
+ duration,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CalDateTime {
|
|
||||||
pub fn parse_prop(
|
|
||||||
prop: &Property,
|
|
||||||
timezones: &HashMap<String, Option<chrono_tz::Tz>>,
|
|
||||||
) -> Result<Self, CalDateTimeError> {
|
|
||||||
let prop_value = prop
|
|
||||||
.value
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| CalDateTimeError::InvalidDatetimeFormat("empty property".into()))?;
|
|
||||||
|
|
||||||
let timezone = if let Some(tzid) = prop.get_param("TZID") {
|
|
||||||
if let Some(timezone) = timezones.get(tzid) {
|
|
||||||
timezone.to_owned()
|
|
||||||
} else {
|
|
||||||
// TZID refers to timezone that does not exist
|
|
||||||
return Err(CalDateTimeError::InvalidTZID(tzid.to_string()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No explicit timezone specified.
|
|
||||||
// This is valid and will be localtime or UTC depending on the value
|
|
||||||
// We will stick to this default as documented in https://github.com/lennart-k/rustical/issues/102
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Self::parse(prop_value, timezone)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn format(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::DateTime(datetime) => match datetime.timezone() {
|
|
||||||
ICalTimezone::Olson(chrono_tz::UTC) => datetime.format(UTC_DATE_TIME).to_string(),
|
|
||||||
_ => datetime.format(LOCAL_DATE_TIME).to_string(),
|
|
||||||
},
|
|
||||||
Self::Date(date, _) => date.format(LOCAL_DATE).to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn format_date(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::DateTime(datetime) => datetime.format(LOCAL_DATE).to_string(),
|
|
||||||
Self::Date(date, _) => date.format(LOCAL_DATE).to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn date(&self) -> NaiveDate {
|
|
||||||
match self {
|
|
||||||
Self::DateTime(datetime) => datetime.date_naive(),
|
|
||||||
Self::Date(date, _) => date.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn is_date(&self) -> bool {
|
|
||||||
matches!(&self, Self::Date(_, _))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn as_datetime(&self) -> Cow<'_, DateTime<ICalTimezone>> {
|
|
||||||
match self {
|
|
||||||
Self::DateTime(datetime) => Cow::Borrowed(datetime),
|
|
||||||
Self::Date(date, tz) => Cow::Owned(
|
|
||||||
date.and_time(NaiveTime::default())
|
|
||||||
.and_local_timezone(tz.to_owned())
|
|
||||||
.earliest()
|
|
||||||
.expect("Midnight always exists"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(value: &str, timezone: Option<Tz>) -> Result<Self, CalDateTimeError> {
|
|
||||||
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) {
|
|
||||||
if let Some(timezone) = timezone {
|
|
||||||
return Ok(Self::DateTime(
|
|
||||||
datetime
|
|
||||||
.and_local_timezone(timezone.into())
|
|
||||||
.earliest()
|
|
||||||
.ok_or(CalDateTimeError::LocalTimeGap)?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return Ok(Self::DateTime(
|
|
||||||
datetime
|
|
||||||
.and_local_timezone(ICalTimezone::Local)
|
|
||||||
.earliest()
|
|
||||||
.ok_or(CalDateTimeError::LocalTimeGap)?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) {
|
|
||||||
return Ok(datetime.and_utc().into());
|
|
||||||
}
|
|
||||||
let timezone = timezone.map_or(ICalTimezone::Local, ICalTimezone::Olson);
|
|
||||||
if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) {
|
|
||||||
return Ok(Self::Date(date, timezone));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
|
|
||||||
return Ok(Self::Date(date, timezone));
|
|
||||||
}
|
|
||||||
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
|
|
||||||
return Ok(Self::Date(date, timezone));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also returns whether the date contains a year
|
|
||||||
pub fn parse_vcard(value: &str) -> Result<(Self, bool), CalDateTimeError> {
|
|
||||||
if let Ok(datetime) = Self::parse(value, None) {
|
|
||||||
return Ok((datetime, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
|
|
||||||
// Because 1972 is a leap year
|
|
||||||
let year = 1972;
|
|
||||||
// Cannot fail because of the regex
|
|
||||||
let month = captures.name("m").unwrap().as_str().parse().ok().unwrap();
|
|
||||||
let day = captures.name("d").unwrap().as_str().parse().ok().unwrap();
|
|
||||||
|
|
||||||
return Ok((
|
|
||||||
Self::Date(
|
|
||||||
NaiveDate::from_ymd_opt(year, month, day)
|
|
||||||
.ok_or_else(|| CalDateTimeError::ParseError(value.to_string()))?,
|
|
||||||
ICalTimezone::Local,
|
|
||||||
),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn utc(&self) -> DateTime<Utc> {
|
|
||||||
self.as_datetime().to_utc()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn timezone(&self) -> ICalTimezone {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.timezone(),
|
|
||||||
Self::Date(_, tz) => tz.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CalDateTime> for DateTime<Utc> {
|
|
||||||
fn from(value: CalDateTime) -> Self {
|
|
||||||
value.utc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Datelike for CalDateTime {
|
|
||||||
fn year(&self) -> i32 {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.year(),
|
|
||||||
Self::Date(date, _) => date.year(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn month(&self) -> u32 {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.month(),
|
|
||||||
Self::Date(date, _) => date.month(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn month0(&self) -> u32 {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.month0(),
|
|
||||||
Self::Date(date, _) => date.month0(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn day(&self) -> u32 {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.day(),
|
|
||||||
Self::Date(date, _) => date.day(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn day0(&self) -> u32 {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.day0(),
|
|
||||||
Self::Date(date, _) => date.day0(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn ordinal(&self) -> u32 {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.ordinal(),
|
|
||||||
Self::Date(date, _) => date.ordinal(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn ordinal0(&self) -> u32 {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.ordinal0(),
|
|
||||||
Self::Date(date, _) => date.ordinal0(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn weekday(&self) -> chrono::Weekday {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.weekday(),
|
|
||||||
Self::Date(date, _) => date.weekday(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn iso_week(&self) -> chrono::IsoWeek {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => datetime.iso_week(),
|
|
||||||
Self::Date(date, _) => date.iso_week(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn with_year(&self, year: i32) -> Option<Self> {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_year(year)?)),
|
|
||||||
Self::Date(date, tz) => Some(Self::Date(date.with_year(year)?, tz.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn with_month(&self, month: u32) -> Option<Self> {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_month(month)?)),
|
|
||||||
Self::Date(date, tz) => Some(Self::Date(date.with_month(month)?, tz.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn with_month0(&self, month0: u32) -> Option<Self> {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_month0(month0)?)),
|
|
||||||
Self::Date(date, tz) => Some(Self::Date(date.with_month0(month0)?, tz.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn with_day(&self, day: u32) -> Option<Self> {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_day(day)?)),
|
|
||||||
Self::Date(date, tz) => Some(Self::Date(date.with_day(day)?, tz.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn with_day0(&self, day0: u32) -> Option<Self> {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_day0(day0)?)),
|
|
||||||
Self::Date(date, tz) => Some(Self::Date(date.with_day0(day0)?, tz.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_ordinal(ordinal)?)),
|
|
||||||
Self::Date(date, tz) => Some(Self::Date(date.with_ordinal(ordinal)?, tz.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
|
|
||||||
match &self {
|
|
||||||
Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_ordinal0(ordinal0)?)),
|
|
||||||
Self::Date(date, tz) => Some(Self::Date(date.with_ordinal0(ordinal0)?, tz.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::CalDateTime;
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vcard_date() {
|
|
||||||
assert_eq!(
|
|
||||||
CalDateTime::parse_vcard("19850412").unwrap(),
|
|
||||||
(
|
|
||||||
CalDateTime::Date(
|
|
||||||
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
|
|
||||||
crate::ICalTimezone::Local
|
|
||||||
),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
CalDateTime::parse_vcard("1985-04-12").unwrap(),
|
|
||||||
(
|
|
||||||
CalDateTime::Date(
|
|
||||||
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
|
|
||||||
crate::ICalTimezone::Local
|
|
||||||
),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
CalDateTime::parse_vcard("--0412").unwrap(),
|
|
||||||
(
|
|
||||||
CalDateTime::Date(
|
|
||||||
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
|
|
||||||
crate::ICalTimezone::Local
|
|
||||||
),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone};
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
use derive_more::{Display, From};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, From, PartialEq, Eq)]
|
|
||||||
pub enum ICalTimezone {
|
|
||||||
Local,
|
|
||||||
Olson(Tz),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ICalTimezone> for rrule::Tz {
|
|
||||||
fn from(value: ICalTimezone) -> Self {
|
|
||||||
match value {
|
|
||||||
ICalTimezone::Local => Self::LOCAL,
|
|
||||||
ICalTimezone::Olson(tz) => Self::Tz(tz),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rrule::Tz> for ICalTimezone {
|
|
||||||
fn from(value: rrule::Tz) -> Self {
|
|
||||||
match value {
|
|
||||||
rrule::Tz::Local(_) => Self::Local,
|
|
||||||
rrule::Tz::Tz(tz) => Self::Olson(tz),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Display)]
|
|
||||||
pub enum CalTimezoneOffset {
|
|
||||||
Local(chrono::FixedOffset),
|
|
||||||
Olson(chrono_tz::TzOffset),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl chrono::Offset for CalTimezoneOffset {
|
|
||||||
fn fix(&self) -> chrono::FixedOffset {
|
|
||||||
match self {
|
|
||||||
Self::Local(local) => local.fix(),
|
|
||||||
Self::Olson(olson) => olson.fix(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimeZone for ICalTimezone {
|
|
||||||
type Offset = CalTimezoneOffset;
|
|
||||||
|
|
||||||
fn from_offset(offset: &Self::Offset) -> Self {
|
|
||||||
match offset {
|
|
||||||
CalTimezoneOffset::Local(_) => Self::Local,
|
|
||||||
CalTimezoneOffset::Olson(offset) => Self::Olson(Tz::from_offset(offset)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn offset_from_local_date(&self, local: &NaiveDate) -> chrono::MappedLocalTime<Self::Offset> {
|
|
||||||
match self {
|
|
||||||
Self::Local => Local
|
|
||||||
.offset_from_local_date(local)
|
|
||||||
.map(CalTimezoneOffset::Local),
|
|
||||||
Self::Olson(tz) => tz
|
|
||||||
.offset_from_local_date(local)
|
|
||||||
.map(CalTimezoneOffset::Olson),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn offset_from_local_datetime(
|
|
||||||
&self,
|
|
||||||
local: &NaiveDateTime,
|
|
||||||
) -> chrono::MappedLocalTime<Self::Offset> {
|
|
||||||
match self {
|
|
||||||
Self::Local => Local
|
|
||||||
.offset_from_local_datetime(local)
|
|
||||||
.map(CalTimezoneOffset::Local),
|
|
||||||
Self::Olson(tz) => tz
|
|
||||||
.offset_from_local_datetime(local)
|
|
||||||
.map(CalTimezoneOffset::Olson),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
|
|
||||||
match self {
|
|
||||||
Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_datetime(utc)),
|
|
||||||
Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_datetime(utc)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
|
|
||||||
match self {
|
|
||||||
Self::Local => CalTimezoneOffset::Local(Local.offset_from_utc_date(utc)),
|
|
||||||
Self::Olson(tz) => CalTimezoneOffset::Olson(tz.offset_from_utc_date(utc)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,6 @@ END:VCALENDAR
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_calendar_object() {
|
fn parse_calendar_object() {
|
||||||
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string(), None).unwrap();
|
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string()).unwrap();
|
||||||
object.expand_recurrence(None, None).unwrap();
|
object.get_inner().expand_recurrence(None, None).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
async fn import_calendar(
|
async fn import_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar: Calendar,
|
calendar: Calendar,
|
||||||
objects: Vec<CalendarObject>,
|
objects: Vec<(String, CalendarObject)>,
|
||||||
merge_existing: bool,
|
merge_existing: bool,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error>;
|
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error>;
|
||||||
|
|
||||||
/// Since the <calendar-query> rules are rather complex this function
|
/// Since the <calendar-query> rules are rather complex this function
|
||||||
/// is only meant to do some prefiltering
|
/// is only meant to do some prefiltering
|
||||||
@@ -55,7 +55,7 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
_query: CalendarQuery,
|
_query: CalendarQuery,
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
) -> Result<Vec<(String, CalendarObject)>, Error> {
|
||||||
self.get_objects(principal, cal_id).await
|
self.get_objects(principal, cal_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
) -> Result<Vec<CalendarObject>, Error>;
|
) -> Result<Vec<(String, CalendarObject)>, Error>;
|
||||||
async fn get_object(
|
async fn get_object(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -77,13 +77,23 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
object_id: &str,
|
object_id: &str,
|
||||||
show_deleted: bool,
|
show_deleted: bool,
|
||||||
) -> Result<CalendarObject, Error>;
|
) -> Result<CalendarObject, Error>;
|
||||||
|
async fn put_objects(
|
||||||
|
&self,
|
||||||
|
principal: String,
|
||||||
|
cal_id: String,
|
||||||
|
objects: Vec<(String, CalendarObject)>,
|
||||||
|
overwrite: bool,
|
||||||
|
) -> Result<(), Error>;
|
||||||
async fn put_object(
|
async fn put_object(
|
||||||
&self,
|
&self,
|
||||||
principal: String,
|
principal: String,
|
||||||
cal_id: String,
|
cal_id: String,
|
||||||
object: CalendarObject,
|
object: (String, CalendarObject),
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error> {
|
||||||
|
self.put_objects(principal, cal_id, vec![object], overwrite)
|
||||||
|
.await
|
||||||
|
}
|
||||||
async fn delete_object(
|
async fn delete_object(
|
||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::CalendarStore;
|
use crate::CalendarStore;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use rustical_ical::CalendarObject;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
pub trait PrefixedCalendarStore: CalendarStore {
|
pub trait PrefixedCalendarStore: CalendarStore {
|
||||||
@@ -88,7 +89,7 @@ impl CalendarStore for CombinedCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
) -> Result<(Vec<rustical_ical::CalendarObject>, Vec<String>, i64), crate::Error> {
|
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), crate::Error> {
|
||||||
self.store_for_id(cal_id)
|
self.store_for_id(cal_id)
|
||||||
.sync_changes(principal, cal_id, synctoken)
|
.sync_changes(principal, cal_id, synctoken)
|
||||||
.await
|
.await
|
||||||
@@ -97,7 +98,7 @@ impl CalendarStore for CombinedCalendarStore {
|
|||||||
async fn import_calendar(
|
async fn import_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar: crate::Calendar,
|
calendar: crate::Calendar,
|
||||||
objects: Vec<rustical_ical::CalendarObject>,
|
objects: Vec<(String, CalendarObject)>,
|
||||||
merge_existing: bool,
|
merge_existing: bool,
|
||||||
) -> Result<(), crate::Error> {
|
) -> Result<(), crate::Error> {
|
||||||
self.store_for_id(&calendar.id)
|
self.store_for_id(&calendar.id)
|
||||||
@@ -110,7 +111,7 @@ impl CalendarStore for CombinedCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
query: crate::calendar_store::CalendarQuery,
|
query: crate::calendar_store::CalendarQuery,
|
||||||
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
|
) -> Result<Vec<(String, CalendarObject)>, crate::Error> {
|
||||||
self.store_for_id(cal_id)
|
self.store_for_id(cal_id)
|
||||||
.calendar_query(principal, cal_id, query)
|
.calendar_query(principal, cal_id, query)
|
||||||
.await
|
.await
|
||||||
@@ -141,21 +142,21 @@ impl CalendarStore for CombinedCalendarStore {
|
|||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
|
) -> Result<Vec<(String, CalendarObject)>, crate::Error> {
|
||||||
self.store_for_id(cal_id)
|
self.store_for_id(cal_id)
|
||||||
.get_objects(principal, cal_id)
|
.get_objects(principal, cal_id)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn put_object(
|
async fn put_objects(
|
||||||
&self,
|
&self,
|
||||||
principal: String,
|
principal: String,
|
||||||
cal_id: String,
|
cal_id: String,
|
||||||
object: rustical_ical::CalendarObject,
|
objects: Vec<(String, CalendarObject)>,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), crate::Error> {
|
) -> Result<(), crate::Error> {
|
||||||
self.store_for_id(&cal_id)
|
self.store_for_id(&cal_id)
|
||||||
.put_object(principal, cal_id, object, overwrite)
|
.put_objects(principal, cal_id, objects, overwrite)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ publish = false
|
|||||||
[features]
|
[features]
|
||||||
test = ["dep:rstest"]
|
test = ["dep:rstest"]
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "insert_calendar_object"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest.workspace = true
|
rstest.workspace = true
|
||||||
|
criterion.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
@@ -31,3 +36,4 @@ pbkdf2.workspace = true
|
|||||||
rustical_ical.workspace = true
|
rustical_ical.workspace = true
|
||||||
rstest = { workspace = true, optional = true }
|
rstest = { workspace = true, optional = true }
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
|
ical.workspace = true
|
||||||
|
|||||||
47
crates/store_sqlite/benches/ical_event.ics
Normal file
47
crates/store_sqlite/benches/ical_event.ics
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
|
||||||
|
VERSION:2.0
|
||||||
|
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Europe/Berlin
|
||||||
|
X-LIC-LOCATION:Europe/Berlin
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19810329T020000
|
||||||
|
RRULE:FREQ=YEARLY;UNTIL=20370329T010000Z;BYDAY=-1SU;BYMONTH=3
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19961027T030000
|
||||||
|
RRULE:FREQ=YEARLY;UNTIL=20361026T010000Z;BYDAY=-1SU;BYMONTH=10
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
|
||||||
|
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:fa915b604e6e3f36772501ff869439e6a3c5cf67
|
||||||
|
DTSTAMP:20250726T112617Z
|
||||||
|
DTSTART;VALUE=DATE:20250806
|
||||||
|
DTEND;VALUE=DATE:20250807
|
||||||
|
SEQUENCE:2
|
||||||
|
SUMMARY:all day event
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
CLASS:PUBLIC
|
||||||
|
CREATED:20250726T144426Z
|
||||||
|
LAST-MODIFIED:20250726T144426Z
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER:-PT30M
|
||||||
|
REPEAT:2
|
||||||
|
DURATION:PT15M
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Breakfast meeting with executive\n
|
||||||
|
team at 8:30 AM EST.
|
||||||
|
END:VALARM
|
||||||
|
END:VEVENT
|
||||||
|
|
||||||
|
END:VCALENDAR
|
||||||
79
crates/store_sqlite/benches/insert_calendar_object.rs
Normal file
79
crates/store_sqlite/benches/insert_calendar_object.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use rustical_ical::{CalendarObject, CalendarObjectType};
|
||||||
|
use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
|
||||||
|
use rustical_store_sqlite::tests::get_test_calendar_store;
|
||||||
|
|
||||||
|
fn benchmark(c: &mut Criterion) {
|
||||||
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let cal_store = runtime.block_on(async {
|
||||||
|
let cal_store = get_test_calendar_store().await;
|
||||||
|
|
||||||
|
cal_store
|
||||||
|
.insert_calendar(Calendar {
|
||||||
|
meta: CalendarMetadata {
|
||||||
|
displayname: Some("Yeet".to_owned()),
|
||||||
|
order: 0,
|
||||||
|
description: None,
|
||||||
|
color: None,
|
||||||
|
},
|
||||||
|
principal: "user".to_owned(),
|
||||||
|
id: "okwow".to_owned(),
|
||||||
|
timezone_id: None,
|
||||||
|
deleted_at: None,
|
||||||
|
synctoken: 0,
|
||||||
|
subscription_url: None,
|
||||||
|
push_topic: "asd".to_owned(),
|
||||||
|
components: vec![
|
||||||
|
CalendarObjectType::Event,
|
||||||
|
CalendarObjectType::Todo,
|
||||||
|
CalendarObjectType::Journal,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cal_store
|
||||||
|
});
|
||||||
|
|
||||||
|
let object = CalendarObject::from_ics(include_str!("ical_event.ics").to_owned()).unwrap();
|
||||||
|
|
||||||
|
let batch_size = 1000;
|
||||||
|
let objects: Vec<_> = std::iter::repeat_n(
|
||||||
|
(object.get_inner().get_uid().to_owned(), object.clone()),
|
||||||
|
batch_size,
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
c.bench_function("put_batch", |b| {
|
||||||
|
b.to_async(&runtime).iter(async || {
|
||||||
|
// yeet
|
||||||
|
cal_store
|
||||||
|
.put_objects("user".to_owned(), "okwow".to_owned(), objects.clone(), true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("put_single", |b| {
|
||||||
|
b.to_async(&runtime).iter(async || {
|
||||||
|
// yeet
|
||||||
|
for _ in 0..1000 {
|
||||||
|
cal_store
|
||||||
|
.put_object(
|
||||||
|
"user".to_owned(),
|
||||||
|
"okwow".to_owned(),
|
||||||
|
(object.get_inner().get_uid().to_owned(), object.clone()),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime
|
||||||
|
.block_on(cal_store.delete_calendar("user", "okwow", false))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, benchmark);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -8,7 +8,6 @@ use rustical_store::{
|
|||||||
};
|
};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use sqlx::{Executor, Sqlite};
|
use sqlx::{Executor, Sqlite};
|
||||||
use std::collections::HashMap;
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
|
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
|
||||||
@@ -312,7 +311,7 @@ impl CalendarStore for SqliteAddressbookStore {
|
|||||||
async fn import_calendar(
|
async fn import_calendar(
|
||||||
&self,
|
&self,
|
||||||
_calendar: Calendar,
|
_calendar: Calendar,
|
||||||
_objects: Vec<CalendarObject>,
|
_objects: Vec<(String, CalendarObject)>,
|
||||||
_merge_existing: bool,
|
_merge_existing: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Err(Error::ReadOnly)
|
Err(Error::ReadOnly)
|
||||||
@@ -324,17 +323,19 @@ impl CalendarStore for SqliteAddressbookStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> {
|
||||||
let cal_id = cal_id
|
let cal_id = cal_id
|
||||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
.ok_or(Error::NotFound)?;
|
.ok_or(Error::NotFound)?;
|
||||||
let (objects, deleted_objects, new_synctoken) =
|
let (objects, deleted_objects, new_synctoken) =
|
||||||
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
|
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
|
||||||
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
|
let objects = objects
|
||||||
.iter()
|
.iter()
|
||||||
.map(AddressObject::get_birthday_object)
|
.map(AddressObject::get_significant_dates)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
let objects = objects?.into_iter().flatten().collect();
|
|
||||||
|
|
||||||
Ok((objects, deleted_objects, new_synctoken))
|
Ok((objects, deleted_objects, new_synctoken))
|
||||||
}
|
}
|
||||||
@@ -356,22 +357,18 @@ impl CalendarStore for SqliteAddressbookStore {
|
|||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
) -> Result<Vec<(String, CalendarObject)>, Error> {
|
||||||
let cal_id = cal_id
|
let cal_id = cal_id
|
||||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
.ok_or(Error::NotFound)?;
|
.ok_or(Error::NotFound)?;
|
||||||
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
|
Ok(AddressbookStore::get_objects(self, principal, cal_id)
|
||||||
AddressbookStore::get_objects(self, principal, cal_id)
|
.await?
|
||||||
.await?
|
.iter()
|
||||||
.iter()
|
.map(AddressObject::get_significant_dates)
|
||||||
.map(AddressObject::get_significant_dates)
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
.collect();
|
|
||||||
let objects = objects?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(HashMap::into_values)
|
.flatten()
|
||||||
.collect();
|
.collect())
|
||||||
|
|
||||||
Ok(objects)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@@ -386,19 +383,22 @@ impl CalendarStore for SqliteAddressbookStore {
|
|||||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
.ok_or(Error::NotFound)?;
|
.ok_or(Error::NotFound)?;
|
||||||
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
|
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
|
||||||
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
|
let addr_object =
|
||||||
.await?
|
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
|
||||||
.get_significant_dates()?
|
.await?;
|
||||||
.remove(date_type)
|
match date_type {
|
||||||
.ok_or(Error::NotFound)
|
"birthday" => addr_object.get_birthday_object()?.ok_or(Error::NotFound),
|
||||||
|
"anniversary" => addr_object.get_anniversary_object()?.ok_or(Error::NotFound),
|
||||||
|
_ => Err(Error::NotFound),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn put_object(
|
async fn put_objects(
|
||||||
&self,
|
&self,
|
||||||
_principal: String,
|
_principal: String,
|
||||||
_cal_id: String,
|
_cal_id: String,
|
||||||
_object: CalendarObject,
|
_objects: Vec<(String, CalendarObject)>,
|
||||||
_overwrite: bool,
|
_overwrite: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Err(Error::ReadOnly)
|
Err(Error::ReadOnly)
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use crate::BEGIN_IMMEDIATE;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use derive_more::derive::Constructor;
|
use derive_more::derive::Constructor;
|
||||||
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
|
use ical::types::CalDateOrDateTime;
|
||||||
|
use rustical_ical::{CalendarObject, CalendarObjectType};
|
||||||
use rustical_store::calendar_store::CalendarQuery;
|
use rustical_store::calendar_store::CalendarQuery;
|
||||||
use rustical_store::synctoken::format_synctoken;
|
use rustical_store::synctoken::format_synctoken;
|
||||||
use rustical_store::{Calendar, CalendarMetadata, CalendarStore, CollectionMetadata, Error};
|
use rustical_store::{Calendar, CalendarMetadata, CalendarStore, CollectionMetadata, Error};
|
||||||
@@ -20,17 +21,27 @@ struct CalendarObjectRow {
|
|||||||
uid: String,
|
uid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<CalendarObjectRow> for (String, CalendarObject) {
|
||||||
|
type Error = rustical_store::Error;
|
||||||
|
|
||||||
|
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
|
||||||
|
let object_id = value.id.clone();
|
||||||
|
|
||||||
|
Ok((object_id, value.try_into()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<CalendarObjectRow> for CalendarObject {
|
impl TryFrom<CalendarObjectRow> for CalendarObject {
|
||||||
type Error = rustical_store::Error;
|
type Error = rustical_store::Error;
|
||||||
|
|
||||||
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
|
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
|
||||||
let object = Self::from_ics(value.ics, Some(value.id))?;
|
let object = Self::from_ics(value.ics)?;
|
||||||
if object.get_uid() != value.uid {
|
if object.get_inner().get_uid() != value.uid {
|
||||||
return Err(rustical_store::Error::IcalError(
|
return Err(rustical_store::Error::IcalError(
|
||||||
rustical_ical::Error::InvalidData(format!(
|
rustical_ical::Error::InvalidData(format!(
|
||||||
"uid={} and UID={} don't match",
|
"uid={} and UID={} don't match",
|
||||||
value.uid,
|
value.uid,
|
||||||
object.get_uid()
|
object.get_inner().get_uid()
|
||||||
)),
|
)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -378,7 +389,7 @@ impl SqliteCalendarStore {
|
|||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
) -> Result<Vec<(String, CalendarObject)>, Error> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
CalendarObjectRow,
|
CalendarObjectRow,
|
||||||
"SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
"SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||||
@@ -388,7 +399,7 @@ impl SqliteCalendarStore {
|
|||||||
.fetch_all(executor)
|
.fetch_all(executor)
|
||||||
.await.map_err(crate::Error::from)?
|
.await.map_err(crate::Error::from)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(std::convert::TryInto::try_into)
|
.map(TryInto::try_into)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +408,7 @@ impl SqliteCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
query: CalendarQuery,
|
query: CalendarQuery,
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
) -> Result<Vec<(String, CalendarObject)>, Error> {
|
||||||
// We extend our query interval by one day in each direction since we really don't want to
|
// We extend our query interval by one day in each direction since we really don't want to
|
||||||
// miss any objects because of timezone differences
|
// miss any objects because of timezone differences
|
||||||
// I've previously tried NaiveDate::MIN,MAX, but it seems like sqlite cannot handle these
|
// I've previously tried NaiveDate::MIN,MAX, but it seems like sqlite cannot handle these
|
||||||
@@ -422,7 +433,7 @@ impl SqliteCalendarStore {
|
|||||||
.await
|
.await
|
||||||
.map_err(crate::Error::from)?
|
.map_err(crate::Error::from)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(std::convert::TryInto::try_into)
|
.map(TryInto::try_into)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,23 +463,26 @@ impl SqliteCalendarStore {
|
|||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
|
object_id: &str,
|
||||||
object: &CalendarObject,
|
object: &CalendarObject,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
let (uid, ics) = (object.get_inner().get_uid(), object.get_ics());
|
||||||
|
|
||||||
let first_occurence = object
|
let first_occurence = object
|
||||||
.get_first_occurence()
|
.get_inner()
|
||||||
|
.get_dtstart()
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(CalDateTime::date);
|
.map(CalDateOrDateTime::date_floor);
|
||||||
let last_occurence = object
|
let last_occurence = object
|
||||||
|
.get_inner()
|
||||||
.get_last_occurence()
|
.get_last_occurence()
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(CalDateTime::date);
|
.map(CalDateOrDateTime::date_ceil);
|
||||||
let etag = object.get_etag();
|
let etag = object.get_etag();
|
||||||
let object_type = object.get_object_type() as u8;
|
let object_type = object.get_object_type() as u8;
|
||||||
|
|
||||||
@@ -560,7 +574,7 @@ impl SqliteCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> {
|
||||||
struct Row {
|
struct Row {
|
||||||
object_id: String,
|
object_id: String,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
@@ -587,7 +601,7 @@ impl SqliteCalendarStore {
|
|||||||
|
|
||||||
for Row { object_id, .. } in changes {
|
for Row { object_id, .. } in changes {
|
||||||
match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await {
|
match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await {
|
||||||
Ok(object) => objects.push(object),
|
Ok(object) => objects.push((object_id, object)),
|
||||||
Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id),
|
Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id),
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
}
|
}
|
||||||
@@ -672,7 +686,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
async fn import_calendar(
|
async fn import_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar: Calendar,
|
calendar: Calendar,
|
||||||
objects: Vec<CalendarObject>,
|
objects: Vec<(String, CalendarObject)>,
|
||||||
merge_existing: bool,
|
merge_existing: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut tx = self
|
let mut tx = self
|
||||||
@@ -695,15 +709,23 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut sync_token = None;
|
let mut sync_token = None;
|
||||||
for object in objects {
|
for (object_id, object) in objects {
|
||||||
Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?;
|
Self::_put_object(
|
||||||
|
&mut *tx,
|
||||||
|
&calendar.principal,
|
||||||
|
&calendar.id,
|
||||||
|
&object_id,
|
||||||
|
&object,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sync_token = Some(
|
sync_token = Some(
|
||||||
Self::log_object_operation(
|
Self::log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&calendar.principal,
|
&calendar.principal,
|
||||||
&calendar.id,
|
&calendar.id,
|
||||||
object.get_id(),
|
&object_id,
|
||||||
ChangeOperation::Add,
|
ChangeOperation::Add,
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
@@ -729,7 +751,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
query: CalendarQuery,
|
query: CalendarQuery,
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
) -> Result<Vec<(String, CalendarObject)>, Error> {
|
||||||
Self::_calendar_query(&self.db, principal, cal_id, query).await
|
Self::_calendar_query(&self.db, principal, cal_id, query).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,7 +782,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
&self,
|
&self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
) -> Result<Vec<(String, CalendarObject)>, Error> {
|
||||||
Self::_get_objects(&self.db, principal, cal_id).await
|
Self::_get_objects(&self.db, principal, cal_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,11 +798,11 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn put_object(
|
async fn put_objects(
|
||||||
&self,
|
&self,
|
||||||
principal: String,
|
principal: String,
|
||||||
cal_id: String,
|
cal_id: String,
|
||||||
object: CalendarObject,
|
objects: Vec<(String, CalendarObject)>,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut tx = self
|
let mut tx = self
|
||||||
@@ -789,33 +811,40 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
.await
|
.await
|
||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
let object_id = object.get_id().to_owned();
|
|
||||||
|
|
||||||
let calendar = Self::_get_calendar(&mut *tx, &principal, &cal_id, true).await?;
|
let calendar = Self::_get_calendar(&mut *tx, &principal, &cal_id, true).await?;
|
||||||
if calendar.subscription_url.is_some() {
|
if calendar.subscription_url.is_some() {
|
||||||
// We cannot commit an object to a subscription calendar
|
// We cannot commit an object to a subscription calendar
|
||||||
return Err(Error::ReadOnly);
|
return Err(Error::ReadOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
|
let mut sync_token = None;
|
||||||
|
for (object_id, object) in objects {
|
||||||
let sync_token = Self::log_object_operation(
|
sync_token = Some(
|
||||||
&mut tx,
|
Self::log_object_operation(
|
||||||
&principal,
|
&mut tx,
|
||||||
&cal_id,
|
&principal,
|
||||||
&object_id,
|
&cal_id,
|
||||||
ChangeOperation::Add,
|
&object_id,
|
||||||
)
|
ChangeOperation::Add,
|
||||||
.await?;
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
Self::_put_object(
|
||||||
|
&mut *tx, &principal, &cal_id, &object_id, &object, overwrite,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
self.send_push_notification(
|
if let Some(sync_token) = sync_token {
|
||||||
CollectionOperationInfo::Content { sync_token },
|
self.send_push_notification(
|
||||||
self.get_calendar(&principal, &cal_id, true)
|
CollectionOperationInfo::Content { sync_token },
|
||||||
.await?
|
self.get_calendar(&principal, &cal_id, true)
|
||||||
.push_topic,
|
.await?
|
||||||
);
|
.push_topic,
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,7 +910,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cal_id: &str,
|
cal_id: &str,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> {
|
||||||
Self::_sync_changes(&self.db, principal, cal_id, synctoken).await
|
Self::_sync_changes(&self.db, principal, cal_id, synctoken).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ source: src/integration_tests/caldav/calendar.rs
|
|||||||
expression: body
|
expression: body
|
||||||
---
|
---
|
||||||
BEGIN:VCALENDAR
|
BEGIN:VCALENDAR
|
||||||
VERSION:4.0
|
VERSION:2.0
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
PRODID:RustiCal
|
PRODID:RustiCal
|
||||||
X-WR-CALNAME:Calendar
|
X-WR-CALNAME:Calendar
|
||||||
|
|||||||
Reference in New Issue
Block a user