Compare commits

...

9 Commits

Author SHA1 Message Date
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
15 changed files with 534 additions and 166 deletions

311
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.5"
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.5"
dependencies = [
"async-std",
"async-trait",
@@ -3155,7 +3348,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"async-trait",
"axum",
@@ -3188,7 +3381,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"async-trait",
"axum",
@@ -3213,7 +3406,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"async-trait",
"axum",
@@ -3238,7 +3431,7 @@ dependencies = [
[[package]]
name = "rustical_frontend"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"askama",
"askama_web",
@@ -3274,7 +3467,7 @@ dependencies = [
[[package]]
name = "rustical_ical"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"axum",
"chrono",
@@ -3291,7 +3484,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"async-trait",
"axum",
@@ -3307,7 +3500,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"anyhow",
"async-trait",
@@ -3340,10 +3533,11 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"async-trait",
"chrono",
"criterion",
"derive_more",
"password-auth",
"password-hash",
@@ -3362,7 +3556,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"quick-xml",
"thiserror 2.0.17",
@@ -4033,6 +4227,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 +4330,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 +4432,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 +4456,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 +4484,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 +4506,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 +4675,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 +4698,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 +5034,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 +5059,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 +5378,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "xml_derive"
version = "0.11.4"
version = "0.11.5"
dependencies = [
"darling 0.23.0",
"heck",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.11.4"
version = "0.11.5"
rust-version = "1.91"
edition = "2024"
description = "A CalDAV server"
@@ -32,7 +32,6 @@ opentelemetry = [
"dep:tracing-opentelemetry",
]
[profile.dev]
debug = 0
@@ -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

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

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

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

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