Compare commits

...

12 Commits

Author SHA1 Message Date
Lennart
b65cca9d17 version 0.11.6 2025-12-27 14:23:56 +01:00
Lennart
55ecbdcd41 carddav: Implement addressbook-query 2025-12-27 14:22:23 +01:00
Lennart
89d3d3b7a4 caldav: Outsource text-match to rustical_dav 2025-12-27 13:45:26 +01:00
Lennart K
a74b74369c version 0.11.5 2025-12-21 16:16:48 +01:00
Lennart K
85c49a0bdf update ical-rs 2025-12-20 14:30:26 +01:00
Lennart K
f2e4e2c1a7 add a database benchmark 2025-12-20 11:48:30 +01:00
Lennart K
28c925301e calendar store: Add method for bulk insert 2025-12-20 11:48:05 +01:00
Lennart K
b50ea478db Content-Type: Add charset=utf-8 2025-12-18 21:43:27 +01:00
Lennart K
2c7748255c update test snapshot 2025-12-18 21:40:39 +01:00
Lennart K
f40a23a1f1 update Cargo.toml and fix calendar export ical version 2025-12-18 21:39:36 +01:00
Lennart K
2a4ba33e45 refactor recurrence expansion 2025-12-18 21:27:40 +01:00
Lennart K
6bc4bd3fa3 Update ical-rs dependency 2025-12-18 14:14:26 +01:00
30 changed files with 866 additions and 212 deletions

312
Cargo.lock generated
View File

@@ -26,6 +26,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -41,6 +50,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "0.6.21"
@@ -156,9 +171,9 @@ dependencies = [
[[package]]
name = "askama_web"
version = "0.14.6"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dcd7d2caaff31b91ef5d112ed10416344e23a33db9e7eea7ba695d2a97a88a"
checksum = "e1acadd534892f9ef8c3809b47997e3cd857fad735edceff77a88be1c8236920"
dependencies = [
"askama",
"askama_web_derive",
@@ -541,10 +556,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.49"
name = "cast"
version = "0.3.0"
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 = [
"find-msvc-tools",
"shlex",
@@ -595,6 +616,33 @@ dependencies = [
"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]]
name = "clap"
version = "4.5.53"
@@ -718,6 +766,61 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "crossbeam-queue"
version = "0.3.12"
@@ -733,6 +836,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
@@ -1382,6 +1491,17 @@ dependencies = [
"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]]
name = "hashbrown"
version = "0.12.3"
@@ -1641,14 +1761,13 @@ dependencies = [
[[package]]
name = "ical"
version = "0.11.0"
source = "git+https://github.com/lennart-k/ical-rs#211ce20acf3ecc7831eecf78d9a23232b83d7a6c"
source = "git+https://github.com/lennart-k/ical-rs#d384dd45495722d69c7f76d62a54a8d6481e90ee"
dependencies = [
"chrono",
"chrono-tz",
"itertools 0.14.0",
"lazy_static",
"regex",
"serde",
"thiserror 2.0.17",
]
@@ -1791,14 +1910,15 @@ checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "insta"
version = "1.44.3"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698"
checksum = "b76866be74d68b1595eb8060cb9191dca9c021db2316558e52ddc5d55d41b66c"
dependencies = [
"console",
"once_cell",
"regex",
"similar",
"tempfile",
]
[[package]]
@@ -1832,6 +1952,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
@@ -2133,6 +2262,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "openidconnect"
version = "4.0.1"
@@ -2327,6 +2462,16 @@ dependencies = [
"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]]
name = "parking"
version = "2.2.1"
@@ -2538,6 +2683,34 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "polling"
version = "3.11.0"
@@ -2777,6 +2950,26 @@ dependencies = [
"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]]
name = "redox_syscall"
version = "0.5.18"
@@ -3068,7 +3261,7 @@ dependencies = [
[[package]]
name = "rustical"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"anyhow",
"argon2",
@@ -3101,7 +3294,7 @@ dependencies = [
"serde",
"sqlx",
"tokio",
"toml 0.9.9+spec-1.0.0",
"toml 0.9.10+spec-1.1.0",
"tower",
"tower-http",
"tower-sessions",
@@ -3113,7 +3306,7 @@ dependencies = [
[[package]]
name = "rustical_caldav"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"async-std",
"async-trait",
@@ -3155,7 +3348,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"async-trait",
"axum",
@@ -3188,7 +3381,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"async-trait",
"axum",
@@ -3197,6 +3390,7 @@ dependencies = [
"futures-util",
"headers",
"http",
"ical",
"itertools 0.14.0",
"log",
"matchit 0.9.0",
@@ -3213,7 +3407,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"async-trait",
"axum",
@@ -3238,7 +3432,7 @@ dependencies = [
[[package]]
name = "rustical_frontend"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"askama",
"askama_web",
@@ -3274,7 +3468,7 @@ dependencies = [
[[package]]
name = "rustical_ical"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"axum",
"chrono",
@@ -3291,7 +3485,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"async-trait",
"axum",
@@ -3307,7 +3501,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"anyhow",
"async-trait",
@@ -3340,10 +3534,11 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"async-trait",
"chrono",
"criterion",
"derive_more",
"password-auth",
"password-hash",
@@ -3362,7 +3557,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"quick-xml",
"thiserror 2.0.17",
@@ -4033,6 +4228,19 @@ dependencies = [
"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]]
name = "thiserror"
version = "1.0.69"
@@ -4123,6 +4331,16 @@ dependencies = [
"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]]
name = "tinyvec"
version = "1.10.0"
@@ -4215,14 +4433,14 @@ dependencies = [
[[package]]
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"
checksum = "eb5238e643fc34a1d5d7e753e1532a91912d74b63b92b3ea51fde8d1b7bc79dd"
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
dependencies = [
"indexmap 2.12.1",
"serde_core",
"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_writer",
"winnow",
@@ -4239,9 +4457,9 @@ dependencies = [
[[package]]
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"
checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
@@ -4267,16 +4485,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap 2.12.1",
"toml_datetime 0.7.4+spec-1.0.0",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"winnow",
]
[[package]]
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"
checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
dependencies = [
"winnow",
]
@@ -4289,9 +4507,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
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"
checksum = "a9cd6190959dce0994aa8970cd32ab116d1851ead27e866039acaf2524ce44fa"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tonic"
@@ -4458,9 +4676,9 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.43"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@@ -4481,9 +4699,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.35"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@@ -4817,6 +5035,22 @@ dependencies = [
"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]]
name = "winapi-util"
version = "0.1.11"
@@ -4826,6 +5060,12 @@ dependencies = [
"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]]
name = "windows-core"
version = "0.62.2"
@@ -5139,7 +5379,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "xml_derive"
version = "0.11.4"
version = "0.11.6"
dependencies = [
"darling 0.23.0",
"heck",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.11.4"
version = "0.11.6"
rust-version = "1.91"
edition = "2024"
description = "A CalDAV server"
@@ -32,12 +32,11 @@ opentelemetry = [
"dep:tracing-opentelemetry",
]
[profile.dev]
debug = 0
[workspace.dependencies]
rustical_dav = { path = "./crates/dav/" }
rustical_dav = { path = "./crates/dav/", features = ["ical"] }
rustical_dav_push = { path = "./crates/dav_push/" }
rustical_store = { path = "./crates/store/" }
rustical_store_sqlite = { path = "./crates/store_sqlite/" }
@@ -109,8 +108,6 @@ strum_macros = "0.27"
serde_json = { version = "1.0", features = ["raw_value"] }
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
ical = { git = "https://github.com/lennart-k/ical-rs", features = [
"generator",
"serde",
"chrono-tz",
] }
toml = "0.9"
@@ -150,6 +147,7 @@ openssl = { version = "0.10", features = ["vendored"] }
async-std = { version = "1.13", features = ["attributes"] }
similar-asserts = "1.7"
insta = { version = "1.44", features = ["filters"] }
criterion = { version = "0.8", features = ["async_tokio"] }
[dev-dependencies]
rstest.workspace = true

View File

@@ -29,7 +29,7 @@ base64.workspace = true
serde.workspace = true
tokio.workspace = true
url.workspace = true
rustical_dav.workspace = true
rustical_dav = { workspace = true, features = ["ical"] }
rustical_store.workspace = true
chrono.workspace = true
chrono-tz.workspace = true

View File

@@ -5,7 +5,8 @@ use axum::extract::State;
use axum::{extract::Path, response::Response};
use headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, Method, StatusCode, header};
use ical::generator::{Emitter, IcalCalendarBuilder};
use ical::builder::calendar::IcalCalendarBuilder;
use ical::generator::Emitter;
use ical::property::Property;
use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_ical::{CalendarObjectComponent, EventObject};
@@ -35,7 +36,7 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
let mut vtimezones = HashMap::new();
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()
.prodid("RustiCal");
if let Some(displayname) = calendar.meta.displayname {
@@ -64,30 +65,24 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
vtimezones.extend(object.get_vtimezones());
match object.get_data() {
CalendarObjectComponent::Event(EventObject { event, .. }, overrides) => {
ical_calendar_builder = ical_calendar_builder.add_event(event.clone());
for ev_override in overrides {
ical_calendar_builder =
ical_calendar_builder.add_event(ev_override.event.clone());
}
ical_calendar_builder = ical_calendar_builder
.add_event(event.clone())
.add_events(overrides.iter().map(|ev| ev.event.clone()));
}
CalendarObjectComponent::Todo(todo, overrides) => {
ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
for ev_override in overrides {
ical_calendar_builder = ical_calendar_builder.add_todo(ev_override.clone());
}
ical_calendar_builder = ical_calendar_builder
.add_todo(todo.clone())
.add_todos(overrides.iter().cloned());
}
CalendarObjectComponent::Journal(journal, overrides) => {
ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
for ev_override in overrides {
ical_calendar_builder = ical_calendar_builder.add_journal(ev_override.clone());
}
ical_calendar_builder = ical_calendar_builder
.add_journal(journal.clone())
.add_journals(overrides.iter().cloned());
}
}
}
for vtimezone in vtimezones.into_values() {
ical_calendar_builder = ical_calendar_builder.add_tz(vtimezone.to_owned());
}
ical_calendar_builder = ical_calendar_builder.add_timezones(vtimezones.into_values().cloned());
let ical_calendar = ical_calendar_builder
.build()
@@ -95,7 +90,7 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
let mut resp = Response::builder().status(StatusCode::OK);
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 = utf8_percent_encode(&filename, CONTROLS);

View File

@@ -137,13 +137,11 @@ impl CompFilterable for CalendarObjectComponent {
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};
use rustical_dav::xml::{NegateCondition, TextCollation, TextMatchElement};
use rustical_ical::{CalendarObject, UtcDateTime};
use crate::calendar::methods::report::calendar_query::{
CompFilterable, TextMatchElement, TimeRangeElement,
comp_filter::CompFilterElement,
prop_filter::PropFilterElement,
text_match::{NegateCondition, TextCollation},
CompFilterElement, CompFilterable, PropFilterElement, TimeRangeElement,
};
const ICS: &str = r"BEGIN:VCALENDAR

View File

@@ -1,11 +1,6 @@
use crate::{
calendar::methods::report::calendar_query::{
TextMatchElement,
comp_filter::{CompFilterElement, CompFilterable},
},
calendar_object::CalendarObjectPropWrapperName,
};
use rustical_dav::xml::PropfindType;
use super::comp_filter::{CompFilterElement, CompFilterable};
use crate::calendar_object::CalendarObjectPropWrapperName;
use rustical_dav::xml::{PropfindType, TextMatchElement};
use rustical_ical::{CalendarObject, UtcDateTime};
use rustical_store::calendar_store::CalendarQuery;
use rustical_xml::XmlDeserialize;

View File

@@ -5,14 +5,11 @@ use rustical_store::CalendarStore;
mod comp_filter;
mod elements;
mod prop_filter;
pub mod text_match;
#[allow(unused_imports)]
pub use comp_filter::{CompFilterElement, CompFilterable};
pub use elements::*;
#[allow(unused_imports)]
pub use prop_filter::{PropFilterElement, PropFilterable};
#[allow(unused_imports)]
pub use text_match::TextMatchElement;
pub async fn get_objects_calendar_query<C: CalendarStore>(
cal_query: &CalendarQueryRequest,
@@ -31,21 +28,16 @@ pub async fn get_objects_calendar_query<C: CalendarStore>(
#[cfg(test)]
mod tests {
use rustical_dav::xml::PropElement;
use rustical_xml::XmlDocument;
use super::{
CalendarQueryRequest, FilterElement, ParamFilterElement, comp_filter::CompFilterElement,
prop_filter::PropFilterElement,
};
use crate::{
calendar::methods::report::{
ReportRequest,
calendar_query::{
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
comp_filter::CompFilterElement,
prop_filter::PropFilterElement,
text_match::{NegateCondition, TextCollation},
},
},
calendar::methods::report::ReportRequest,
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
};
use rustical_dav::xml::{NegateCondition, PropElement, TextCollation, TextMatchElement};
use rustical_xml::XmlDocument;
#[test]
fn calendar_query_7_8_7() {

View File

@@ -1,5 +1,4 @@
use std::collections::HashMap;
use super::{ParamFilterElement, TimeRangeElement};
use ical::{
generator::{IcalCalendar, IcalEvent},
parser::{
@@ -8,12 +7,10 @@ use ical::{
},
property::Property,
};
use rustical_dav::xml::TextMatchElement;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectComponent, UtcDateTime};
use rustical_xml::XmlDeserialize;
use crate::calendar::methods::report::calendar_query::{
ParamFilterElement, TextMatchElement, TimeRangeElement,
};
use std::collections::HashMap;
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]

View File

@@ -1,10 +1,9 @@
use derive_more::derive::{From, Into};
use rustical_dav::xml::TextCollation;
use rustical_ical::CalendarObjectType;
use rustical_xml::{XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray;
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
pub struct SupportedCalendarComponent {
#[xml(ty = "attr")]

View File

@@ -42,7 +42,7 @@ pub async fn get_event<C: CalendarStore>(
let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().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) {
Ok(resp.body(Body::empty()).unwrap())
} else {

View File

@@ -22,7 +22,7 @@ base64.workspace = true
serde.workspace = true
tokio.workspace = true
url.workspace = true
rustical_dav.workspace = true
rustical_dav = { workspace = true, features = ["ical"] }
rustical_store.workspace = true
chrono.workspace = true
rustical_xml.workspace = true

View File

@@ -50,7 +50,7 @@ pub async fn get_object<AS: AddressbookStore>(
let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().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) {
Ok(resp.body(Body::empty()).unwrap())
} else {

View File

@@ -46,7 +46,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
let mut resp = Response::builder().status(StatusCode::OK);
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 = utf8_percent_encode(&filename, CONTROLS);
hdrs.insert(

View File

@@ -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,
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -1,6 +1,14 @@
use crate::{
CardDavPrincipalUri, Error, address_object::AddressObjectPropWrapperName,
addressbook::AddressbookResourceService,
CardDavPrincipalUri, Error,
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 axum::{
@@ -8,19 +16,30 @@ use axum::{
extract::{OriginalUri, Path, State},
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_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection;
use tracing::instrument;
mod addressbook_multiget;
mod addressbook_query;
mod sync_collection;
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
pub(crate) enum ReportRequest {
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressbookMultiget(AddressbookMultigetRequest),
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressbookQuery(AddressbookQueryRequest),
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
SyncCollection(SyncCollectionRequest<AddressObjectPropWrapperName>),
}
@@ -29,11 +48,49 @@ impl ReportRequest {
const fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
match self {
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))]
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>,
@@ -75,13 +132,34 @@ pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore
)
.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)]
mod tests {
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};
#[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
}]
}
})
);
}
}

View File

@@ -28,3 +28,7 @@ headers.workspace = true
strum.workspace = true
matchit.workspace = true
matchit-serde.workspace = true
ical = { workspace = true, optional = true }
[features]
ical = ["dep:ical"]

View File

@@ -15,3 +15,7 @@ mod report_set;
pub use report_set::SupportedReportSet;
mod group;
pub use group::*;
#[cfg(feature = "ical")]
mod text_match;
#[cfg(feature = "ical")]
pub use text_match::*;

View File

@@ -64,9 +64,9 @@ pub struct TextMatchElement {
#[xml(ty = "attr", default = "Default::default")]
pub collation: TextCollation,
#[xml(ty = "attr", default = "Default::default")]
pub(crate) negate_condition: NegateCondition,
pub negate_condition: NegateCondition,
#[xml(ty = "text")]
pub(crate) needle: String,
pub needle: String,
}
impl TextMatchElement {
@@ -90,7 +90,7 @@ impl TextMatchElement {
#[cfg(test)]
mod tests {
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
use super::TextCollation;
#[test]
fn test_collation() {

View File

@@ -179,4 +179,9 @@ END:VCALENDAR",
}
Ok(out)
}
#[must_use]
pub fn get_vcard(&self) -> &VcardContact {
&self.vcard
}
}

View File

@@ -99,85 +99,85 @@ impl EventObject {
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());
let Some(mut rrule_set) = self.recurrence_ruleset()? else {
return Ok(vec![self.event.clone()]);
};
'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()])
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().as_ref());
'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)
}
}

View File

@@ -77,13 +77,23 @@ pub trait CalendarStore: Send + Sync + 'static {
object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error>;
async fn put_objects(
&self,
principal: String,
cal_id: String,
objects: Vec<CalendarObject>,
overwrite: bool,
) -> Result<(), Error>;
async fn put_object(
&self,
principal: String,
cal_id: String,
object: CalendarObject,
overwrite: bool,
) -> Result<(), Error>;
) -> Result<(), Error> {
self.put_objects(principal, cal_id, vec![object], overwrite)
.await
}
async fn delete_object(
&self,
principal: &str,

View File

@@ -147,15 +147,15 @@ impl CalendarStore for CombinedCalendarStore {
.await
}
async fn put_object(
async fn put_objects(
&self,
principal: String,
cal_id: String,
object: rustical_ical::CalendarObject,
objects: Vec<rustical_ical::CalendarObject>,
overwrite: bool,
) -> Result<(), crate::Error> {
self.store_for_id(&cal_id)
.put_object(principal, cal_id, object, overwrite)
.put_objects(principal, cal_id, objects, overwrite)
.await
}

View File

@@ -11,8 +11,13 @@ publish = false
[features]
test = ["dep:rstest"]
[[bench]]
name = "insert_calendar_object"
harness = false
[dev-dependencies]
rstest.workspace = true
criterion.workspace = true
[dependencies]
tokio.workspace = true

View 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

View File

@@ -0,0 +1,70 @@
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(), None).unwrap();
let batch_size = 1000;
let objects: Vec<_> = std::iter::repeat_n(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.clone(), true)
.await
.unwrap();
}
});
});
runtime
.block_on(cal_store.delete_calendar("user", "okwow", false))
.unwrap();
}
criterion_group!(benches, benchmark);
criterion_main!(benches);

View File

@@ -394,11 +394,11 @@ impl CalendarStore for SqliteAddressbookStore {
}
#[instrument]
async fn put_object(
async fn put_objects(
&self,
_principal: String,
_cal_id: String,
_object: CalendarObject,
_objects: Vec<CalendarObject>,
_overwrite: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)

View File

@@ -776,11 +776,11 @@ impl CalendarStore for SqliteCalendarStore {
}
#[instrument]
async fn put_object(
async fn put_objects(
&self,
principal: String,
cal_id: String,
object: CalendarObject,
objects: Vec<CalendarObject>,
overwrite: bool,
) -> Result<(), Error> {
let mut tx = self
@@ -789,33 +789,37 @@ impl CalendarStore for SqliteCalendarStore {
.await
.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?;
if calendar.subscription_url.is_some() {
// We cannot commit an object to a subscription calendar
return Err(Error::ReadOnly);
}
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
let sync_token = Self::log_object_operation(
&mut tx,
&principal,
&cal_id,
&object_id,
ChangeOperation::Add,
)
.await?;
let mut sync_token = None;
for object in objects {
sync_token = Some(
Self::log_object_operation(
&mut tx,
&principal,
&cal_id,
object.get_id(),
ChangeOperation::Add,
)
.await?,
);
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
}
tx.commit().await.map_err(crate::Error::from)?;
self.send_push_notification(
CollectionOperationInfo::Content { sync_token },
self.get_calendar(&principal, &cal_id, true)
.await?
.push_topic,
);
if let Some(sync_token) = sync_token {
self.send_push_notification(
CollectionOperationInfo::Content { sync_token },
self.get_calendar(&principal, &cal_id, true)
.await?
.push_topic,
);
}
Ok(())
}

View File

@@ -3,7 +3,7 @@ source: src/integration_tests/caldav/calendar.rs
expression: body
---
BEGIN:VCALENDAR
VERSION:4.0
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:RustiCal
X-WR-CALNAME:Calendar