Compare commits

..

12 Commits

Author SHA1 Message Date
Lennart K
291bd967da Re-add get_last_occurence for sqlite store 2026-01-09 10:32:50 +01:00
Lennart K
002814a564 Remove unused code 2026-01-08 23:24:47 +01:00
Lennart K
ba13aaa703 Re-implement calendar imports 2026-01-08 23:17:39 +01:00
Lennart K
7a02bfeffc Calendar export: Fix PRODID 2026-01-08 16:17:39 +01:00
Lennart K
1b69148d6f Re-implement calendar export 2026-01-08 15:36:02 +01:00
Lennart K
f4de80c6b9 clean up ical-related stuff 2026-01-08 14:31:28 +01:00
Lennart K
7a1ec3e351 make calendar object id extrinsic 2026-01-07 13:14:50 +01:00
Lennart K
eb7bdd0018 Make AddressObject object_id an extrinsic property 2026-01-07 12:19:30 +01:00
Lennart K
8e583e24cb small fixes 2026-01-07 11:58:02 +01:00
Lennart K
5e5017a185 Decrease folder nesting 2026-01-07 11:46:28 +01:00
Lennart K
3c87191f69 incorporate get_first_occurenec 2026-01-07 11:44:55 +01:00
Lennart K
d1947a159b migrate to new ical-rs version 2026-01-07 11:32:53 +01:00
61 changed files with 614 additions and 2133 deletions

View File

@@ -2,7 +2,7 @@ name: Docker
on: on:
push: push:
branches: ["main", "dev"] branches: ["main"]
release: release:
types: ["published"] types: ["published"]

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM calendarobjectchangelog WHERE (principal, cal_id, object_id) = (?, ?, ?);",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "146e23ae4e0eaae4d65ac7563c67d4f295ccc2534dcc4b3bd710de773ed137f9"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE calendarobjects SET ics = ? WHERE (principal, cal_id, id) = (?, ?, ?);",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "354decac84758c88280f60fbf0f93dddc6c7ff92ac7b8ba44049d31df3c680e3"
}

View File

@@ -1,38 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT principal, cal_id, id, ics FROM calendarobjects WHERE ics LIKE '%VERSION:4.0%';",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "cal_id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "ics",
"ordinal": 3,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "bdaa4bee8b01d0e3773e34672ed4805d1e71d24888f2227045afd90bf080fc23"
}

190
Cargo.lock generated
View File

@@ -477,9 +477,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.8.3" version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82"
[[package]] [[package]]
name = "basic-toml" name = "basic-toml"
@@ -595,9 +595,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.43" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
@@ -689,9 +689,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.7.7" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
@@ -1422,9 +1422,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.17" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -1771,22 +1771,7 @@ dependencies = [
[[package]] [[package]]
name = "ical" name = "ical"
version = "0.11.0" version = "0.11.0"
source = "git+https://github.com/lennart-k/ical-rs?rev=7c2ab1f3#7c2ab1f3abdca768f22d8a36627eebbdd7947e29" source = "git+https://github.com/lennart-k/ical-rs?branch=dev#64c342e7258ba445dc91b47cd8e20e0ac8ffc417"
dependencies = [
"chrono",
"chrono-tz",
"derive_more",
"itertools 0.14.0",
"lazy_static",
"regex",
"rrule",
"thiserror 2.0.17",
]
[[package]]
name = "ical"
version = "0.12.0-dev"
source = "git+https://github.com/lennart-k/ical-rs?branch=dev#d2226f6b92fa45dcc5a243adc57e6a07a67741a8"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz", "chrono-tz",
@@ -1938,9 +1923,9 @@ checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.46.1" version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8" checksum = "1b66886d14d18d420ab5052cbff544fc5d34d0b2cdd35eb5976aaa10a4a472e5"
dependencies = [ dependencies = [
"console", "console",
"once_cell", "once_cell",
@@ -2006,9 +1991,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.85" version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -2257,7 +2242,7 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"getrandom 0.2.17", "getrandom 0.2.16",
"http", "http",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest",
@@ -2536,7 +2521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524" checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524"
dependencies = [ dependencies = [
"argon2", "argon2",
"getrandom 0.2.17", "getrandom 0.2.16",
"password-hash", "password-hash",
"pbkdf2", "pbkdf2",
"rand_core 0.6.4", "rand_core 0.6.4",
@@ -2862,9 +2847,9 @@ dependencies = [
[[package]] [[package]]
name = "prost" name = "prost"
version = "0.14.3" version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
dependencies = [ dependencies = [
"bytes", "bytes",
"prost-derive", "prost-derive",
@@ -2872,9 +2857,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-derive" name = "prost-derive"
version = "0.14.3" version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.14.0", "itertools 0.14.0",
@@ -2980,7 +2965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rand_core 0.9.5", "rand_core 0.9.3",
] ]
[[package]] [[package]]
@@ -3000,7 +2985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core 0.9.5", "rand_core 0.9.3",
] ]
[[package]] [[package]]
@@ -3009,14 +2994,14 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom 0.2.17", "getrandom 0.2.16",
] ]
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.9.5" version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
] ]
@@ -3175,7 +3160,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom 0.2.17", "getrandom 0.2.16",
"libc", "libc",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -3277,9 +3262,9 @@ dependencies = [
[[package]] [[package]]
name = "rust-embed" name = "rust-embed"
version = "8.11.0" version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca"
dependencies = [ dependencies = [
"rust-embed-impl", "rust-embed-impl",
"rust-embed-utils", "rust-embed-utils",
@@ -3288,9 +3273,9 @@ dependencies = [
[[package]] [[package]]
name = "rust-embed-impl" name = "rust-embed-impl"
version = "8.11.0" version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3301,9 +3286,9 @@ dependencies = [
[[package]] [[package]]
name = "rust-embed-utils" name = "rust-embed-utils"
version = "8.11.0" version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475"
dependencies = [ dependencies = [
"sha2", "sha2",
"walkdir", "walkdir",
@@ -3311,9 +3296,9 @@ dependencies = [
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.27" version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
@@ -3332,7 +3317,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3343,7 +3328,6 @@ dependencies = [
"figment", "figment",
"headers", "headers",
"http", "http",
"ical 0.12.0-dev",
"insta", "insta",
"opentelemetry", "opentelemetry",
"opentelemetry-otlp", "opentelemetry-otlp",
@@ -3366,7 +3350,7 @@ dependencies = [
"serde", "serde",
"sqlx", "sqlx",
"tokio", "tokio",
"toml 0.9.11+spec-1.1.0", "toml 0.9.10+spec-1.1.0",
"tower", "tower",
"tower-http", "tower-http",
"tower-sessions", "tower-sessions",
@@ -3378,7 +3362,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3391,7 +3375,7 @@ dependencies = [
"futures-util", "futures-util",
"headers", "headers",
"http", "http",
"ical 0.11.0", "ical",
"insta", "insta",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml",
@@ -3420,7 +3404,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3430,7 +3414,7 @@ dependencies = [
"derive_more", "derive_more",
"futures-util", "futures-util",
"http", "http",
"ical 0.11.0", "ical",
"insta", "insta",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml",
@@ -3454,7 +3438,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3463,7 +3447,7 @@ dependencies = [
"futures-util", "futures-util",
"headers", "headers",
"http", "http",
"ical 0.11.0", "ical",
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
"matchit 0.9.1", "matchit 0.9.1",
@@ -3480,7 +3464,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3505,7 +3489,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3541,13 +3525,13 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"derive_more", "derive_more",
"ical 0.11.0", "ical",
"regex", "regex",
"rrule", "rrule",
"rstest", "rstest",
@@ -3560,7 +3544,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3576,7 +3560,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3588,7 +3572,7 @@ dependencies = [
"futures-core", "futures-core",
"headers", "headers",
"http", "http",
"ical 0.11.0", "ical",
"regex", "regex",
"rrule", "rrule",
"rstest", "rstest",
@@ -3609,16 +3593,16 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
"criterion", "criterion",
"derive_more", "derive_more",
"ical",
"password-auth", "password-auth",
"password-hash", "password-hash",
"pbkdf2", "pbkdf2",
"regex",
"rstest", "rstest",
"rustical_ical", "rustical_ical",
"rustical_store", "rustical_store",
@@ -3633,7 +3617,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -3669,9 +3653,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.13.3" version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4910321ebe4151be888e35fe062169554e74aad01beafed60410131420ceffbc" checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
dependencies = [ dependencies = [
"web-time", "web-time",
"zeroize", "zeroize",
@@ -4369,30 +4353,30 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.45" version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
"num-conv", "num-conv",
"powerfmt", "powerfmt",
"serde_core", "serde",
"time-core", "time-core",
"time-macros", "time-macros",
] ]
[[package]] [[package]]
name = "time-core" name = "time-core"
version = "0.1.7" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.25" version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",
@@ -4510,9 +4494,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.9.11+spec-1.1.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 = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
dependencies = [ dependencies = [
"indexmap 2.13.0", "indexmap 2.13.0",
"serde_core", "serde_core",
@@ -4627,9 +4611,9 @@ dependencies = [
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.3" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
@@ -4797,13 +4781,16 @@ dependencies = [
[[package]] [[package]]
name = "tracing-opentelemetry" name = "tracing-opentelemetry"
version = "0.32.1" version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"opentelemetry", "opentelemetry",
"opentelemetry_sdk",
"rustversion",
"smallvec", "smallvec",
"thiserror 2.0.17",
"tracing", "tracing",
"tracing-core", "tracing-core",
"tracing-log", "tracing-log",
@@ -5000,9 +4987,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasip2" name = "wasip2"
version = "1.0.2+wasi-0.2.9" version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [ dependencies = [
"wit-bindgen", "wit-bindgen",
] ]
@@ -5015,9 +5002,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.108" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -5028,12 +5015,11 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.58" version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util",
"js-sys", "js-sys",
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -5042,9 +5028,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.108" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -5052,9 +5038,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.108" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -5065,18 +5051,18 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.108" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.85" version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -5443,9 +5429,9 @@ dependencies = [
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.51.0" version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]] [[package]]
name = "writeable" name = "writeable"
@@ -5455,7 +5441,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]] [[package]]
name = "xml_derive" name = "xml_derive"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"darling 0.23.0", "darling 0.23.0",
"heck", "heck",
@@ -5576,6 +5562,6 @@ dependencies = [
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.14" version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.11.17" version = "0.11.10"
rust-version = "1.92" rust-version = "1.92"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
@@ -107,7 +107,9 @@ 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", rev = "7c2ab1f3" } ical = { git = "https://github.com/lennart-k/ical-rs", branch = "dev", 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 = [
@@ -199,7 +201,3 @@ tower-http.workspace = true
axum-extra.workspace = true axum-extra.workspace = true
headers.workspace = true headers.workspace = true
http.workspace = true http.workspace = true
# TODO: Remove in next major release
ical_dev = { package = "ical", git = "https://github.com/lennart-k/ical-rs", branch = "dev", features = [
"chrono-tz",
] }

View File

@@ -36,7 +36,7 @@ COPY --from=planner /rustical/recipe.json recipe.json
RUN cargo chef cook --release --target "$(cat /tmp/rust_target)" RUN cargo chef cook --release --target "$(cat /tmp/rust_target)"
COPY . . COPY . .
RUN cargo install --locked --target "$(cat /tmp/rust_target)" --path . RUN cargo install --target "$(cat /tmp/rust_target)" --path .
FROM scratch FROM scratch
COPY --from=builder /usr/local/cargo/bin/rustical /usr/local/bin/rustical COPY --from=builder /usr/local/cargo/bin/rustical /usr/local/bin/rustical

View File

@@ -5,13 +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::builder::calendar::IcalCalendarBuilder; use ical::component::IcalCalendar;
use ical::generator::Emitter; use ical::generator::Emitter;
use ical::property::Property; use ical::property::ContentLine;
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::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -33,60 +31,45 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let mut vtimezones = HashMap::new(); let objects = cal_store
let objects = cal_store.get_objects(&principal, &calendar_id).await?; .get_objects(&principal, &calendar_id)
.await?
.into_iter()
.map(|(_, object)| object.into())
.collect();
let mut props = vec![];
let mut ical_calendar_builder = IcalCalendarBuilder::version("2.0")
.gregorian()
.prodid("RustiCal");
if let Some(displayname) = calendar.meta.displayname { if let Some(displayname) = calendar.meta.displayname {
ical_calendar_builder = ical_calendar_builder.set(Property { props.push(ContentLine {
name: "X-WR-CALNAME".to_owned(), name: "X-WR-CALNAME".to_owned(),
value: Some(displayname), value: Some(displayname),
params: vec![], params: vec![].into(),
}); });
} }
if let Some(description) = calendar.meta.description { if let Some(description) = calendar.meta.description {
ical_calendar_builder = ical_calendar_builder.set(Property { props.push(ContentLine {
name: "X-WR-CALDESC".to_owned(), name: "X-WR-CALDESC".to_owned(),
value: Some(description), value: Some(description),
params: vec![], params: vec![].into(),
});
}
if let Some(color) = calendar.meta.color {
props.push(ContentLine {
name: "X-WR-CALCOLOR".to_owned(),
value: Some(color),
params: vec![].into(),
}); });
} }
if let Some(timezone_id) = calendar.timezone_id { if let Some(timezone_id) = calendar.timezone_id {
ical_calendar_builder = ical_calendar_builder.set(Property { props.push(ContentLine {
name: "X-WR-TIMEZONE".to_owned(), name: "X-WR-TIMEZONE".to_owned(),
value: Some(timezone_id), value: Some(timezone_id),
params: vec![], params: vec![].into(),
}); });
} }
for object in &objects { let export_calendar = IcalCalendar::from_objects("RustiCal Export".to_owned(), objects, props);
vtimezones.extend(object.get_vtimezones());
match object.get_data() {
CalendarObjectComponent::Event(EventObject { event, .. }, overrides) => {
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())
.add_todos(overrides.iter().cloned());
}
CalendarObjectComponent::Journal(journal, overrides) => {
ical_calendar_builder = ical_calendar_builder
.add_journal(journal.clone())
.add_journals(overrides.iter().cloned());
}
}
}
ical_calendar_builder = ical_calendar_builder.add_timezones(vtimezones.into_values().cloned());
let ical_calendar = ical_calendar_builder
.build()
.map_err(|parser_error| Error::IcalError(parser_error.into()))?;
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();
@@ -104,6 +87,6 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
if matches!(method, Method::HEAD) { if matches!(method, Method::HEAD) {
Ok(resp.body(Body::empty()).unwrap()) Ok(resp.body(Body::empty()).unwrap())
} else { } else {
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap()) Ok(resp.body(Body::new(export_calendar.generate())).unwrap())
} }
} }

View File

@@ -5,16 +5,13 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use http::StatusCode; use http::StatusCode;
use ical::{ use ical::parser::{Component, ComponentMut};
generator::Emitter,
parser::{Component, ComponentMut},
};
use rustical_dav::header::Overwrite; use rustical_dav::header::Overwrite;
use rustical_ical::{CalendarObject, CalendarObjectType}; use rustical_ical::CalendarObjectType;
use rustical_store::{ use rustical_store::{
Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal, Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal,
}; };
use std::io::BufReader; use std::{collections::HashMap, io::BufReader};
use tracing::instrument; use tracing::instrument;
#[instrument(skip(resource_service))] #[instrument(skip(resource_service))]
@@ -29,18 +26,11 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let mut parser = ical::IcalParser::new(BufReader::new(body.as_bytes())); let parser = ical::IcalParser::new(BufReader::new(body.as_bytes()));
let mut cal = parser let mut cal = parser
.next() .expect_one()
.expect("input must contain calendar") .map_err(rustical_ical::Error::ParserError)?
.unwrap()
.mutable(); .mutable();
if parser.next().is_some() {
return Err(rustical_ical::Error::InvalidData(
"multiple calendars, only one allowed".to_owned(),
)
.into());
}
// Extract calendar metadata // Extract calendar metadata
let displayname = cal let displayname = cal
@@ -49,14 +39,19 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
let description = cal let description = cal
.get_property("X-WR-CALDESC") .get_property("X-WR-CALDESC")
.and_then(|prop| prop.value.clone()); .and_then(|prop| prop.value.clone());
let color = cal
.get_property("X-WR-CALCOLOR")
.and_then(|prop| prop.value.clone());
let timezone_id = cal let timezone_id = cal
.get_property("X-WR-TIMEZONE") .get_property("X-WR-TIMEZONE")
.and_then(|prop| prop.value.clone()); .and_then(|prop| prop.value.clone());
// These properties should not appear in the expanded calendar objects // These properties should not appear in the expanded calendar objects
cal.remove_property("X-WR-CALNAME"); cal.remove_property("X-WR-CALNAME");
cal.remove_property("X-WR-CALDESC"); cal.remove_property("X-WR-CALDESC");
cal.remove_property("X-WR-CALCOLOR");
cal.remove_property("X-WR-TIMEZONE"); cal.remove_property("X-WR-TIMEZONE");
let cal = cal.verify().unwrap(); let cal = cal.build(&HashMap::new()).unwrap();
// Make sure timezone is valid // Make sure timezone is valid
if let Some(timezone_id) = timezone_id.as_ref() { if let Some(timezone_id) = timezone_id.as_ref() {
assert!( assert!(
@@ -64,8 +59,7 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
"Invalid calendar timezone id" "Invalid calendar timezone id"
); );
} }
// // Extract necessary component types
// Extract necessary component types
let mut cal_components = vec![]; let mut cal_components = vec![];
if !cal.events.is_empty() { if !cal.events.is_empty() {
cal_components.push(CalendarObjectType::Event); cal_components.push(CalendarObjectType::Event);
@@ -77,13 +71,12 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
cal_components.push(CalendarObjectType::Todo); cal_components.push(CalendarObjectType::Todo);
} }
let expanded_cals = cal.expand_calendar(); let objects = cal
// Janky way to convert between IcalCalendar and CalendarObject .into_objects()
let objects = expanded_cals .map_err(rustical_ical::Error::ParserError)?
.into_iter() .into_iter()
.map(|cal| cal.generate()) .map(Into::into)
.map(|ics| CalendarObject::from_ics(ics, None)) .collect();
.collect::<Result<Vec<_>, _>>()?;
let new_cal = Calendar { let new_cal = Calendar {
principal, principal,
id: cal_id, id: cal_id,
@@ -91,7 +84,7 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
displayname, displayname,
order: 0, order: 0,
description, description,
color: None, color,
}, },
timezone_id, timezone_id,
deleted_at: None, deleted_at: None,

View File

@@ -92,10 +92,11 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
.ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))? .ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?
.map_err(|_| rustical_dav::Error::BadRequest("Error parsing timezone".to_owned()))?; .map_err(|_| rustical_dav::Error::BadRequest("Error parsing timezone".to_owned()))?;
let timezone = calendar.timezones.first().ok_or_else(|| { let timezone = calendar.vtimezones.values().next().ok_or_else(|| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?; })?;
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| { let timezone: Option<chrono_tz::Tz> = timezone.into();
let timezone = timezone.ok_or_else(|| {
rustical_dav::Error::BadRequest("Cannot translate VTIMEZONE into IANA TZID".to_owned()) rustical_dav::Error::BadRequest("Cannot translate VTIMEZONE into IANA TZID".to_owned())
})?; })?;

View File

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

View File

@@ -1,9 +1,10 @@
use crate::calendar::methods::report::calendar_query::{ use crate::calendar::methods::report::calendar_query::{
TimeRangeElement, TimeRangeElement, prop_filter::PropFilterElement,
prop_filter::{PropFilterElement, PropFilterable}, };
use ical::{
component::IcalCalendarObject,
parser::{Component, ical::component::IcalTimeZone},
}; };
use ical::parser::ical::component::IcalTimeZone;
use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType};
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
@@ -24,9 +25,7 @@ pub struct CompFilterElement {
pub(crate) name: String, pub(crate) name: String,
} }
pub trait CompFilterable: PropFilterable + Sized { pub trait CompFilterable: Component + Sized {
fn get_comp_name(&self) -> &'static str;
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool; fn match_time_range(&self, time_range: &TimeRangeElement) -> bool;
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool; fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool;
@@ -68,11 +67,7 @@ pub trait CompFilterable: PropFilterable + Sized {
} }
} }
impl CompFilterable for CalendarObject { impl CompFilterable for IcalCalendarObject {
fn get_comp_name(&self) -> &'static str {
"VCALENDAR"
}
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool { fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
// VCALENDAR has no concept of time range // VCALENDAR has no concept of time range
false false
@@ -83,7 +78,7 @@ impl CompFilterable for CalendarObject {
.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)
@@ -94,10 +89,6 @@ impl CompFilterable for CalendarObject {
} }
impl CompFilterable for IcalTimeZone { impl CompFilterable for IcalTimeZone {
fn get_comp_name(&self) -> &'static str {
"VTIMEZONE"
}
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool { fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
false false
} }
@@ -107,33 +98,6 @@ impl CompFilterable for IcalTimeZone {
} }
} }
impl CompFilterable for CalendarObjectComponent {
fn get_comp_name(&self) -> &'static str {
CalendarObjectType::from(self).as_str()
}
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool {
if let Some(start) = &time_range.start
&& let Some(last_occurence) = self.get_last_occurence().unwrap_or(None)
&& **start > last_occurence.utc()
{
return false;
}
if let Some(end) = &time_range.end
&& let Some(first_occurence) = self.get_first_occurence().unwrap_or(None)
&& **end < first_occurence.utc()
{
return false;
}
true
}
fn match_subcomponents(&self, _comp_filter: &CompFilterElement) -> bool {
// TODO: Properly check subcomponents
true
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
@@ -166,7 +130,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(()),
@@ -175,7 +139,10 @@ END:VCALENDAR";
prop_filter: vec![], prop_filter: vec![],
comp_filter: vec![], comp_filter: vec![],
}; };
assert!(!object.matches(&comp_filter), "filter: wants no VCALENDAR"); assert!(
!object.get_inner().matches(&comp_filter),
"filter: wants no VCALENDAR"
);
let comp_filter = CompFilterElement { let comp_filter = CompFilterElement {
is_not_defined: None, is_not_defined: None,
@@ -190,7 +157,10 @@ END:VCALENDAR";
comp_filter: vec![], comp_filter: vec![],
}], }],
}; };
assert!(!object.matches(&comp_filter), "filter matches VTODO"); assert!(
!object.get_inner().matches(&comp_filter),
"filter matches VTODO"
);
let comp_filter = CompFilterElement { let comp_filter = CompFilterElement {
is_not_defined: None, is_not_defined: None,
@@ -205,7 +175,10 @@ END:VCALENDAR";
comp_filter: vec![], comp_filter: vec![],
}], }],
}; };
assert!(object.matches(&comp_filter), "filter matches VEVENT"); assert!(
object.get_inner().matches(&comp_filter),
"filter matches VEVENT"
);
let comp_filter = CompFilterElement { let comp_filter = CompFilterElement {
is_not_defined: None, is_not_defined: None,
@@ -252,13 +225,13 @@ END:VCALENDAR";
}], }],
}; };
assert!( assert!(
object.matches(&comp_filter), object.get_inner().matches(&comp_filter),
"Some prop filters on VCALENDAR and VEVENT" "Some prop filters on VCALENDAR and VEVENT"
); );
} }
#[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,
@@ -281,7 +254,7 @@ END:VCALENDAR";
}], }],
}; };
assert!( assert!(
object.matches(&comp_filter), object.get_inner().matches(&comp_filter),
"event should lie in time range" "event should lie in time range"
); );
@@ -306,14 +279,14 @@ END:VCALENDAR";
}], }],
}; };
assert!( assert!(
!object.matches(&comp_filter), !object.get_inner().matches(&comp_filter),
"event should not lie in time range" "event should not lie in time range"
); );
} }
#[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,
@@ -340,7 +313,7 @@ END:VCALENDAR";
}], }],
}; };
assert!( assert!(
object.matches(&comp_filter), object.get_inner().matches(&comp_filter),
"Timezone should be Europe/Berlin" "Timezone should be Europe/Berlin"
); );
} }

View File

@@ -1,8 +1,8 @@
use super::comp_filter::{CompFilterElement, CompFilterable}; use super::comp_filter::{CompFilterElement, CompFilterable};
use crate::calendar_object::CalendarObjectPropWrapperName; use crate::calendar_object::CalendarObjectPropWrapperName;
use ical::property::Property; use ical::{component::IcalCalendarObject, property::ContentLine};
use rustical_dav::xml::{PropfindType, TextMatchElement}; use rustical_dav::xml::{PropfindType, TextMatchElement};
use rustical_ical::{CalendarObject, UtcDateTime}; use rustical_ical::UtcDateTime;
use rustical_store::calendar_store::CalendarQuery; use rustical_store::calendar_store::CalendarQuery;
use rustical_xml::{XmlDeserialize, XmlRootTag}; use rustical_xml::{XmlDeserialize, XmlRootTag};
@@ -30,8 +30,8 @@ pub struct ParamFilterElement {
impl ParamFilterElement { impl ParamFilterElement {
#[must_use] #[must_use]
pub fn match_property(&self, prop: &Property) -> bool { pub fn match_property(&self, prop: &ContentLine) -> bool {
let Some(param) = prop.get_param(&self.name) else { let Some(param) = prop.params.get_param(&self.name) else {
return self.is_not_defined.is_some(); return self.is_not_defined.is_some();
}; };
if self.is_not_defined.is_some() { if self.is_not_defined.is_some() {
@@ -57,7 +57,7 @@ pub struct FilterElement {
impl FilterElement { impl FilterElement {
#[must_use] #[must_use]
pub fn matches(&self, cal_object: &CalendarObject) -> bool { pub fn matches(&self, cal_object: &IcalCalendarObject) -> bool {
cal_object.matches(&self.comp_filter) cal_object.matches(&self.comp_filter)
} }
} }

View File

@@ -11,19 +11,19 @@ mod tests;
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;
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(|(_id, object)| filter.matches(object.get_inner()));
} }
Ok(objects) Ok(objects)
} }

View File

@@ -1,14 +1,7 @@
use super::{ParamFilterElement, TimeRangeElement}; use super::{ParamFilterElement, TimeRangeElement};
use ical::{ use ical::{parser::Component, property::ContentLine, types::CalDateTime};
generator::{IcalCalendar, IcalEvent},
parser::{
Component,
ical::component::{IcalJournal, IcalTimeZone, IcalTodo},
},
property::Property,
};
use rustical_dav::xml::TextMatchElement; use rustical_dav::xml::TextMatchElement;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectComponent, UtcDateTime}; use rustical_ical::UtcDateTime;
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
use std::collections::HashMap; use std::collections::HashMap;
@@ -31,7 +24,7 @@ pub struct PropFilterElement {
impl PropFilterElement { impl PropFilterElement {
#[must_use] #[must_use]
pub fn match_property(&self, property: &Property) -> bool { pub fn match_property(&self, property: &ContentLine) -> bool {
if let Some(TimeRangeElement { start, end }) = &self.time_range { if let Some(TimeRangeElement { start, end }) = &self.time_range {
// TODO: Respect timezones // TODO: Respect timezones
let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else { let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else {
@@ -68,7 +61,7 @@ impl PropFilterElement {
true true
} }
pub fn match_component(&self, comp: &impl PropFilterable) -> bool { pub fn match_component(&self, comp: &impl Component) -> bool {
let properties = comp.get_named_properties(&self.name); let properties = comp.get_named_properties(&self.name);
if self.is_not_defined.is_some() { if self.is_not_defined.is_some() {
return properties.is_empty(); return properties.is_empty();
@@ -79,53 +72,3 @@ impl PropFilterElement {
properties.iter().any(|prop| self.match_property(prop)) properties.iter().any(|prop| self.match_property(prop))
} }
} }
pub trait PropFilterable {
fn get_named_properties(&self, name: &str) -> Vec<&Property>;
}
impl PropFilterable for CalendarObject {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Self::get_named_properties(self, name)
}
}
impl PropFilterable for IcalEvent {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for IcalTodo {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for IcalJournal {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for IcalCalendar {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for IcalTimeZone {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for CalendarObjectComponent {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
match self {
Self::Event(event, _) => PropFilterable::get_named_properties(&event.event, name),
Self::Todo(todo, _) => PropFilterable::get_named_properties(todo, name),
Self::Journal(journal, _) => PropFilterable::get_named_properties(journal, name),
}
}
}

View File

@@ -77,7 +77,7 @@ const FILTER_2: &str = r#"
#[case(ICS_1, FILTER_1, true)] #[case(ICS_1, FILTER_1, true)]
#[case(ICS_1, FILTER_2, false)] #[case(ICS_1, FILTER_2, false)]
fn yeet(#[case] ics: &str, #[case] filter: &str, #[case] matches: bool) { fn yeet(#[case] ics: &str, #[case] filter: &str, #[case] matches: bool) {
let obj = CalendarObject::from_ics(ics.to_owned(), None).unwrap(); let obj = CalendarObject::from_ics(ics.to_owned()).unwrap();
let filter = FilterElement::parse_str(filter).unwrap(); let filter = FilterElement::parse_str(filter).unwrap();
assert_eq!(matches, filter.matches(&obj)); assert_eq!(matches, filter.matches(obj.get_inner()));
} }

View File

@@ -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!("{path}/{object_id}.ics");
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)?,

View File

@@ -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)?,

View File

@@ -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};
@@ -215,13 +215,13 @@ impl Resource for CalendarResource {
) )
})?; })?;
let timezone = calendar.timezones.first().ok_or_else(|| { let timezone = calendar.vtimezones.values().next().ok_or_else(|| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?; })?;
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| { let timezone: Option<chrono_tz::Tz> = timezone.into();
let timezone = timezone.ok_or_else(|| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?; })?;
self.cal.timezone_id = Some(timezone.name().to_owned()); self.cal.timezone_id = Some(timezone.name().to_owned());
} }
Ok(()) Ok(())

View File

@@ -78,8 +78,9 @@ 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,
object_id,
principal: principal.to_owned(), principal: principal.to_owned(),
}) })
.collect()) .collect())
@@ -91,7 +92,7 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
file: Self::Resource, file: Self::Resource,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.cal_store self.cal_store
.update_calendar(principal.to_owned(), cal_id.to_owned(), file.into()) .update_calendar(principal, cal_id, file.into())
.await?; .await?;
Ok(()) Ok(())
} }

View File

@@ -94,13 +94,13 @@ 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));
}; };
let etag = object.get_etag(); let etag = object.get_etag();
cal_store cal_store
.put_object(principal, calendar_id, object, overwrite) .put_object(&principal, &calendar_id, &object_id, object, overwrite)
.await?; .await?;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();

View File

@@ -1,11 +1,10 @@
use std::borrow::Cow;
use super::prop::{ use super::prop::{
CalendarData, CalendarObjectProp, CalendarObjectPropName, CalendarObjectPropWrapper, CalendarData, CalendarObjectProp, CalendarObjectPropName, CalendarObjectPropWrapper,
CalendarObjectPropWrapperName, CalendarObjectPropWrapperName,
}; };
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,
@@ -14,16 +13,18 @@ use rustical_dav::{
}; };
use rustical_ical::CalendarObject; use rustical_ical::CalendarObject;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use std::borrow::Cow;
#[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) -> Cow<'_, str> { fn get_name(&self) -> Cow<'_, str> {
Cow::from(format!("{}.ics", self.object.get_id())) Cow::from(format!("{}.ics", self.object_id))
} }
} }
@@ -53,14 +54,18 @@ impl Resource for CalendarObjectResource {
CalendarObjectProp::Getetag(self.object.get_etag()) CalendarObjectProp::Getetag(self.object.get_etag())
} }
CalendarObjectPropName::CalendarData(CalendarData { expand, .. }) => { CalendarObjectPropName::CalendarData(CalendarData { expand, .. }) => {
CalendarObjectProp::CalendarData(if let Some(expand) = expand.as_ref() { CalendarObjectProp::CalendarData(expand.as_ref().map_or_else(
self.object.expand_recurrence( || self.object.get_ics().to_owned(),
|expand| {
self.object
.get_inner()
.expand_recurrence(
Some(expand.start.to_utc()), Some(expand.start.to_utc()),
Some(expand.end.to_utc()), Some(expand.end.to_utc()),
)? )
} else { .generate()
self.object.get_ics().to_owned() },
}) ))
} }
CalendarObjectPropName::Getcontenttype => { CalendarObjectPropName::Getcontenttype => {
CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8") CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8")

View File

@@ -66,6 +66,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
.await?; .await?;
Ok(CalendarObjectResource { Ok(CalendarObjectResource {
object, object,
object_id: object_id.to_owned(),
principal: principal.to_owned(), principal: principal.to_owned(),
}) })
} }

View File

@@ -103,10 +103,10 @@ pub async fn put_object<AS: AddressbookStore>(
true true
}; };
let object = AddressObject::from_vcf(object_id, body)?; let object = AddressObject::from_vcf(body)?;
let etag = object.get_etag(); let etag = object.get_etag();
addr_store addr_store
.put_object(principal, addressbook_id, object, overwrite) .put_object(&principal, &addressbook_id, &object_id, object, overwrite)
.await?; .await?;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();

View File

@@ -21,11 +21,12 @@ use rustical_store::auth::Principal;
pub struct AddressObjectResource { pub struct AddressObjectResource {
pub object: AddressObject, pub object: AddressObject,
pub principal: String, pub principal: String,
pub object_id: String,
} }
impl ResourceName for AddressObjectResource { impl ResourceName for AddressObjectResource {
fn get_name(&self) -> Cow<'_, str> { fn get_name(&self) -> Cow<'_, str> {
Cow::from(format!("{}.vcf", self.object.get_id())) Cow::from(format!("{}.vcf", self.object_id))
} }
} }
@@ -69,7 +70,8 @@ impl Resource for AddressObjectResource {
} }
fn get_displayname(&self) -> Option<&str> { fn get_displayname(&self) -> Option<&str> {
self.object.get_full_name() todo!()
// self.object.get_full_name()
} }
fn get_owner(&self) -> Option<&str> { fn get_owner(&self) -> Option<&str> {

View File

@@ -57,6 +57,7 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
.await?; .await?;
Ok(AddressObjectResource { Ok(AddressObjectResource {
object, object,
object_id: object_id.to_owned(),
principal: principal.to_owned(), principal: principal.to_owned(),
}) })
} }

View File

@@ -9,7 +9,6 @@ use http::{HeaderValue, Method, StatusCode, header};
use percent_encoding::{CONTROLS, utf8_percent_encode}; use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_dav::privileges::UserPrivilege; use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_ical::AddressObject;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::str::FromStr; use std::str::FromStr;
@@ -40,7 +39,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
let objects = addr_store.get_objects(&principal, &addressbook_id).await?; let objects = addr_store.get_objects(&principal, &addressbook_id).await?;
let vcf = objects let vcf = objects
.iter() .iter()
.map(AddressObject::get_vcf) .map(|(_id, obj)| obj.get_vcf())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\r\n"); .join("\r\n");

View File

@@ -1,4 +1,4 @@
use std::io::BufReader; use std::{collections::HashMap, io::BufReader};
use crate::Error; use crate::Error;
use crate::addressbook::AddressbookResourceService; use crate::addressbook::AddressbookResourceService;
@@ -9,7 +9,7 @@ use axum::{
use http::StatusCode; use http::StatusCode;
use ical::{ use ical::{
parser::{Component, ComponentMut, vcard}, parser::{Component, ComponentMut, vcard},
property::Property, property::ContentLine,
}; };
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::Principal}; use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::Principal};
use tracing::instrument; use tracing::instrument;
@@ -33,15 +33,16 @@ pub async fn route_import<AS: AddressbookStore, S: SubscriptionStore>(
let uid = card.get_uid(); let uid = card.get_uid();
if uid.is_none() { if uid.is_none() {
let mut card_mut = card.mutable(); let mut card_mut = card.mutable();
card_mut.set_property(Property { card_mut.add_content_line(ContentLine {
name: "UID".to_owned(), name: "UID".to_owned(),
value: Some(uuid::Uuid::new_v4().to_string()), value: Some(uuid::Uuid::new_v4().to_string()),
params: vec![], params: vec![].into(),
}); });
card = card_mut.verify().unwrap(); card = card_mut.build(&HashMap::new()).unwrap();
} }
// TODO: Make nicer
objects.push(card.try_into().unwrap()); let uid = card.get_uid().unwrap();
objects.push((uid.to_owned(), card.into()));
} }
if objects.is_empty() { if objects.is_empty() {

View File

@@ -29,7 +29,7 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
store: &AS, store: &AS,
) -> Result<(Vec<AddressObject>, Vec<String>), Error> { ) -> Result<(Vec<(String, AddressObject)>, Vec<String>), Error> {
let mut result = vec![]; let mut result = vec![];
let mut not_found = vec![]; let mut not_found = vec![];
@@ -43,7 +43,7 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
.get_object(principal, addressbook_id, object_id, false) .get_object(principal, addressbook_id, object_id, false)
.await .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()),
} }
@@ -74,11 +74,12 @@ pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
.await?; .await?;
let mut responses = Vec::new(); let mut responses = Vec::new();
for object in objects { for (object_id, object) in objects {
let path = format!("{}/{}.vcf", path, object.get_id()); let path = format!("{path}/{object_id}.vcf");
responses.push( responses.push(
AddressObjectResource { AddressObjectResource {
object, object,
object_id,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, prop, None, puri, user)?, .propfind(&path, prop, None, puri, user)?,

View File

@@ -3,7 +3,7 @@ use crate::{
addressbook::methods::report::addressbook_query::PropFilterElement, addressbook::methods::report::addressbook_query::PropFilterElement,
}; };
use derive_more::{From, Into}; use derive_more::{From, Into};
use ical::property::Property; use ical::property::ContentLine;
use rustical_dav::xml::{PropfindType, TextMatchElement}; use rustical_dav::xml::{PropfindType, TextMatchElement};
use rustical_ical::{AddressObject, UtcDateTime}; use rustical_ical::{AddressObject, UtcDateTime};
use rustical_xml::{ValueDeserialize, XmlDeserialize, XmlRootTag}; use rustical_xml::{ValueDeserialize, XmlDeserialize, XmlRootTag};
@@ -32,8 +32,8 @@ pub struct ParamFilterElement {
impl ParamFilterElement { impl ParamFilterElement {
#[must_use] #[must_use]
pub fn match_property(&self, prop: &Property) -> bool { pub fn match_property(&self, prop: &ContentLine) -> bool {
let Some(param) = prop.get_param(&self.name) else { let Some(param) = prop.params.get_param(&self.name) else {
return self.is_not_defined.is_some(); return self.is_not_defined.is_some();
}; };
if self.is_not_defined.is_some() { if self.is_not_defined.is_some() {

View File

@@ -15,8 +15,8 @@ pub async fn get_objects_addressbook_query<AS: AddressbookStore>(
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
store: &AS, store: &AS,
) -> Result<Vec<AddressObject>, Error> { ) -> Result<Vec<(String, AddressObject)>, Error> {
let mut objects = store.get_objects(principal, addressbook_id).await?; let mut objects = store.get_objects(principal, addressbook_id).await?;
objects.retain(|object| addr_query.filter.matches(object)); objects.retain(|(_id, object)| addr_query.filter.matches(object));
Ok(objects) Ok(objects)
} }

View File

@@ -1,5 +1,5 @@
use super::{Allof, ParamFilterElement}; use super::{Allof, ParamFilterElement};
use ical::{parser::Component, property::Property}; use ical::{parser::Component, property::ContentLine};
use rustical_dav::xml::TextMatchElement; use rustical_dav::xml::TextMatchElement;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
@@ -31,7 +31,7 @@ pub struct PropFilterElement {
impl PropFilterElement { impl PropFilterElement {
#[must_use] #[must_use]
pub fn match_property(&self, property: &Property) -> bool { pub fn match_property(&self, property: &ContentLine) -> bool {
if self.param_filter.is_empty() && self.text_match.is_empty() { if self.param_filter.is_empty() && self.text_match.is_empty() {
// Filter empty // Filter empty
return true; return true;
@@ -67,11 +67,11 @@ impl PropFilterElement {
} }
pub trait PropFilterable { pub trait PropFilterable {
fn get_named_properties(&self, name: &str) -> Vec<&Property>; fn get_named_properties(&self, name: &str) -> Vec<&ContentLine>;
} }
impl PropFilterable for AddressObject { impl PropFilterable for AddressObject {
fn get_named_properties(&self, name: &str) -> Vec<&Property> { fn get_named_properties(&self, name: &str) -> Vec<&ContentLine> {
self.get_vcard().get_named_properties(name) self.get_vcard().get_named_properties(name)
} }
} }

View File

@@ -64,7 +64,7 @@ const FILTER_2: &str = r#"
#[case(VCF_2, FILTER_2, true)] #[case(VCF_2, FILTER_2, true)]
fn test_filter(#[case] vcf: &str, #[case] filter: &str, #[case] matches: bool) { fn test_filter(#[case] vcf: &str, #[case] filter: &str, #[case] matches: bool) {
dbg!(vcf); dbg!(vcf);
let obj = AddressObject::from_vcf(String::new(), vcf.to_owned()).unwrap(); let obj = AddressObject::from_vcf(vcf.to_owned()).unwrap();
let filter = FilterElement::parse_str(filter).unwrap(); let filter = FilterElement::parse_str(filter).unwrap();
assert_eq!(matches, filter.matches(&obj)); assert_eq!(matches, filter.matches(&obj));
} }

View File

@@ -55,7 +55,7 @@ impl ReportRequest {
} }
fn objects_response( fn objects_response(
objects: Vec<AddressObject>, objects: Vec<(String, AddressObject)>,
not_found: Vec<String>, not_found: Vec<String>,
path: &str, path: &str,
principal: &str, principal: &str,
@@ -64,11 +64,12 @@ fn objects_response(
prop: &PropfindType<AddressObjectPropWrapperName>, prop: &PropfindType<AddressObjectPropWrapperName>,
) -> Result<MultistatusElement<AddressObjectPropWrapper, String>, Error> { ) -> Result<MultistatusElement<AddressObjectPropWrapper, 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!("{}/{}.vcf", path, object.get_id()); let path = format!("{}/{}.vcf", path, &object_id);
responses.push( responses.push(
AddressObjectResource { AddressObjectResource {
object, object,
object_id,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, prop, None, puri, user)?, .propfind(&path, prop, None, puri, user)?,

View File

@@ -32,11 +32,12 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
.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!("{}/{}.vcf", path.trim_end_matches('/'), object.get_id()); let path = format!("{}/{}.vcf", path.trim_end_matches('/'), object_id);
responses.push( responses.push(
AddressObjectResource { AddressObjectResource {
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)?,

View File

@@ -78,7 +78,8 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
.get_objects(principal, addressbook_id) .get_objects(principal, addressbook_id)
.await? .await?
.into_iter() .into_iter()
.map(|object| AddressObjectResource { .map(|(object_id, object)| AddressObjectResource {
object_id,
object, object,
principal: principal.to_owned(), principal: principal.to_owned(),
}) })
@@ -91,7 +92,7 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
file: Self::Resource, file: Self::Resource,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.addr_store self.addr_store
.update_addressbook(principal.to_owned(), addressbook_id.to_owned(), file.into()) .update_addressbook(principal, addressbook_id, file.into())
.await?; .await?;
Ok(()) Ok(())
} }

View File

@@ -1,4 +1,4 @@
use ical::property::Property; use ical::property::ContentLine;
use rustical_xml::{ValueDeserialize, XmlDeserialize}; use rustical_xml::{ValueDeserialize, XmlDeserialize};
use std::borrow::Cow; use std::borrow::Cow;
@@ -128,7 +128,7 @@ impl TextMatchElement {
negate_condition.0 ^ matches negate_condition.0 ^ matches
} }
#[must_use] #[must_use]
pub fn match_property(&self, property: &Property) -> bool { pub fn match_property(&self, property: &ContentLine) -> bool {
let text = property.value.as_deref().unwrap_or(""); let text = property.value.as_deref().unwrap_or("");
self.match_text(text) self.match_text(text)
} }

View File

@@ -1,59 +1,32 @@
use crate::{CalDateTime, LOCAL_DATE};
use crate::{CalendarObject, Error}; use crate::{CalendarObject, Error};
use chrono::Datelike;
use ical::generator::Emitter; use ical::generator::Emitter;
use ical::parser::{ use ical::parser::vcard::{self, component::VcardContact};
Component,
vcard::{self, component::VcardContact},
};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::{collections::HashMap, io::BufReader}; use std::{collections::HashMap, io::BufReader};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AddressObject { pub struct AddressObject {
id: String,
vcf: String, vcf: String,
vcard: VcardContact, vcard: VcardContact,
} }
impl TryFrom<VcardContact> for AddressObject { impl From<VcardContact> for AddressObject {
type Error = Error; fn from(vcard: VcardContact) -> Self {
fn try_from(vcard: VcardContact) -> Result<Self, Self::Error> {
let uid = vcard
.get_uid()
.ok_or_else(|| Error::InvalidData("missing UID".to_owned()))?
.to_owned();
let vcf = vcard.generate(); let vcf = vcard.generate();
Ok(Self { Self { vcf, vcard }
vcf,
vcard,
id: uid,
})
} }
} }
impl AddressObject { impl AddressObject {
pub fn from_vcf(id: String, vcf: String) -> Result<Self, Error> { pub fn from_vcf(vcf: String) -> Result<Self, Error> {
let mut parser = vcard::VcardParser::new(BufReader::new(vcf.as_bytes())); let parser = vcard::VcardParser::new(BufReader::new(vcf.as_bytes()));
let vcard = parser.next().ok_or(Error::MissingContact)??; let vcard = parser.expect_one()?;
if parser.next().is_some() { Ok(Self { vcf, vcard })
return Err(Error::InvalidData(
"multiple vcards, only one allowed".to_owned(),
));
}
Ok(Self { id, vcf, vcard })
}
#[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_id());
hasher.update(self.get_vcf()); hasher.update(self.get_vcf());
format!("\"{:x}\"", hasher.finalize()) format!("\"{:x}\"", hasher.finalize())
} }
@@ -63,109 +36,12 @@ impl AddressObject {
&self.vcf &self.vcf
} }
#[must_use]
pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
CalDateTime::parse_vcard(prop).ok()
}
#[must_use]
pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
CalDateTime::parse_vcard(prop).ok()
}
#[must_use]
pub fn get_full_name(&self) -> Option<&str> {
let prop = self.vcard.get_property("FN")?;
prop.value.as_deref()
}
pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> { pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok( todo!();
if let Some((anniversary, contains_year)) = self.get_anniversary() {
let Some(fullname) = self.get_full_name() else {
return Ok(None);
};
let anniversary = anniversary.date();
let year = contains_year.then_some(anniversary.year());
let anniversary_start = anniversary.format(LOCAL_DATE);
let anniversary_end = anniversary
.succ_opt()
.unwrap_or(anniversary)
.format(LOCAL_DATE);
let uid = format!("{}-anniversary", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(
format!(
r"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
BEGIN:VEVENT
DTSTART;VALUE=DATE:{anniversary_start}
DTEND;VALUE=DATE:{anniversary_end}
UID:{uid}
RRULE:FREQ=YEARLY
SUMMARY:💍 {fullname}{year_suffix}
TRANSP:TRANSPARENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY
DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR",
),
None,
)?)
} else {
None
},
)
} }
pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> { pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok( todo!();
if let Some((birthday, contains_year)) = self.get_birthday() {
let Some(fullname) = self.get_full_name() else {
return Ok(None);
};
let birthday = birthday.date();
let year = contains_year.then_some(birthday.year());
let birthday_start = birthday.format(LOCAL_DATE);
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
let uid = format!("{}-birthday", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(
format!(
r"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
BEGIN:VEVENT
DTSTART;VALUE=DATE:{birthday_start}
DTEND;VALUE=DATE:{birthday_end}
UID:{uid}
RRULE:FREQ=YEARLY
SUMMARY:🎂 {fullname}{year_suffix}
TRANSP:TRANSPARENT
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT0M
ACTION:DISPLAY
DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR",
),
None,
)?)
} else {
None
},
)
} }
/// Get significant dates associated with this address object /// Get significant dates associated with this address object

View File

@@ -0,0 +1,120 @@
use crate::Error;
use derive_more::Display;
use ical::component::CalendarInnerData;
use ical::component::IcalCalendarObject;
use ical::generator::Emitter;
use ical::parser::ComponentParser;
use serde::Deserialize;
use serde::Serialize;
use sha2::{Digest, Sha256};
#[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 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 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 struct CalendarObject {
inner: IcalCalendarObject,
ics: String,
}
impl CalendarObject {
pub fn from_ics(ics: String) -> Result<Self, Error> {
let parser: ComponentParser<_, IcalCalendarObject> = ComponentParser::new(ics.as_bytes());
let inner = parser.expect_one()?;
Ok(Self { inner, ics })
}
#[must_use]
pub const fn get_inner(&self) -> &IcalCalendarObject {
&self.inner
}
#[must_use]
pub fn get_uid(&self) -> &str {
self.inner.get_uid()
}
#[must_use]
pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.get_uid());
hasher.update(self.get_ics());
format!("\"{:x}\"", hasher.finalize())
}
#[must_use]
pub fn get_ics(&self) -> &str {
&self.ics
}
#[must_use]
pub fn get_object_type(&self) -> CalendarObjectType {
(&self.inner).into()
}
}
impl From<CalendarObject> for IcalCalendarObject {
fn from(value: CalendarObject) -> Self {
value.inner
}
}
impl From<IcalCalendarObject> for CalendarObject {
fn from(value: IcalCalendarObject) -> Self {
Self {
ics: value.generate(),
inner: value,
}
}
}

View File

@@ -1,7 +1,5 @@
use axum::{http::StatusCode, response::IntoResponse}; use axum::{http::StatusCode, response::IntoResponse};
use crate::CalDateTimeError;
#[derive(Debug, thiserror::Error, PartialEq, Eq)] #[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error { pub enum Error {
#[error("Invalid ics/vcf input: {0}")] #[error("Invalid ics/vcf input: {0}")]
@@ -15,12 +13,6 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
ParserError(#[from] ical::parser::ParserError), ParserError(#[from] ical::parser::ParserError),
#[error(transparent)]
CalDateTimeError(#[from] CalDateTimeError),
#[error(transparent)]
RRuleError(#[from] rrule::RRuleError),
} }
impl Error { impl Error {
@@ -30,7 +22,7 @@ impl Error {
Self::InvalidData(_) | Self::MissingCalendar | Self::MissingContact => { Self::InvalidData(_) | Self::MissingCalendar | Self::MissingContact => {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
} }
_ => StatusCode::INTERNAL_SERVER_ERROR, Self::ParserError(_) => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }
} }

View File

@@ -1,386 +0,0 @@
use crate::CalDateTime;
use crate::Error;
use chrono::{DateTime, Duration, Utc};
use ical::parser::ComponentMut;
use ical::{generator::IcalEvent, parser::Component, property::Property};
use rrule::{RRule, RRuleSet};
use std::{collections::HashMap, str::FromStr};
#[derive(Debug, Clone, Default)]
pub struct EventObject {
pub event: IcalEvent,
// If a timezone is None that means that in the VCALENDAR object there's a timezone defined
// with that name but its not from the Olson DB
pub timezones: HashMap<String, Option<chrono_tz::Tz>>,
}
impl EventObject {
#[must_use]
pub fn get_uid(&self) -> &str {
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> {
if self.event.get_rrule().is_some() {
// TODO: understand recurrence rules
return Ok(None);
}
if let Some(dtend) = self.get_dtend()? {
return Ok(Some(dtend));
}
let duration = self.event.get_duration().unwrap_or(Duration::days(1));
let first_occurence = self.get_dtstart()?;
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);
// TODO: Make nice, this is just a bodge to get correct behaviour
let mut empty = true;
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();
empty = false;
rrule_set.rrule(rrule)
}
"RDATE" => {
let rdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
empty = false;
rrule_set.rdate(rdate)
}
"EXDATE" => {
let exdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
empty = false;
rrule_set.exdate(exdate)
}
_ => rrule_set,
}
}
if empty {
return Ok(None);
}
Ok(Some(rrule_set))
}
// The returned calendar components MUST NOT use recurrence
// properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT
// have reference to or include VTIMEZONE components. Date and local
// time with reference to time zone information MUST be converted
// into date with UTC time.
pub fn expand_recurrence(
&self,
start: Option<DateTime<Utc>>,
end: Option<DateTime<Utc>>,
overrides: &[Self],
) -> Result<Vec<IcalEvent>, Error> {
let mut events = vec![];
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());
let Some(mut rrule_set) = self.recurrence_ruleset()? else {
// If ruleset empty simply return main event AND all overrides
return Ok(std::iter::once(self.clone())
.chain(overrides.iter().cloned())
.map(|event| event.event)
.collect());
};
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 dates = rrule_set.all(2048).dates;
'recurrence: for date in dates {
let date = CalDateTime::from(date.to_utc());
let recurrence_id = 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 == &recurrence_id
{
// 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(recurrence_id.clone()),
params: vec![],
});
ev.set_property(Property {
name: "DTSTART".to_string(),
value: Some(recurrence_id),
params: vec![],
});
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)
}
}
#[cfg(test)]
mod tests {
use crate::{CalDateTime, CalendarObject};
use chrono::{DateTime, Utc};
use ical::generator::Emitter;
use rstest::rstest;
const ICS_1: &str = r"BEGIN:VCALENDAR
CALSCALE:GREGORIAN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
END:VTIMEZONE
BEGIN:VEVENT
UID:318ec6503573d9576818daf93dac07317058d95c
DTSTAMP:20250502T132758Z
DTSTART;TZID=Europe/Berlin:20250506T090000
DTEND;TZID=Europe/Berlin:20250506T092500
SEQUENCE:2
SUMMARY:weekly stuff
TRANSP:OPAQUE
RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU
END:VEVENT
END:VCALENDAR";
const EXPANDED_1: &[&str] = &[
"BEGIN:VEVENT\r
UID:318ec6503573d9576818daf93dac07317058d95c\r
DTSTAMP:20250502T132758Z\r
SEQUENCE:2\r
SUMMARY:weekly stuff\r
TRANSP:OPAQUE\r
RECURRENCE-ID:20250506T070000Z\r
DTSTART:20250506T070000Z\r
DTEND:20250506T072500Z\r
END:VEVENT\r\n",
"BEGIN:VEVENT\r
UID:318ec6503573d9576818daf93dac07317058d95c\r
DTSTAMP:20250502T132758Z\r
SEQUENCE:2\r
SUMMARY:weekly stuff\r
TRANSP:OPAQUE\r
RECURRENCE-ID:20250508T070000Z\r
DTSTART:20250508T070000Z\r
DTEND:20250508T072500Z\r
END:VEVENT\r\n",
"BEGIN:VEVENT\r
UID:318ec6503573d9576818daf93dac07317058d95c\r
DTSTAMP:20250502T132758Z\r
SEQUENCE:2\r
SUMMARY:weekly stuff\r
TRANSP:OPAQUE\r
RECURRENCE-ID:20250511T090000\r
DTSTART:20250511T070000Z\r
DTEND:20250511T072500Z\r
END:VEVENT\r\n",
"BEGIN:VEVENT\r
UID:318ec6503573d9576818daf93dac07317058d95c\r
DTSTAMP:20250502T132758Z\r
SEQUENCE:2\r
SUMMARY:weekly stuff\r
TRANSP:OPAQUE\r
RECURRENCE-ID:20250520T090000\r
DTSTA:20250520T070000Z\r
DTEND:20250520T072500Z\r
END:VEVENT\r\n",
];
const ICS_2: &str = r"BEGIN:VCALENDAR
CALSCALE:GREGORIAN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:US/Eastern
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:abcd2
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:abcd2
END:VEVENT
END:VCALENDAR
";
const EXPANDED_2: &[&str] = &[
"BEGIN:VEVENT\r
DTSTAMP:20060206T001121Z\r
DURATION:PT1H\r
SUMMARY:Event #2\r
UID:abcd2\r
RECURRENCE-ID:20060103T170000\r
DTSTART:20060103T170000\r
END:VEVENT\r\n",
"BEGIN:VEVENT\r
DTSTAMP:20060206T001121Z\r
DURATION:PT1H\r
SUMMARY:Event #2 bis\r
UID:abcd2\r
RECURRENCE-ID:20060104T170000\r
DTSTART:20060104T190000\r
END:VEVENT\r
END:VCALENDAR\r\n",
];
const ICS_3: &str = r"BEGIN:VCALENDAR
CALSCALE:GREGORIAN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:US/Eastern
END:VTIMEZONE
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:abcd3
END:VEVENT
END:VCALENDAR
";
const EXPANDED_3: &[&str] = &["BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART:20060104T150000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:abcd3
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT"];
// The implementation never was entirely correct but will be fixed in v0.12
// #[rstest]
// #[case(ICS_1, EXPANDED_1, None, None)]
// // from https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.3
// #[case(ICS_2, EXPANDED_2,
// Some(CalDateTime::parse("20060103T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc()),
// Some(CalDateTime::parse("20060105T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc())
// )]
// #[case(ICS_3, EXPANDED_3,
// Some(CalDateTime::parse("20060103T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc()),
// Some(CalDateTime::parse("20060105T000000Z", Some(chrono_tz::US::Eastern)).unwrap().utc())
// )]
// fn test_expand_recurrence(
// #[case] ics: &'static str,
// #[case] expanded: &[&str],
// #[case] from: Option<DateTime<Utc>>,
// #[case] to: Option<DateTime<Utc>>,
// ) {
// let event = CalendarObject::from_ics(ics.to_string(), None).unwrap();
// let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else {
// panic!()
// };
//
// let events: Vec<String> = event
// .expand_recurrence(from, to, overrides)
// .unwrap()
// .into_iter()
// .map(|event| Emitter::generate(&event))
// .collect();
// assert_eq!(events.len(), expanded.len());
// for (output, reference) in events.iter().zip(expanded) {
// similar_asserts::assert_eq!(output, reference);
// }
// }
}

View File

@@ -1,5 +0,0 @@
mod event;
mod object;
pub use event::*;
pub use object::*;

View File

@@ -1,366 +0,0 @@
use super::EventObject;
use crate::CalDateTime;
use crate::Error;
use chrono::DateTime;
use chrono::Utc;
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 serde::Deserialize;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::{collections::HashMap, 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> {
// A calendar object does not necessarily have to contain a main VOBJECT
if events.is_empty() {
return Err(Error::MissingCalendar);
}
#[allow(clippy::option_if_let_else)]
let main_event = if let Some(main) = events
.extract_if(.., |event| event.event.get_recurrence_id().is_none())
.next()
{
main
} else {
events.remove(0)
};
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> {
// A calendar object does not necessarily have to contain a main VOBJECT
if todos.is_empty() {
return Err(Error::MissingCalendar);
}
#[allow(clippy::option_if_let_else)]
let main_todo = if let Some(main) = todos
.extract_if(.., |todo| todo.get_recurrence_id().is_none())
.next()
{
main
} else {
todos.remove(0)
};
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> {
// A calendar object does not necessarily have to contain a main VOBJECT
if journals.is_empty() {
return Err(Error::MissingCalendar);
}
#[allow(clippy::option_if_let_else)]
let main_journal = if let Some(main) = journals
.extract_if(.., |journal| journal.get_recurrence_id().is_none())
.next()
{
main
} else {
journals.remove(0)
};
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)]
pub struct CalendarObject {
data: CalendarObjectComponent,
properties: Vec<Property>,
id: String,
ics: String,
vtimezones: HashMap<String, IcalTimeZone>,
}
impl CalendarObject {
pub fn from_ics(ics: String, id: Option<String>) -> Result<Self, Error> {
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
let cal = parser.next().ok_or(Error::MissingCalendar)??;
if parser.next().is_some() {
return Err(Error::InvalidData(
"multiple calendars, only one allowed".to_owned(),
));
}
if u8::from(!cal.events.is_empty())
+ 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]
pub const fn get_vtimezones(&self) -> &HashMap<String, IcalTimeZone> {
&self.vtimezones
}
#[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]
pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.get_uid());
hasher.update(self.get_ics());
format!("\"{:x}\"", hasher.finalize())
}
#[must_use]
pub fn get_ics(&self) -> &str {
&self.ics
}
#[must_use]
pub fn get_component_name(&self) -> &str {
self.get_object_type().as_str()
}
#[must_use]
pub fn get_object_type(&self) -> CalendarObjectType {
(&self.data).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]
pub fn get_property(&self, name: &str) -> Option<&Property> {
self.properties
.iter()
.find(|property| property.name == name)
}
#[must_use]
pub fn get_named_properties(&self, name: &str) -> Vec<&Property> {
self.properties
.iter()
.filter(|property| property.name == name)
.collect()
}
}

View File

@@ -1,12 +1,10 @@
#![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 calendar_object;
pub use icalendar::*; pub use calendar_object::*;
mod error; mod error;
pub use error::Error; pub use error::Error;

View File

@@ -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,375 +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"),
),
}
}
#[must_use]
pub fn with_timezone(&self, tz: &ICalTimezone) -> Self {
match self {
Self::DateTime(datetime) => Self::DateTime(datetime.with_timezone(tz)),
Self::Date(date, _) => Self::Date(date.to_owned(), tz.to_owned()),
}
}
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
)
);
}
}

View File

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

View File

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

View File

@@ -15,8 +15,8 @@ pub trait AddressbookStore: Send + Sync + 'static {
async fn update_addressbook( async fn update_addressbook(
&self, &self,
principal: String, principal: &str,
id: String, id: &str,
addressbook: Addressbook, addressbook: Addressbook,
) -> Result<(), Error>; ) -> Result<(), Error>;
async fn insert_addressbook(&self, addressbook: Addressbook) -> Result<(), Error>; async fn insert_addressbook(&self, addressbook: Addressbook) -> Result<(), Error>;
@@ -33,7 +33,7 @@ pub trait AddressbookStore: Send + Sync + 'static {
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
synctoken: i64, synctoken: i64,
) -> Result<(Vec<AddressObject>, Vec<String>, i64), Error>; ) -> Result<(Vec<(String, AddressObject)>, Vec<String>, i64), Error>;
async fn addressbook_metadata( async fn addressbook_metadata(
&self, &self,
@@ -45,7 +45,7 @@ pub trait AddressbookStore: Send + Sync + 'static {
&self, &self,
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
) -> Result<Vec<AddressObject>, Error>; ) -> Result<Vec<(String, AddressObject)>, Error>;
async fn get_object( async fn get_object(
&self, &self,
principal: &str, principal: &str,
@@ -55,8 +55,9 @@ pub trait AddressbookStore: Send + Sync + 'static {
) -> Result<AddressObject, Error>; ) -> Result<AddressObject, Error>;
async fn put_object( async fn put_object(
&self, &self,
principal: String, principal: &str,
addressbook_id: String, addressbook_id: &str,
object_id: &str,
object: AddressObject, object: AddressObject,
overwrite: bool, overwrite: bool,
) -> Result<(), Error>; ) -> Result<(), Error>;
@@ -77,7 +78,7 @@ pub trait AddressbookStore: Send + Sync + 'static {
async fn import_addressbook( async fn import_addressbook(
&self, &self,
addressbook: Addressbook, addressbook: Addressbook,
objects: Vec<AddressObject>, objects: Vec<(String, AddressObject)>,
merge_existing: bool, merge_existing: bool,
) -> Result<(), Error>; ) -> Result<(), Error>;
} }

View File

@@ -22,8 +22,8 @@ pub trait CalendarStore: Send + Sync + 'static {
async fn update_calendar( async fn update_calendar(
&self, &self,
principal: String, principal: &str,
id: String, id: &str,
calendar: Calendar, calendar: Calendar,
) -> Result<(), Error>; ) -> Result<(), Error>;
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error>; async fn insert_calendar(&self, calendar: Calendar) -> 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,
@@ -79,19 +79,25 @@ pub trait CalendarStore: Send + Sync + 'static {
) -> Result<CalendarObject, Error>; ) -> Result<CalendarObject, Error>;
async fn put_objects( async fn put_objects(
&self, &self,
principal: String, principal: &str,
cal_id: String, cal_id: &str,
objects: Vec<CalendarObject>, objects: Vec<(String, CalendarObject)>,
overwrite: bool, overwrite: bool,
) -> Result<(), Error>; ) -> Result<(), Error>;
async fn put_object( async fn put_object(
&self, &self,
principal: String, principal: &str,
cal_id: String, cal_id: &str,
object_id: &str,
object: CalendarObject, object: CalendarObject,
overwrite: bool, overwrite: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.put_objects(principal, cal_id, vec![object], overwrite) self.put_objects(
principal,
cal_id,
vec![(object_id.to_owned(), object)],
overwrite,
)
.await .await
} }
async fn delete_object( async fn delete_object(

View File

@@ -1,5 +1,6 @@
use crate::CalendarStore; use crate::{Calendar, CalendarStore, calendar_store::CalendarQuery};
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 {
@@ -51,11 +52,11 @@ impl CalendarStore for CombinedCalendarStore {
async fn update_calendar( async fn update_calendar(
&self, &self,
principal: String, principal: &str,
id: String, id: &str,
calendar: crate::Calendar, calendar: Calendar,
) -> Result<(), crate::Error> { ) -> Result<(), crate::Error> {
self.store_for_id(&id) self.store_for_id(id)
.update_calendar(principal, id, calendar) .update_calendar(principal, id, calendar)
.await .await
} }
@@ -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<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)
@@ -109,8 +110,8 @@ impl CalendarStore for CombinedCalendarStore {
&self, &self,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
query: crate::calendar_store::CalendarQuery, query: 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,7 +142,7 @@ 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
@@ -149,12 +150,12 @@ impl CalendarStore for CombinedCalendarStore {
async fn put_objects( async fn put_objects(
&self, &self,
principal: String, principal: &str,
cal_id: String, cal_id: &str,
objects: Vec<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_objects(principal, cal_id, objects, overwrite) .put_objects(principal, cal_id, objects, overwrite)
.await .await
} }

View File

@@ -20,6 +20,7 @@ rstest.workspace = true
criterion.workspace = true criterion.workspace = true
[dependencies] [dependencies]
ical.workspace = true
tokio.workspace = true tokio.workspace = true
rustical_store.workspace = true rustical_store.workspace = true
async-trait.workspace = true async-trait.workspace = true
@@ -36,4 +37,3 @@ 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
regex.workspace = true

View File

@@ -34,16 +34,19 @@ fn benchmark(c: &mut Criterion) {
cal_store cal_store
}); });
let object = CalendarObject::from_ics(include_str!("ical_event.ics").to_owned(), None).unwrap(); let row = (
"asd".to_owned(),
CalendarObject::from_ics(include_str!("ical_event.ics").to_owned()).unwrap(),
);
let batch_size = 1000; let batch_size = 1000;
let objects: Vec<_> = std::iter::repeat_n(object.clone(), batch_size).collect(); let objects: Vec<_> = std::iter::repeat_n(row.clone(), batch_size).collect();
c.bench_function("put_batch", |b| { c.bench_function("put_batch", |b| {
b.to_async(&runtime).iter(async || { b.to_async(&runtime).iter(async || {
// yeet // yeet
cal_store cal_store
.put_objects("user".to_owned(), "okwow".to_owned(), objects.clone(), true) .put_objects("user", "okwow", objects.clone(), true)
.await .await
.unwrap(); .unwrap();
}); });
@@ -54,7 +57,7 @@ fn benchmark(c: &mut Criterion) {
// yeet // yeet
for _ in 0..1000 { for _ in 0..1000 {
cal_store cal_store
.put_object("user".to_owned(), "okwow".to_owned(), object.clone(), true) .put_object("user", "okwow", &row.0, row.1.clone(), true)
.await .await
.unwrap(); .unwrap();
} }

View File

@@ -1,14 +1,13 @@
use crate::addressbook_store::SqliteAddressbookStore; use crate::addressbook_store::SqliteAddressbookStore;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType}; use rustical_ical::{CalendarObject, CalendarObjectType};
use rustical_store::{ use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarMetadata, CalendarStore, CollectionMetadata, Addressbook, AddressbookStore, Calendar, CalendarMetadata, CalendarStore, CollectionMetadata,
Error, PrefixedCalendarStore, Error, PrefixedCalendarStore,
}; };
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_";
@@ -269,10 +268,11 @@ impl CalendarStore for SqliteAddressbookStore {
#[instrument] #[instrument]
async fn update_calendar( async fn update_calendar(
&self, &self,
principal: String, principal: &str,
id: String, id: &str,
mut calendar: Calendar, mut calendar: Calendar,
) -> Result<(), Error> { ) -> Result<(), Error> {
assert_eq!(principal, calendar.principal);
assert_eq!(id, calendar.id); assert_eq!(id, calendar.id);
calendar.id = calendar calendar.id = calendar
.id .id
@@ -324,19 +324,20 @@ 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 todo!();
.iter() // let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
.map(AddressObject::get_birthday_object) // .iter()
.collect(); // .map(AddressObject::get_birthday_object)
let objects = objects?.into_iter().flatten().collect(); // .collect();
// let objects = objects?.into_iter().flatten().collect();
Ok((objects, deleted_objects, new_synctoken)) //
// Ok((objects, deleted_objects, new_synctoken))
} }
#[instrument] #[instrument]
@@ -356,22 +357,23 @@ 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 todo!()
.strip_prefix(BIRTHDAYS_PREFIX) // let cal_id = cal_id
.ok_or(Error::NotFound)?; // .strip_prefix(BIRTHDAYS_PREFIX)
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> = // .ok_or(Error::NotFound)?;
AddressbookStore::get_objects(self, principal, cal_id) // let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
.await? // AddressbookStore::get_objects(self, principal, cal_id)
.iter() // .await?
.map(AddressObject::get_significant_dates) // .iter()
.collect(); // .map(AddressObject::get_significant_dates)
let objects = objects? // .collect();
.into_iter() // let objects = objects?
.flat_map(HashMap::into_values) // .into_iter()
.collect(); // .flat_map(HashMap::into_values)
// .collect();
Ok(objects) //
// Ok(objects)
} }
#[instrument] #[instrument]
@@ -396,9 +398,9 @@ impl CalendarStore for SqliteAddressbookStore {
#[instrument] #[instrument]
async fn put_objects( async fn put_objects(
&self, &self,
_principal: String, _principal: &str,
_cal_id: String, _cal_id: &str,
_objects: Vec<CalendarObject>, _objects: Vec<(String, CalendarObject)>,
_overwrite: bool, _overwrite: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
Err(Error::ReadOnly) Err(Error::ReadOnly)

View File

@@ -19,11 +19,11 @@ struct AddressObjectRow {
vcf: String, vcf: String,
} }
impl TryFrom<AddressObjectRow> for AddressObject { impl TryFrom<AddressObjectRow> for (String, AddressObject) {
type Error = rustical_store::Error; type Error = rustical_store::Error;
fn try_from(value: AddressObjectRow) -> Result<Self, Self::Error> { fn try_from(value: AddressObjectRow) -> Result<Self, Self::Error> {
Ok(Self::from_vcf(value.id, value.vcf)?) Ok((value.id, AddressObject::from_vcf(value.vcf)?))
} }
} }
@@ -290,7 +290,7 @@ impl SqliteAddressbookStore {
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
synctoken: i64, synctoken: i64,
) -> Result<(Vec<AddressObject>, Vec<String>, i64), rustical_store::Error> { ) -> Result<(Vec<(String, AddressObject)>, Vec<String>, i64), rustical_store::Error> {
struct Row { struct Row {
object_id: String, object_id: String,
synctoken: i64, synctoken: i64,
@@ -318,7 +318,7 @@ impl SqliteAddressbookStore {
for Row { object_id, .. } in changes { for Row { object_id, .. } in changes {
match Self::_get_object(&mut *conn, principal, addressbook_id, &object_id, false).await match Self::_get_object(&mut *conn, principal, addressbook_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),
} }
@@ -353,7 +353,7 @@ impl SqliteAddressbookStore {
executor: E, executor: E,
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
) -> Result<Vec<AddressObject>, rustical_store::Error> { ) -> Result<Vec<(String, AddressObject)>, rustical_store::Error> {
sqlx::query_as!( sqlx::query_as!(
AddressObjectRow, AddressObjectRow,
"SELECT id, vcf FROM addressobjects WHERE principal = ? AND addressbook_id = ? AND deleted_at IS NULL", "SELECT id, vcf FROM addressobjects WHERE principal = ? AND addressbook_id = ? AND deleted_at IS NULL",
@@ -374,7 +374,7 @@ impl SqliteAddressbookStore {
object_id: &str, object_id: &str,
show_deleted: bool, show_deleted: bool,
) -> Result<AddressObject, rustical_store::Error> { ) -> Result<AddressObject, rustical_store::Error> {
sqlx::query_as!( let (id, object) = sqlx::query_as!(
AddressObjectRow, AddressObjectRow,
"SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)", "SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
principal, principal,
@@ -385,17 +385,20 @@ impl SqliteAddressbookStore {
.fetch_one(executor) .fetch_one(executor)
.await .await
.map_err(crate::Error::from)? .map_err(crate::Error::from)?
.try_into() .try_into()?;
assert_eq!(id, object_id);
Ok(object)
} }
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>( async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
executor: E, executor: E,
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
object_id: &str,
object: &AddressObject, object: &AddressObject,
overwrite: bool, overwrite: bool,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
let (object_id, vcf) = (object.get_id(), object.get_vcf()); let vcf = object.get_vcf();
(if overwrite { (if overwrite {
sqlx::query!( sqlx::query!(
@@ -500,10 +503,12 @@ impl AddressbookStore for SqliteAddressbookStore {
#[instrument] #[instrument]
async fn update_addressbook( async fn update_addressbook(
&self, &self,
principal: String, principal: &str,
id: String, id: &str,
addressbook: Addressbook, addressbook: Addressbook,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
assert_eq!(principal, &addressbook.principal);
assert_eq!(id, &addressbook.id);
Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await
} }
@@ -569,7 +574,7 @@ impl AddressbookStore for SqliteAddressbookStore {
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
synctoken: i64, synctoken: i64,
) -> Result<(Vec<AddressObject>, Vec<String>, i64), rustical_store::Error> { ) -> Result<(Vec<(String, AddressObject)>, Vec<String>, i64), rustical_store::Error> {
Self::_sync_changes(&self.db, principal, addressbook_id, synctoken).await Self::_sync_changes(&self.db, principal, addressbook_id, synctoken).await
} }
@@ -601,7 +606,7 @@ impl AddressbookStore for SqliteAddressbookStore {
&self, &self,
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
) -> Result<Vec<AddressObject>, rustical_store::Error> { ) -> Result<Vec<(String, AddressObject)>, rustical_store::Error> {
Self::_get_objects(&self.db, principal, addressbook_id).await Self::_get_objects(&self.db, principal, addressbook_id).await
} }
@@ -619,8 +624,9 @@ impl AddressbookStore for SqliteAddressbookStore {
#[instrument] #[instrument]
async fn put_object( async fn put_object(
&self, &self,
principal: String, principal: &str,
addressbook_id: String, addressbook_id: &str,
object_id: &str,
object: AddressObject, object: AddressObject,
overwrite: bool, overwrite: bool,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
@@ -630,9 +636,15 @@ impl AddressbookStore for SqliteAddressbookStore {
.await .await
.map_err(crate::Error::from)?; .map_err(crate::Error::from)?;
let object_id = object.get_id().to_owned(); Self::_put_object(
&mut *tx,
Self::_put_object(&mut *tx, &principal, &addressbook_id, &object, overwrite).await?; principal,
addressbook_id,
object_id,
&object,
overwrite,
)
.await?;
let sync_token = Self::log_object_operation( let sync_token = Self::log_object_operation(
&mut tx, &mut tx,
@@ -648,7 +660,7 @@ impl AddressbookStore for SqliteAddressbookStore {
self.send_push_notification( self.send_push_notification(
CollectionOperationInfo::Content { sync_token }, CollectionOperationInfo::Content { sync_token },
self.get_addressbook(&principal, &addressbook_id, false) self.get_addressbook(principal, addressbook_id, false)
.await? .await?
.push_topic, .push_topic,
); );
@@ -733,7 +745,7 @@ impl AddressbookStore for SqliteAddressbookStore {
async fn import_addressbook( async fn import_addressbook(
&self, &self,
addressbook: Addressbook, addressbook: Addressbook,
objects: Vec<AddressObject>, objects: Vec<(String, AddressObject)>,
merge_existing: bool, merge_existing: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut tx = self let mut tx = self
@@ -758,11 +770,12 @@ impl AddressbookStore for SqliteAddressbookStore {
} }
let mut sync_token = None; let mut sync_token = None;
for object in objects { for (object_id, object) in objects {
Self::_put_object( Self::_put_object(
&mut *tx, &mut *tx,
&addressbook.principal, &addressbook.principal,
&addressbook.id, &addressbook.id,
&object_id,
&object, &object,
false, false,
) )
@@ -773,7 +786,7 @@ impl AddressbookStore for SqliteAddressbookStore {
&mut tx, &mut tx,
&addressbook.principal, &addressbook.principal,
&addressbook.id, &addressbook.id,
object.get_id(), &object_id,
ChangeOperation::Add, ChangeOperation::Add,
) )
.await?, .await?,

View File

@@ -3,8 +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 regex::Regex; use ical::types::CalDateTime;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType}; 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};
@@ -21,11 +21,11 @@ struct CalendarObjectRow {
uid: String, uid: String,
} }
impl TryFrom<CalendarObjectRow> for CalendarObject { impl TryFrom<CalendarObjectRow> for (String, 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 = CalendarObject::from_ics(value.ics)?;
if object.get_uid() != value.uid { if object.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!(
@@ -35,7 +35,7 @@ impl TryFrom<CalendarObjectRow> for CalendarObject {
)), )),
)); ));
} }
Ok(object) Ok((value.id, object))
} }
} }
@@ -146,83 +146,6 @@ impl SqliteCalendarStore {
} }
} }
/// In the past exports generated objects with invalid VERSION:4.0
/// This repair sets them to VERSION:2.0
#[allow(clippy::missing_panics_doc)]
pub async fn repair_invalid_version_4_0(&self) -> Result<(), Error> {
struct Row {
principal: String,
cal_id: String,
id: String,
ics: String,
}
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
#[allow(clippy::missing_panics_doc)]
let version_pattern = Regex::new(r"(?mi)^VERSION:4.0").unwrap();
let repairs: Vec<Row> = sqlx::query_as!(
Row,
r#"SELECT principal, cal_id, id, ics FROM calendarobjects WHERE ics LIKE '%VERSION:4.0%';"#
)
.fetch_all(&mut *tx)
.await
.map_err(crate::Error::from)?
.into_iter()
.filter_map(|mut row| {
version_pattern.find(&row.ics)?;
let new_ics = version_pattern.replace(&row.ics, "VERSION:2.0");
// Safeguard that we really only changed the version
assert_eq!(row.ics.len(), new_ics.len());
row.ics = new_ics.to_string();
Some(row)
})
.collect();
if repairs.is_empty() {
return Ok(());
}
warn!(
"Found {} calendar objects with invalid VERSION:4.0. Repairing by setting to VERSION:2.0",
repairs.len()
);
for repair in &repairs {
// calendarobjectchangelog is used by sync-collection to fetch changes
// By deleting entries we will later regenerate new entries such that clients will notice
// the objects have changed
warn!(
"Repairing VERSION for {}/{}/{}.ics",
repair.principal, repair.cal_id, repair.id
);
sqlx::query!(
"DELETE FROM calendarobjectchangelog WHERE (principal, cal_id, object_id) = (?, ?, ?);",
repair.principal, repair.cal_id, repair.id
).execute(&mut *tx).await
.map_err(crate::Error::from)?;
sqlx::query!(
"UPDATE calendarobjects SET ics = ? WHERE (principal, cal_id, id) = (?, ?, ?);",
repair.ics,
repair.principal,
repair.cal_id,
repair.id
)
.execute(&mut *tx)
.await
.map_err(crate::Error::from)?;
}
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
// Commit "orphaned" objects to the changelog table // Commit "orphaned" objects to the changelog table
pub async fn repair_orphans(&self) -> Result<(), Error> { pub async fn repair_orphans(&self) -> Result<(), Error> {
struct Row { struct Row {
@@ -357,8 +280,8 @@ impl SqliteCalendarStore {
async fn _update_calendar<'e, E: Executor<'e, Database = Sqlite>>( async fn _update_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E, executor: E,
principal: String, principal: &str,
id: String, id: &str,
calendar: Calendar, calendar: Calendar,
) -> Result<(), Error> { ) -> Result<(), Error> {
let comp_event = calendar.components.contains(&CalendarObjectType::Event); let comp_event = calendar.components.contains(&CalendarObjectType::Event);
@@ -456,7 +379,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",
@@ -475,7 +398,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
@@ -511,7 +434,7 @@ impl SqliteCalendarStore {
object_id: &str, object_id: &str,
show_deleted: bool, show_deleted: bool,
) -> Result<CalendarObject, Error> { ) -> Result<CalendarObject, Error> {
sqlx::query_as!( let (row_id, object) = sqlx::query_as!(
CalendarObjectRow, CalendarObjectRow,
"SELECT id, uid, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)", "SELECT id, uid, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
principal, principal,
@@ -522,7 +445,9 @@ impl SqliteCalendarStore {
.fetch_one(executor) .fetch_one(executor)
.await .await
.map_err(crate::Error::from)? .map_err(crate::Error::from)?
.try_into() .try_into()?;
assert_eq!(object_id, row_id);
Ok(object)
} }
#[instrument] #[instrument]
@@ -530,23 +455,24 @@ 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_uid(), object.get_ics());
let first_occurence = object let first_occurence = object
.get_inner()
.get_inner()
.get_first_occurence() .get_first_occurence()
.ok()
.flatten()
.as_ref() .as_ref()
.map(CalDateTime::date); .map(CalDateTime::date_floor);
let last_occurence = object let last_occurence = object
.get_inner()
.get_inner()
.get_last_occurence() .get_last_occurence()
.ok()
.flatten()
.as_ref() .as_ref()
.map(CalDateTime::date); .map(CalDateTime::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;
@@ -638,7 +564,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,
@@ -665,7 +591,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),
} }
@@ -705,8 +631,8 @@ impl CalendarStore for SqliteCalendarStore {
#[instrument] #[instrument]
async fn update_calendar( async fn update_calendar(
&self, &self,
principal: String, principal: &str,
id: String, id: &str,
calendar: Calendar, calendar: Calendar,
) -> Result<(), Error> { ) -> Result<(), Error> {
Self::_update_calendar(&self.db, principal, id, calendar).await Self::_update_calendar(&self.db, principal, id, calendar).await
@@ -774,14 +700,23 @@ impl CalendarStore for SqliteCalendarStore {
let mut sync_token = None; let mut sync_token = None;
for object in objects { for object in objects {
Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?; let object_id = object.get_uid();
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?,
@@ -807,7 +742,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
} }
@@ -838,7 +773,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
} }
@@ -856,9 +791,9 @@ impl CalendarStore for SqliteCalendarStore {
#[instrument] #[instrument]
async fn put_objects( async fn put_objects(
&self, &self,
principal: String, principal: &str,
cal_id: String, cal_id: &str,
objects: Vec<CalendarObject>, objects: Vec<(String, CalendarObject)>,
overwrite: bool, overwrite: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut tx = self let mut tx = self
@@ -874,18 +809,21 @@ impl CalendarStore for SqliteCalendarStore {
} }
let mut sync_token = None; let mut sync_token = None;
for object in objects { for (object_id, object) in objects {
sync_token = Some( sync_token = Some(
Self::log_object_operation( Self::log_object_operation(
&mut tx, &mut tx,
&principal, &principal,
&cal_id, &cal_id,
object.get_id(), &object_id,
ChangeOperation::Add, ChangeOperation::Add,
) )
.await?, .await?,
); );
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).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)?;
@@ -963,7 +901,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
} }

View File

@@ -17,7 +17,6 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
http: HttpConfig::default(), http: HttpConfig::default(),
data_store: DataStoreConfig::Sqlite(SqliteDataStoreConfig { data_store: DataStoreConfig::Sqlite(SqliteDataStoreConfig {
db_url: "/var/lib/rustical/db.sqlite3".to_owned(), db_url: "/var/lib/rustical/db.sqlite3".to_owned(),
run_repairs: true,
}), }),
tracing: TracingConfig::default(), tracing: TracingConfig::default(),
frontend: FrontendConfig { frontend: FrontendConfig {

View File

@@ -26,8 +26,6 @@ impl Default for HttpConfig {
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct SqliteDataStoreConfig { pub struct SqliteDataStoreConfig {
pub db_url: String, pub db_url: String,
#[serde(default = "default_true")]
pub run_repairs: bool,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]

View File

@@ -0,0 +1,14 @@
---
source: src/integration_tests/caldav/calendar.rs
assertion_line: 145
expression: body
---
BEGIN:VCALENDAR
VERSION:2.0
PRODID:RustiCal Export
CALSCALE:GREGORIAN
X-WR-CALNAME:Calendar
X-WR-CALDESC:Description
X-WR-CALCOLOR:#00FF00
X-WR-TIMEZONE:US/Eastern
END:VCALENDAR

View File

@@ -0,0 +1,36 @@
---
source: src/integration_tests/carddav/addressbook.rs
assertion_line: 446
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/carddav/principal/user/contacts/newcard.vcf</href>
<propstat>
<prop>
<getetag>&quot;ea0bf4a2ce7ef84606a4cf9235776dbc11b3e7ce351ddf35f27cbc0088acca7e&quot;</getetag>
<CARD:address-data>BEGIN:VCARD
VERSION:3.0
FN:Cyrus Daboo
N:Daboo;Cyrus
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
NICKNAME:me
NOTE:Example VCard.
ORG:Self Employed
TEL;TYPE=WORK,VOICE:412 605 0499
TEL;TYPE=FAX:412 605 0705
URL:http://www.example.com
UID:1234-5678-9000-1
END:VCARD
</CARD:address-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/home/bernard/addressbook/vcf1.vcf</href>
<status>HTTP/1.1 404 Not Found</status>
</response>
</multistatus>

View File

@@ -25,7 +25,7 @@ use std::sync::Arc;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tower::Layer; use tower::Layer;
use tower_http::normalize_path::NormalizePathLayer; use tower_http::normalize_path::NormalizePathLayer;
use tracing::{info, warn}; use tracing::info;
mod app; mod app;
mod commands; mod commands;
@@ -34,9 +34,6 @@ mod config;
pub mod integration_tests; pub mod integration_tests;
mod setup_tracing; mod setup_tracing;
mod migration_0_12;
use migration_0_12::{validate_address_objects_0_12, validate_calendar_objects_0_12};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
struct Args { struct Args {
@@ -70,22 +67,15 @@ async fn get_data_stores(
Receiver<CollectionOperation>, Receiver<CollectionOperation>,
)> { )> {
Ok(match &config { Ok(match &config {
DataStoreConfig::Sqlite(SqliteDataStoreConfig { DataStoreConfig::Sqlite(SqliteDataStoreConfig { db_url }) => {
db_url,
run_repairs,
}) => {
let db = create_db_pool(db_url, migrate).await?; let db = create_db_pool(db_url, migrate).await?;
// Channel to watch for changes (for DAV Push) // Channel to watch for changes (for DAV Push)
let (send, recv) = tokio::sync::mpsc::channel(1000); let (send, recv) = tokio::sync::mpsc::channel(1000);
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone())); let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
if *run_repairs {
info!("Running repair tasks");
addressbook_store.repair_orphans().await?; addressbook_store.repair_orphans().await?;
cal_store.repair_invalid_version_4_0().await?; let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
cal_store.repair_orphans().await?; cal_store.repair_orphans().await?;
}
let subscription_store = Arc::new(SqliteStore::new(db.clone())); let subscription_store = Arc::new(SqliteStore::new(db.clone()));
let principal_store = Arc::new(SqlitePrincipalStore::new(db)); let principal_store = Arc::new(SqlitePrincipalStore::new(db));
( (
@@ -125,14 +115,6 @@ async fn main() -> Result<()> {
let (addr_store, cal_store, subscription_store, principal_store, update_recv) = let (addr_store, cal_store, subscription_store, principal_store, update_recv) =
get_data_stores(!args.no_migrations, &config.data_store).await?; get_data_stores(!args.no_migrations, &config.data_store).await?;
warn!(
"Validating calendar data against the next-version ical parser.
In the next major release these will be rejected and cause errors.
If any errors occur, please open an issue so they can be fixed before the next major release."
);
validate_calendar_objects_0_12(principal_store.as_ref(), cal_store.as_ref()).await?;
validate_address_objects_0_12(principal_store.as_ref(), addr_store.as_ref()).await?;
let mut tasks = vec![]; let mut tasks = vec![];
if config.dav_push.enabled { if config.dav_push.enabled {

View File

@@ -1,81 +0,0 @@
use rustical_store::{AddressbookStore, CalendarStore, auth::AuthenticationProvider};
use tracing::{error, info};
pub async fn validate_calendar_objects_0_12(
principal_store: &impl AuthenticationProvider,
cal_store: &impl CalendarStore,
) -> Result<(), rustical_store::Error> {
let mut success = true;
for principal in principal_store.get_principals().await? {
for calendar in cal_store.get_calendars(&principal.id).await? {
for object in cal_store
.get_objects(&calendar.principal, &calendar.id)
.await?
{
if let Err(err) = ical_dev::parser::ical::IcalObjectParser::from_slice(
object.get_ics().as_bytes(),
)
.expect_one()
{
success = false;
error!(
"An error occured parsing a calendar object: principal={principal}, calendar={calendar}, object_id={object_id}: {err}",
principal = principal.id,
calendar = calendar.id,
object_id = object.get_id()
);
println!("{}", object.get_ics());
}
}
}
}
if success {
info!("Your calendar data seems to be valid in the next major version.");
} else {
error!(
"Not all calendar objects will be successfully parsed in the next major version (v0.12).
This will not cause issues in this version, but please comment under the tracking issue on GitHub:
https://github.com/lennart-k/rustical/issues/165"
);
}
Ok(())
}
pub async fn validate_address_objects_0_12(
principal_store: &impl AuthenticationProvider,
addr_store: &impl AddressbookStore,
) -> Result<(), rustical_store::Error> {
let mut success = true;
for principal in principal_store.get_principals().await? {
for addressbook in addr_store.get_addressbooks(&principal.id).await? {
for object in addr_store
.get_objects(&addressbook.principal, &addressbook.id)
.await?
{
if let Err(err) =
ical_dev::parser::vcard::VcardParser::from_slice(object.get_vcf().as_bytes())
.expect_one()
{
success = false;
error!(
"An error occured parsing an address object: principal={principal}, addressbook={addressbook}, object_id={object_id}: {err}",
principal = principal.id,
addressbook = addressbook.id,
object_id = object.get_id()
);
println!("{}", object.get_vcf());
}
}
}
}
if success {
info!("Your addressbook data seems to be valid in the next major version.");
} else {
error!(
"Not all address objects will be successfully parsed in the next major version (v0.12).
This will not cause issues in this version, but please comment under the tracking issue on GitHub:
https://github.com/lennart-k/rustical/issues/165"
);
}
Ok(())
}