Compare commits

..

20 Commits

Author SHA1 Message Date
Lennart
4dd12bfe52 version 0.9.6 2025-09-17 11:35:20 +02:00
Lennart
5e004a6edc calendar import: Enable import to existing calendars (if no objects are overwritten) 2025-09-17 11:33:49 +02:00
Lennart
03e550c2f8 add some debug logging for invalid data in put_event
#125
2025-09-17 10:18:46 +02:00
Lennart
b2f5d5486c version 0.9.5 2025-09-17 10:06:07 +02:00
Lennart
db674d5895 Allow setting HTTP payload limit and set default to 4MB
#124
2025-09-17 10:06:07 +02:00
Lennart K
bc98d1be42 document thing to watch out for with Kubernetes #122 2025-09-16 15:34:31 +02:00
Lennart
4bb8cae9ea docs: Fix typo for env var configuration 2025-09-14 18:55:33 +02:00
Lennart
3774b358a5 version 0.9.4 2025-09-10 23:23:12 +02:00
Lennart
c6b612e5a0 Update dependencies 2025-09-10 23:20:40 +02:00
Lennart
91586ee797 migrate quick-xml to 0.38
fixes #120
2025-09-05 15:24:34 +02:00
Lennart K
87adf94947 Update Cargo.toml and Dockerfile 2025-09-04 13:05:14 +02:00
Lennart
f850f9b3a3 version 0.9.3 2025-09-02 23:38:41 +02:00
Lennart
0eb8359e26 rewrite combined calendar store in preparation for sharing 2025-09-02 23:30:16 +02:00
Lennart
7d961ea93b frontend: make button descriptions shorter to fit mobile screen 2025-09-02 23:19:15 +02:00
Lennart
375caedec6 update docs 2025-09-02 09:32:28 +02:00
Lennart
2d8d2eb194 Update README.md 2025-09-01 00:29:55 +02:00
Lennart
69e788b363 store: prevent objects from being commited to subscription calendar 2025-08-31 12:40:20 +02:00
Lennart
8ea5321503 Merge branch 'main' into sharing 2025-08-30 13:58:50 +02:00
Lennart
76c03fa4d4 clippy appeasement 2025-08-30 11:56:58 +02:00
Lennart
a4285fb2ac Outsource some Calendar info to CalendarMetadata struct 2025-08-24 12:52:28 +02:00
45 changed files with 545 additions and 513 deletions

216
Cargo.lock generated
View File

@@ -32,12 +32,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -476,9 +470,9 @@ dependencies = [
[[package]]
name = "bitflags"
version = "2.9.3"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
dependencies = [
"serde",
]
@@ -540,10 +534,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.34"
version = "1.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc"
checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54"
dependencies = [
"find-msvc-tools",
"shlex",
]
@@ -561,17 +556,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.41"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-link 0.2.0",
]
[[package]]
@@ -595,9 +589,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.46"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
dependencies = [
"clap_builder",
"clap_derive",
@@ -605,9 +599,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.46"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
dependencies = [
"anstream",
"anstyle",
@@ -617,9 +611,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.45"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
dependencies = [
"heck",
"proc-macro2",
@@ -842,9 +836,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.0"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
dependencies = [
"powerfmt",
"serde",
@@ -1008,12 +1002,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.13"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -1090,6 +1084,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
[[package]]
name = "flume"
version = "0.11.1"
@@ -1288,7 +1288,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"wasi 0.14.3+wasi-0.2.4",
"wasi 0.14.5+wasi-0.2.4",
"wasm-bindgen",
]
@@ -1339,7 +1339,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http",
"indexmap 2.11.0",
"indexmap 2.11.1",
"slab",
"tokio",
"tokio-util",
@@ -1737,9 +1737,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.11.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
@@ -1811,9 +1811,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
version = "0.3.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1873,9 +1873,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
@@ -1896,9 +1896,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.27"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
dependencies = [
"value-bag",
]
@@ -2599,9 +2599,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.37.5"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
dependencies = [
"memchr",
]
@@ -3017,7 +3017,7 @@ dependencies = [
[[package]]
name = "rustical"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"anyhow",
"argon2",
@@ -3060,7 +3060,7 @@ dependencies = [
[[package]]
name = "rustical_caldav"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"async-std",
"async-trait",
@@ -3100,7 +3100,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"async-trait",
"axum",
@@ -3132,7 +3132,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"async-trait",
"axum",
@@ -3157,7 +3157,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"async-trait",
"axum",
@@ -3182,7 +3182,7 @@ dependencies = [
[[package]]
name = "rustical_frontend"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"askama",
"askama_web",
@@ -3215,7 +3215,7 @@ dependencies = [
[[package]]
name = "rustical_ical"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"axum",
"chrono",
@@ -3233,7 +3233,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"async-trait",
"axum",
@@ -3248,7 +3248,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"anyhow",
"async-trait",
@@ -3282,7 +3282,7 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"async-trait",
"chrono",
@@ -3303,7 +3303,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.9.2"
version = "0.9.6"
dependencies = [
"quick-xml",
"thiserror 2.0.16",
@@ -3312,15 +3312,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.0.8"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -3530,7 +3530,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.11.0",
"indexmap 2.11.1",
"schemars 0.9.0",
"schemars 1.0.4",
"serde",
@@ -3690,7 +3690,7 @@ dependencies = [
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap 2.11.0",
"indexmap 2.11.1",
"log",
"memchr",
"once_cell",
@@ -3994,12 +3994,11 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.41"
version = "0.3.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
@@ -4009,15 +4008,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.4"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "time-macros"
version = "0.2.22"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [
"num-conv",
"time-core",
@@ -4132,7 +4131,7 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"indexmap 2.11.0",
"indexmap 2.11.1",
"serde",
"serde_spanned 1.0.0",
"toml_datetime 0.7.0",
@@ -4165,7 +4164,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap 2.11.0",
"indexmap 2.11.1",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
@@ -4228,7 +4227,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"indexmap 2.11.0",
"indexmap 2.11.1",
"pin-project-lite",
"slab",
"sync_wrapper",
@@ -4461,9 +4460,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "unicode-normalization"
@@ -4518,9 +4517,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.0"
version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.3",
"js-sys",
@@ -4590,9 +4589,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.3+wasi-0.2.4"
version = "0.14.5+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4"
dependencies = [
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.0+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24"
dependencies = [
"wit-bindgen",
]
@@ -4605,21 +4613,22 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
dependencies = [
"bumpalo",
"log",
@@ -4631,9 +4640,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.50"
version = "0.4.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
dependencies = [
"cfg-if",
"js-sys",
@@ -4644,9 +4653,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4654,9 +4663,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
dependencies = [
"proc-macro2",
"quote",
@@ -4667,18 +4676,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.77"
version = "0.3.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4715,11 +4724,11 @@ dependencies = [
[[package]]
name = "winapi-util"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -4730,7 +4739,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-link 0.1.3",
"windows-result",
"windows-strings",
]
@@ -4763,13 +4772,19 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -4778,7 +4793,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -4817,6 +4832,15 @@ dependencies = [
"windows-targets 0.53.3",
]
[[package]]
name = "windows-sys"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
dependencies = [
"windows-link 0.2.0",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@@ -4854,7 +4878,7 @@ version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows-link 0.1.3",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
@@ -5014,9 +5038,9 @@ dependencies = [
[[package]]
name = "wit-bindgen"
version = "0.45.0"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
[[package]]
name = "writeable"
@@ -5067,18 +5091,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.26"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -2,9 +2,10 @@
members = ["crates/*"]
[workspace.package]
version = "0.9.2"
version = "0.9.6"
edition = "2024"
description = "A CalDAV server"
documentation = "https://lennart-k.github.io/rustical/"
repository = "https://github.com/lennart-k/rustical"
license = "AGPL-3.0-or-later"
@@ -16,7 +17,7 @@ description.workspace = true
repository.workspace = true
license.workspace = true
resolver = "2"
publish = false
publish = true
[features]
debug = ["opentelemetry"]
@@ -61,7 +62,7 @@ tokio = { version = "1", features = [
url = "2.5"
base64 = "0.22"
thiserror = "2.0"
quick-xml = { version = "0.37" }
quick-xml = { version = "0.38" }
rust-embed = "8.5"
tower-sessions = "0.14"
futures-core = "0.3.31"

View File

@@ -45,4 +45,5 @@ CMD ["/usr/local/bin/rustical"]
ENV RUSTICAL_DATA_STORE__SQLITE__DB_URL=/var/lib/rustical/db.sqlite3
LABEL org.opencontainers.image.authors="Lennart K github.com/lennart-k"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
EXPOSE 4000

View File

@@ -4,14 +4,15 @@ a CalDAV/CardDAV server
> [!WARNING]
RustiCal is under **active development**!
While I've been successfully using RustiCal productively for a few weeks now,
While I've been successfully using RustiCal productively for some months now and there seems to be a growing user base,
you'd still be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
If you still want to use it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features
- easy to backup, everything saved in one SQLite database
- also export feature in the frontend
- Import your existing calendars in the frontend
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
- lightweight (the container image contains only one binary)
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)

View File

@@ -43,24 +43,24 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
.gregorian()
.prodid("RustiCal");
if calendar.displayname.is_some() {
if let Some(displayname) = calendar.meta.displayname {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALNAME".to_owned(),
value: calendar.displayname,
value: Some(displayname),
params: None,
});
}
if calendar.description.is_some() {
if let Some(description) = calendar.meta.description {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALDESC".to_owned(),
value: calendar.description,
value: Some(description),
params: None,
});
}
if calendar.timezone_id.is_some() {
if let Some(timezone_id) = calendar.timezone_id {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-TIMEZONE".to_owned(),
value: calendar.timezone_id,
value: Some(timezone_id),
params: None,
});
}

View File

@@ -9,8 +9,11 @@ use ical::{
generator::Emitter,
parser::{Component, ComponentMut},
};
use rustical_dav::header::Overwrite;
use rustical_ical::{CalendarObject, CalendarObjectType};
use rustical_store::{Calendar, CalendarStore, SubscriptionStore, auth::Principal};
use rustical_store::{
Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal,
};
use std::io::BufReader;
use tracing::instrument;
@@ -19,6 +22,7 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>,
user: Principal,
State(resource_service): State<CalendarResourceService<C, S>>,
overwrite: Overwrite,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
@@ -83,10 +87,12 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
let new_cal = Calendar {
principal,
id: cal_id,
displayname,
order: 0,
description,
color: None,
meta: CalendarMetadata {
displayname,
order: 0,
description,
color: None,
},
timezone_id,
deleted_at: None,
synctoken: 0,
@@ -96,7 +102,9 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
};
let cal_store = resource_service.cal_store;
cal_store.import_calendar(new_cal, objects, false).await?;
cal_store
.import_calendar(new_cal, objects, overwrite.is_true())
.await?;
Ok(StatusCode::OK.into_response())
}

View File

@@ -8,7 +8,7 @@ use ical::IcalParser;
use rustical_dav::xml::HrefElement;
use rustical_ical::CalendarObjectType;
use rustical_store::auth::Principal;
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
use rustical_store::{Calendar, CalendarMetadata, CalendarStore, SubscriptionStore};
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument;
@@ -112,11 +112,13 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
let calendar = Calendar {
id: cal_id.to_owned(),
principal: principal.to_owned(),
order: request.calendar_order.unwrap_or(0),
displayname: request.displayname,
meta: CalendarMetadata {
order: request.calendar_order.unwrap_or(0),
displayname: request.displayname,
color: request.calendar_color,
description: request.calendar_description,
},
timezone_id,
color: request.calendar_color,
description: request.calendar_description,
deleted_at: None,
synctoken: 0,
subscription_url: request.source.map(|href| href.href),

View File

@@ -116,19 +116,17 @@ impl CompFilterElement {
// TODO: Implement prop-filter (and comp-filter?) at some point
if let Some(time_range) = &self.time_range {
if let Some(start) = &time_range.start {
if let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) {
if start.deref() > &last_occurence.utc() {
return false;
}
};
if let Some(start) = &time_range.start
&& let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None)
&& start.deref() > &last_occurence.utc()
{
return false;
}
if let Some(end) = &time_range.end {
if let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) {
if end.deref() < &first_occurence.utc() {
return false;
}
};
if let Some(end) = &time_range.end
&& let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None)
&& end.deref() < &first_occurence.utc()
{
return false;
}
}
true
@@ -156,15 +154,15 @@ impl From<&FilterElement> for CalendarQuery {
for comp_filter in comp_filter_vcalendar.comp_filter.iter() {
// A calendar object cannot contain both VEVENT and VTODO, so we only have to handle
// whatever we get first
if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO") {
if let Some(time_range) = &comp_filter.time_range {
let start = time_range.start.as_ref().map(|start| start.date_naive());
let end = time_range.end.as_ref().map(|end| end.date_naive());
return CalendarQuery {
time_start: start,
time_end: end,
};
}
if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO")
&& let Some(time_range) = &comp_filter.time_range
{
let start = time_range.start.as_ref().map(|start| start.date_naive());
let end = time_range.end.as_ref().map(|end| end.date_naive());
return CalendarQuery {
time_start: start,
time_end: end,
};
}
}
Default::default()

View File

@@ -128,10 +128,10 @@ impl Resource for CalendarResource {
Ok(match prop {
CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
CalendarPropName::CalendarColor => {
CalendarProp::CalendarColor(self.cal.color.clone())
CalendarProp::CalendarColor(self.cal.meta.color.clone())
}
CalendarPropName::CalendarDescription => {
CalendarProp::CalendarDescription(self.cal.description.clone())
CalendarProp::CalendarDescription(self.cal.meta.description.clone())
}
CalendarPropName::CalendarTimezone => {
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
@@ -146,7 +146,7 @@ impl Resource for CalendarResource {
CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone())
}
CalendarPropName::CalendarOrder => {
CalendarProp::CalendarOrder(Some(self.cal.order))
CalendarProp::CalendarOrder(Some(self.cal.meta.order))
}
CalendarPropName::SupportedCalendarComponentSet => {
CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into())
@@ -187,11 +187,11 @@ impl Resource for CalendarResource {
match prop {
CalendarPropWrapper::Calendar(prop) => match prop {
CalendarProp::CalendarColor(color) => {
self.cal.color = color;
self.cal.meta.color = color;
Ok(())
}
CalendarProp::CalendarDescription(description) => {
self.cal.description = description;
self.cal.meta.description = description;
Ok(())
}
CalendarProp::CalendarTimezone(timezone) => {
@@ -236,7 +236,7 @@ impl Resource for CalendarResource {
Ok(())
}
CalendarProp::CalendarOrder(order) => {
self.cal.order = order.unwrap_or_default();
self.cal.meta.order = order.unwrap_or_default();
Ok(())
}
CalendarProp::SupportedCalendarComponentSet(comp_set) => {
@@ -264,11 +264,11 @@ impl Resource for CalendarResource {
match prop {
CalendarPropWrapperName::Calendar(prop) => match prop {
CalendarPropName::CalendarColor => {
self.cal.color = None;
self.cal.meta.color = None;
Ok(())
}
CalendarPropName::CalendarDescription => {
self.cal.description = None;
self.cal.meta.description = None;
Ok(())
}
CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
@@ -277,7 +277,7 @@ impl Resource for CalendarResource {
}
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::CalendarOrder => {
self.cal.order = 0;
self.cal.meta.order = 0;
Ok(())
}
CalendarPropName::SupportedCalendarComponentSet => {
@@ -300,10 +300,10 @@ impl Resource for CalendarResource {
}
fn get_displayname(&self) -> Option<&str> {
self.cal.displayname.as_deref()
self.cal.meta.displayname.as_deref()
}
fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
self.cal.displayname = name;
self.cal.meta.displayname = name;
Ok(())
}

View File

@@ -11,7 +11,7 @@ use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
use rustical_store::auth::Principal;
use std::str::FromStr;
use tracing::instrument;
use tracing::{debug, instrument};
#[instrument(skip(cal_store))]
pub async fn get_event<C: CalendarStore>(
@@ -78,9 +78,10 @@ pub async fn put_event<C: CalendarStore>(
true
};
let object = match CalendarObject::from_ics(body) {
let object = match CalendarObject::from_ics(body.clone()) {
Ok(obj) => obj,
Err(_) => {
debug!("invalid calendar data:\n{body}");
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
}
};

View File

@@ -79,5 +79,5 @@ async fn test_propfind() {
)
.unwrap();
let output = response.serialize_to_string().unwrap();
let _output = response.serialize_to_string().unwrap();
}

View File

@@ -66,6 +66,9 @@ impl<PN: XmlDeserialize> XmlDeserialize for PropElement<PN> {
Event::Text(_) | Event::CData(_) => {
return Err(XmlError::UnsupportedEvent("Not expecting text here"));
}
Event::GeneralRef(_) => {
return Err(::rustical_xml::XmlError::UnsupportedEvent("GeneralRef"));
}
Event::Decl(_) | Event::Comment(_) | Event::DocType(_) | Event::PI(_) => { /* ignore */
}
Event::End(_end) => {

View File

@@ -29,16 +29,11 @@ impl XmlSerialize for TagList {
});
let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname
.as_ref()
.map(|tagname| ::quick_xml::name::QName(tagname.as_bytes()));
if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned());
if !has_prefix {
if let Some(ns) = &ns {
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
}
if let Some(tagname) = tagname.as_ref() {
let mut bytes_start = BytesStart::new(tagname);
if !has_prefix && let Some(ns) = &ns {
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
}
writer.write_event(Event::Start(bytes_start))?;
}
@@ -51,8 +46,8 @@ impl XmlSerialize for TagList {
el.write_empty()?;
}
if let Some(qname) = &qname {
writer.write_event(Event::End(BytesEnd::from(qname.to_owned())))?;
if let Some(tagname) = tagname.as_ref() {
writer.write_event(Event::End(BytesEnd::new(tagname)))?;
}
Ok(())
}

View File

@@ -17,7 +17,7 @@ export class DeleteButton extends LitElement {
}
protected render() {
let text = this.trash ? 'Move to trash' : 'Delete'
let text = this.trash ? 'Trash' : 'Delete'
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
}

View File

@@ -28,7 +28,7 @@ export class EditAddressbookForm extends LitElement {
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Edit addressbook</button>
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${ref(this.dialog)}>
<h3>Edit addressbook</h3>
<form @submit=${this.submit} ${ref(this.form)}>

View File

@@ -40,7 +40,7 @@ export class EditCalendarForm extends LitElement {
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Edit calendar</button>
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${ref(this.dialog)}>
<h3>Edit calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}>

View File

@@ -19,7 +19,7 @@ let DeleteButton = class extends i {
return this;
}
render() {
let text = this.trash ? "Move to trash" : "Delete";
let text = this.trash ? "Trash" : "Delete";
return x`<button class="delete" @click=${(e) => this._onClick(e)}>${text}</button>`;
}
async _onClick(event) {

View File

@@ -27,7 +27,7 @@ let EditAddressbookForm = class extends i {
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Edit addressbook</button>
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${n(this.dialog)}>
<h3>Edit addressbook</h3>
<form @submit=${this.submit} ${n(this.form)}>

View File

@@ -28,7 +28,7 @@ let EditCalendarForm = class extends i {
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Edit calendar</button>
<button @click=${() => this.dialog.value.showModal()}>Edit</button>
<dialog ${n(this.dialog)}>
<h3>Edit calendar</h3>
<form @submit=${this.submit} ${n(this.form)}>

View File

@@ -1,13 +1,13 @@
<h2>{{ user.id }}'s Calendars</h2>
<ul class="collection-list">
{% for (meta, calendar) in calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
{% let color = calendar.meta.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a>
<div class="inner">
<span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
{{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
@@ -15,7 +15,7 @@
</div>
</span>
<span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
{% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %}
</span>
{% if let Some(subscription_url) = calendar.subscription_url %}
<span class="subscription-url">{{ subscription_url }}</span>
@@ -29,9 +29,9 @@
principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}"
timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}"
displayname="{{ calendar.displayname.as_deref().unwrap_or_default() }}"
description="{{ calendar.description.as_deref().unwrap_or_default() }}"
color="{{ calendar.color.as_deref().unwrap_or_default() }}"
displayname="{{ calendar.meta.displayname.as_deref().unwrap_or_default() }}"
description="{{ calendar.meta.description.as_deref().unwrap_or_default() }}"
color="{{ calendar.meta.color.as_deref().unwrap_or_default() }}"
components="{{ calendar.components | json }}"
></edit-calendar-form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
@@ -51,13 +51,13 @@
<h3>Deleted Calendars</h3>
<ul class="collection-list">
{% for (meta, calendar) in deleted_calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
{% let color = calendar.meta.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a>
<div class="inner">
<span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
{{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
@@ -65,7 +65,7 @@
</div>
</span>
<span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
{% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %}
</span>
<div class="actions">
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"

View File

@@ -4,9 +4,9 @@
{% endblock %}
{% block content %}
{% let name = calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
{% let name = calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
<h1>{{ calendar.principal }}/{{ name }}</h1>
{% if let Some(description) = calendar.description %}<p>{{ description }}</p>{% endif%}
{% if let Some(description) = calendar.meta.description %}<p>{{ description }}</p>{% endif%}
{% if let Some(subscription_url) = calendar.subscription_url %}
<h2>Subscription URL</h2>
@@ -25,9 +25,6 @@
{% if let Some(timezone_id) = calendar.timezone_id %}
<p>{{ timezone_id }}</p>
{% endif %}
{% if let Some(timezone) = calendar.get_vtimezone() %}
<textarea rows="16" readonly>{{ timezone }}</textarea>
{% endif %}
<pre>{{ calendar|json }}</pre>

View File

@@ -13,6 +13,7 @@ use tower::Service;
#[derive(Clone, RustEmbed, Default)]
#[folder = "public/assets"]
#[allow(dead_code)] // Since this is not used with the frontend-dev feature
pub struct Assets;
#[derive(Clone, Default)]

View File

@@ -192,20 +192,19 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
.await
.map_err(|e| OidcError::UserInfo(e.to_string()))?;
if let Some(require_group) = &oidc_config.require_group {
if !user_info_claims
if let Some(require_group) = &oidc_config.require_group
&& !user_info_claims
.additional_claims()
.groups
.clone()
.unwrap_or_default()
.contains(require_group)
{
return Ok((
StatusCode::UNAUTHORIZED,
"User is not in an authorized group to use RustiCal",
)
.into_response());
}
{
return Ok((
StatusCode::UNAUTHORIZED,
"User is not in an authorized group to use RustiCal",
)
.into_response());
}
let user_id = match oidc_config.claim_userid {

View File

@@ -72,12 +72,11 @@ where
let mut inner = self.inner.clone();
Box::pin(async move {
if let Some(session) = request.extensions().get::<Session>() {
if let Ok(Some(user_id)) = session.get::<String>("user").await {
if let Ok(Some(user)) = ap.get_principal(&user_id).await {
request.extensions_mut().insert(user);
}
}
if let Some(session) = request.extensions().get::<Session>()
&& let Ok(Some(user_id)) = session.get::<String>("user").await
&& let Ok(Some(user)) = ap.get_principal(&user_id).await
{
request.extensions_mut().insert(user);
}
if let Some(auth) = auth_header {

View File

@@ -6,13 +6,23 @@ use rustical_ical::CalendarObjectType;
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Calendar {
pub principal: String,
pub id: String,
pub struct CalendarMetadata {
// Attributes that may be outsourced
pub displayname: Option<String>,
pub order: i64,
pub description: Option<String>,
pub color: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Calendar {
// Attributes that may be outsourced
#[serde(flatten)]
pub meta: CalendarMetadata,
// Common calendar attributes
pub principal: String,
pub id: String,
pub timezone_id: Option<String>,
pub deleted_at: Option<NaiveDateTime>,
pub synctoken: i64,

View File

@@ -1,282 +1,208 @@
use crate::CalendarStore;
use async_trait::async_trait;
use derive_more::Constructor;
use rustical_ical::CalendarObject;
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};
use crate::{
Calendar, CalendarStore, Error, calendar_store::CalendarQuery,
contact_birthday_store::BIRTHDAYS_PREFIX,
};
#[derive(Debug, Constructor)]
pub struct CombinedCalendarStore<CS: CalendarStore, BS: CalendarStore> {
cal_store: Arc<CS>,
birthday_store: Arc<BS>,
pub trait PrefixedCalendarStore: CalendarStore {
const PREFIX: &'static str;
}
impl<CS: CalendarStore, BS: CalendarStore> Clone for CombinedCalendarStore<CS, BS> {
fn clone(&self) -> Self {
#[derive(Clone)]
pub struct CombinedCalendarStore {
stores: HashMap<&'static str, Arc<dyn CalendarStore>>,
default: Arc<dyn CalendarStore>,
}
impl CombinedCalendarStore {
pub fn new(default: Arc<dyn CalendarStore>) -> Self {
Self {
cal_store: self.cal_store.clone(),
birthday_store: self.birthday_store.clone(),
stores: HashMap::new(),
default,
}
}
pub fn with_store<CS: PrefixedCalendarStore>(mut self, store: Arc<CS>) -> Self {
let store: Arc<dyn CalendarStore> = store;
self.stores.insert(CS::PREFIX, store);
self
}
fn store_for_id(&self, id: &str) -> Arc<dyn CalendarStore> {
self.stores
.iter()
.find(|&(prefix, _store)| id.starts_with(prefix))
.map(|(_prefix, store)| store.clone())
.unwrap_or(self.default.clone())
}
}
#[async_trait]
impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarStore<CS, BS> {
impl CalendarStore for CombinedCalendarStore {
#[inline]
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
if id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.get_calendar(principal, id, show_deleted)
.await
} else {
self.cal_store
.get_calendar(principal, id, show_deleted)
.await
}
) -> Result<crate::Calendar, crate::Error> {
self.store_for_id(id)
.get_calendar(principal, id, show_deleted)
.await
}
#[inline]
async fn update_calendar(
&self,
principal: String,
id: String,
calendar: Calendar,
calendar: crate::Calendar,
) -> Result<(), crate::Error> {
if id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.update_calendar(principal, id, calendar)
.await
} else {
self.cal_store
.update_calendar(principal, id, calendar)
.await
}
self.store_for_id(&id)
.update_calendar(principal, id, calendar)
.await
}
#[inline]
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> {
if calendar.id.starts_with(BIRTHDAYS_PREFIX) {
Err(Error::ReadOnly)
} else {
self.cal_store.insert_calendar(calendar).await
}
async fn insert_calendar(&self, calendar: crate::Calendar) -> Result<(), crate::Error> {
self.store_for_id(&calendar.id)
.insert_calendar(calendar)
.await
}
#[inline]
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Ok([
self.cal_store.get_calendars(principal).await?,
self.birthday_store.get_calendars(principal).await?,
]
.concat())
async fn delete_calendar(
&self,
principal: &str,
name: &str,
use_trashbin: bool,
) -> Result<(), crate::Error> {
self.store_for_id(name)
.delete_calendar(principal, name, use_trashbin)
.await
}
async fn restore_calendar(&self, principal: &str, name: &str) -> Result<(), crate::Error> {
self.store_for_id(name)
.restore_calendar(principal, name)
.await
}
async fn sync_changes(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<rustical_ical::CalendarObject>, Vec<String>, i64), crate::Error> {
self.store_for_id(cal_id)
.sync_changes(principal, cal_id, synctoken)
.await
}
async fn import_calendar(
&self,
calendar: crate::Calendar,
objects: Vec<rustical_ical::CalendarObject>,
merge_existing: bool,
) -> Result<(), crate::Error> {
self.store_for_id(&calendar.id)
.import_calendar(calendar, objects, merge_existing)
.await
}
async fn calendar_query(
&self,
principal: &str,
cal_id: &str,
query: crate::calendar_store::CalendarQuery,
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
self.store_for_id(cal_id)
.calendar_query(principal, cal_id, query)
.await
}
async fn restore_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
) -> Result<(), crate::Error> {
self.store_for_id(cal_id)
.restore_object(principal, cal_id, object_id)
.await
}
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<crate::CollectionMetadata, crate::Error> {
self.store_for_id(cal_id)
.calendar_metadata(principal, cal_id)
.await
}
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
self.store_for_id(cal_id)
.get_objects(principal, cal_id)
.await
}
async fn put_object(
&self,
principal: String,
cal_id: String,
object: rustical_ical::CalendarObject,
overwrite: bool,
) -> Result<(), crate::Error> {
self.store_for_id(&cal_id)
.put_object(principal, cal_id, object, overwrite)
.await
}
#[inline]
async fn delete_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.delete_object(principal, cal_id, object_id, use_trashbin)
.await
} else {
self.cal_store
.delete_object(principal, cal_id, object_id, use_trashbin)
.await
}
) -> Result<(), crate::Error> {
self.store_for_id(cal_id)
.delete_object(principal, cal_id, object_id, use_trashbin)
.await
}
#[inline]
async fn get_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.get_object(principal, cal_id, object_id, show_deleted)
.await
} else {
self.cal_store
.get_object(principal, cal_id, object_id, show_deleted)
.await
}
) -> Result<rustical_ical::CalendarObject, crate::Error> {
self.store_for_id(cal_id)
.get_object(principal, cal_id, object_id, show_deleted)
.await
}
#[inline]
async fn sync_changes(
async fn get_calendars(&self, principal: &str) -> Result<Vec<crate::Calendar>, crate::Error> {
let mut calendars = self.default.get_calendars(principal).await?;
for store in self.stores.values() {
calendars.extend(store.get_calendars(principal).await?);
}
Ok(calendars)
}
async fn get_deleted_calendars(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.sync_changes(principal, cal_id, synctoken)
.await
} else {
self.cal_store
.sync_changes(principal, cal_id, synctoken)
.await
) -> Result<Vec<crate::Calendar>, crate::Error> {
let mut calendars = self.default.get_deleted_calendars(principal).await?;
for store in self.stores.values() {
calendars.extend(store.get_deleted_calendars(principal).await?);
}
Ok(calendars)
}
#[inline]
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<crate::CollectionMetadata, Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.calendar_metadata(principal, cal_id)
.await
} else {
self.cal_store.calendar_metadata(principal, cal_id).await
}
}
#[inline]
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store.get_objects(principal, cal_id).await
} else {
self.cal_store.get_objects(principal, cal_id).await
}
}
#[inline]
async fn calendar_query(
&self,
principal: &str,
cal_id: &str,
query: CalendarQuery,
) -> Result<Vec<CalendarObject>, Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.calendar_query(principal, cal_id, query)
.await
} else {
self.cal_store
.calendar_query(principal, cal_id, query)
.await
}
}
#[inline]
async fn restore_calendar(&self, principal: &str, name: &str) -> Result<(), Error> {
if name.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store.restore_calendar(principal, name).await
} else {
self.cal_store.restore_calendar(principal, name).await
}
}
#[inline]
async fn import_calendar(
&self,
calendar: Calendar,
objects: Vec<CalendarObject>,
merge_existing: bool,
) -> Result<(), Error> {
if calendar.id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.import_calendar(calendar, objects, merge_existing)
.await
} else {
self.cal_store
.import_calendar(calendar, objects, merge_existing)
.await
}
}
#[inline]
async fn delete_calendar(
&self,
principal: &str,
name: &str,
use_trashbin: bool,
) -> Result<(), Error> {
if name.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.delete_calendar(principal, name, use_trashbin)
.await
} else {
self.cal_store
.delete_calendar(principal, name, use_trashbin)
.await
}
}
#[inline]
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Ok([
self.birthday_store.get_deleted_calendars(principal).await?,
self.cal_store.get_deleted_calendars(principal).await?,
]
.concat())
}
#[inline]
async fn restore_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
) -> Result<(), Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.restore_object(principal, cal_id, object_id)
.await
} else {
self.cal_store
.restore_object(principal, cal_id, object_id)
.await
}
}
#[inline]
async fn put_object(
&self,
principal: String,
cal_id: String,
object: CalendarObject,
overwrite: bool,
) -> Result<(), Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store
.put_object(principal, cal_id, object, overwrite)
.await
} else {
self.cal_store
.put_object(principal, cal_id, object, overwrite)
.await
}
}
#[inline]
fn is_read_only(&self, cal_id: &str) -> bool {
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store.is_read_only(cal_id)
} else {
self.cal_store.is_read_only(cal_id)
}
self.store_for_id(cal_id).is_read_only(cal_id)
}
}

View File

@@ -1,4 +1,7 @@
use crate::{Addressbook, AddressbookStore, Calendar, CalendarStore, Error};
use crate::{
Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
combined_calendar_store::PrefixedCalendarStore,
};
use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
@@ -10,16 +13,22 @@ pub(crate) const BIRTHDAYS_PREFIX: &str = "_birthdays_";
#[derive(Constructor, Clone)]
pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>);
impl<AS: AddressbookStore> PrefixedCalendarStore for ContactBirthdayStore<AS> {
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
}
fn birthday_calendar(addressbook: Addressbook) -> Calendar {
Calendar {
principal: addressbook.principal,
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
displayname: addressbook
.displayname
.map(|name| format!("{name} birthdays")),
order: 0,
description: None,
color: None,
meta: CalendarMetadata {
displayname: addressbook
.displayname
.map(|name| format!("{name} birthdays")),
order: 0,
description: None,
color: None,
},
timezone_id: None,
deleted_at: addressbook.deleted_at,
synctoken: addressbook.synctoken,

View File

@@ -22,7 +22,7 @@ pub use secret::Secret;
pub use subscription_store::*;
pub use addressbook::Addressbook;
pub use calendar::Calendar;
pub use calendar::{Calendar, CalendarMetadata};
#[derive(Debug, Clone)]
pub enum CollectionOperationInfo {

View File

@@ -433,14 +433,14 @@ impl AddressbookStore for SqliteAddressbookStore {
Self::_delete_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await?;
tx.commit().await.map_err(crate::Error::from)?;
if let Some(addressbook) = addressbook {
if let Err(err) = self.sender.try_send(CollectionOperation {
if let Some(addressbook) = addressbook
&& let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Delete,
topic: addressbook.push_topic,
}) {
error!("Push notification about deleted addressbook failed: {err}");
};
}
})
{
error!("Push notification about deleted addressbook failed: {err}");
};
Ok(())
}

View File

@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
use rustical_store::calendar_store::CalendarQuery;
use rustical_store::synctoken::format_synctoken;
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error};
use rustical_store::{Calendar, CalendarMetadata, CalendarStore, CollectionMetadata, Error};
use rustical_store::{CollectionOperation, CollectionOperationInfo};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
@@ -69,10 +69,12 @@ impl From<CalendarRow> for Calendar {
Self {
principal: value.principal,
id: value.id,
displayname: value.displayname,
order: value.order,
description: value.description,
color: value.color,
meta: CalendarMetadata {
displayname: value.displayname,
order: value.order,
description: value.description,
color: value.color,
},
timezone_id: value.timezone_id,
deleted_at: value.deleted_at,
synctoken: value.synctoken,
@@ -159,10 +161,10 @@ impl SqliteCalendarStore {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
calendar.principal,
calendar.id,
calendar.displayname,
calendar.description,
calendar.order,
calendar.color,
calendar.meta.displayname,
calendar.meta.description,
calendar.meta.order,
calendar.meta.color,
calendar.subscription_url,
calendar.timezone_id,
calendar.push_topic,
@@ -189,10 +191,10 @@ impl SqliteCalendarStore {
WHERE (principal, id) = (?, ?)"#,
calendar.principal,
calendar.id,
calendar.displayname,
calendar.description,
calendar.order,
calendar.color,
calendar.meta.displayname,
calendar.meta.description,
calendar.meta.order,
calendar.meta.color,
calendar.timezone_id,
calendar.push_topic,
comp_event, comp_todo, comp_journal,
@@ -351,7 +353,6 @@ impl SqliteCalendarStore {
object: CalendarObject,
overwrite: bool,
) -> Result<(), Error> {
// TODO: Prevent objects from being commited to a subscription calendar
let (object_id, ics) = (object.get_id(), object.get_ics());
let first_occurence = object
@@ -554,14 +555,14 @@ impl CalendarStore for SqliteCalendarStore {
Self::_delete_calendar(&mut *tx, principal, id, use_trashbin).await?;
tx.commit().await.map_err(crate::Error::from)?;
if let Some(cal) = cal {
if let Err(err) = self.sender.try_send(CollectionOperation {
if let Some(cal) = cal
&& let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Delete,
topic: cal.push_topic,
}) {
error!("Push notification about deleted calendar failed: {err}");
};
}
})
{
error!("Push notification about deleted calendar failed: {err}");
};
Ok(())
}
@@ -667,11 +668,16 @@ impl CalendarStore for SqliteCalendarStore {
object: CalendarObject,
overwrite: bool,
) -> Result<(), Error> {
// TODO: Prevent objects from being commited to a subscription calendar
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let object_id = object.get_id().to_owned();
let calendar = Self::_get_calendar(&mut *tx, &principal, &cal_id, true).await?;
if calendar.subscription_url.is_some() {
// We cannot commit an object to a subscription calendar
return Err(Error::ReadOnly);
}
Self::_put_object(
&mut *tx,
principal.to_owned(),

View File

@@ -34,12 +34,11 @@ impl Enum {
});
let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname.as_ref().map(|tagname| ::quick_xml::name::QName(tagname.as_bytes()));
const enum_untagged: bool = #enum_untagged;
if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned());
if let Some(tagname) = tagname.as_ref() {
let mut bytes_start = BytesStart::new(tagname);
if !has_prefix {
if let Some(ns) = &ns {
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
@@ -50,8 +49,8 @@ impl Enum {
#(#variant_serializers);*
if let Some(qname) = &qname {
writer.write_event(Event::End(BytesEnd::from(qname.to_owned())))?;
if let Some(tagname) = tagname.as_ref() {
writer.write_event(Event::End(BytesEnd::new(tagname)))?;
}
Ok(())
}

View File

@@ -66,6 +66,9 @@ impl Enum {
Event::CData(cdata) => {
return Err(::rustical_xml::XmlError::UnsupportedEvent("CDATA"));
}
Event::GeneralRef(_) => {
return Err(::rustical_xml::XmlError::UnsupportedEvent("GeneralRef"));
}
Event::Decl(_) => { /* <?xml ... ?> ignore this */ }
Event::Comment(_) => { /* ignore */ }
Event::DocType(_) => { /* ignore */ }

View File

@@ -111,10 +111,9 @@ impl NamedStruct {
});
let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname.as_ref().map(|tagname| ::quick_xml::name::QName(tagname.as_bytes()));
if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned());
if let Some(tagname) = tagname.as_ref() {
let mut bytes_start = BytesStart::new(tagname);
if !has_prefix {
if let Some(ns) = &ns {
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
@@ -133,8 +132,8 @@ impl NamedStruct {
}
if !#is_empty {
#(#tag_writers);*
if let Some(qname) = &qname {
writer.write_event(Event::End(BytesEnd::from(qname.to_owned())))?;
if let Some(tagname) = tagname.as_ref() {
writer.write_event(Event::End(BytesEnd::new(tagname)))?;
}
}
Ok(())

View File

@@ -148,6 +148,8 @@ impl NamedStruct {
}
}
let mut string = String::new();
if !empty {
loop {
let event = reader.read_event_into(&mut buf)?;
@@ -167,12 +169,23 @@ impl NamedStruct {
}
}
Event::Text(bytes_text) => {
let text = bytes_text.unescape()?;
#(#text_field_branches)*
let text = bytes_text.decode()?;
string.push_str(&text);
}
Event::CData(cdata) => {
let text = String::from_utf8(cdata.to_vec())?;
#(#text_field_branches)*
string.push_str(&text);
}
Event::GeneralRef(gref) => {
if let Some(char) = gref.resolve_char_ref()? {
string.push(char);
} else if let Some(text) =
quick_xml::escape::resolve_xml_entity(&gref.xml_content()?)
{
string.push_str(text);
} else {
return Err(XmlError::UnsupportedEvent("invalid XML ref"));
}
}
Event::Decl(_) => { /* <?xml ... ?> ignore this */ }
Event::Comment(_) => { /* ignore */ }
@@ -185,6 +198,9 @@ impl NamedStruct {
}
}
let text = string;
#(#text_field_branches)*
Ok(Self {
#(#builder_field_builds),*
})

View File

@@ -8,6 +8,8 @@ pub enum XmlError {
#[error(transparent)]
QuickXmlError(#[from] quick_xml::Error),
#[error(transparent)]
QuickXmlEncodingError(#[from] quick_xml::encoding::EncodingError),
#[error(transparent)]
QuickXmlAttrError(#[from] quick_xml::events::attributes::AttrError),
#[error(transparent)]
FromUtf8Error(#[from] FromUtf8Error),

View File

@@ -1,7 +1,7 @@
use crate::XmlRootTag;
use quick_xml::{
events::{BytesStart, Event, attributes::Attribute},
name::{Namespace, QName},
name::Namespace,
};
use std::collections::HashMap;
pub use xml_derive::XmlSerialize;
@@ -76,9 +76,8 @@ impl XmlSerialize for () {
});
let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname.as_ref().map(|tagname| QName(tagname.as_bytes()));
if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned());
if let Some(tagname) = tagname.as_ref() {
let mut bytes_start = BytesStart::new(tagname);
if !has_prefix && let Some(ns) = &ns {
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
}

View File

@@ -1,6 +1,6 @@
use crate::{XmlDeserialize, XmlError, XmlSerialize};
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::name::{Namespace, QName};
use quick_xml::name::Namespace;
use std::collections::HashMap;
use std::num::{ParseFloatError, ParseIntError};
use std::{convert::Infallible, io::BufRead};
@@ -77,20 +77,23 @@ impl<T: ValueDeserialize> XmlDeserialize for T {
loop {
match reader.read_event_into(&mut buf)? {
Event::Text(bytes_text) => {
let text = bytes_text.unescape()?;
if !string.is_empty() {
// Content already written
return Err(XmlError::UnsupportedEvent("content already written"));
}
string = text.to_string();
let text = bytes_text.decode()?;
string.push_str(&text);
}
Event::CData(cdata) => {
let text = String::from_utf8(cdata.to_vec())?;
if !string.is_empty() {
// Content already written
return Err(XmlError::UnsupportedEvent("content already written"));
string.push_str(&text);
}
Event::GeneralRef(gref) => {
if let Some(char) = gref.resolve_char_ref()? {
string.push(char);
} else if let Some(text) =
quick_xml::escape::resolve_xml_entity(&gref.xml_content()?)
{
string.push_str(text);
} else {
return Err(XmlError::UnsupportedEvent("invalid XML ref"));
}
string = text;
}
Event::End(_) => break,
Event::Eof => return Err(XmlError::Eof),
@@ -123,17 +126,16 @@ impl<T: ValueSerialize> XmlSerialize for T {
});
let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
let qname = tagname.as_ref().map(|tagname| QName(tagname.as_bytes()));
if let Some(qname) = &qname {
let mut bytes_start = BytesStart::from(qname.to_owned());
if let Some(tagname) = tagname.as_ref() {
let mut bytes_start = BytesStart::new(tagname);
if !has_prefix && let Some(ns) = &ns {
bytes_start.push_attribute((b"xmlns".as_ref(), ns.as_ref()));
}
writer.write_event(Event::Start(bytes_start))?;
}
writer.write_event(Event::Text(BytesText::new(&self.serialize())))?;
if let Some(qname) = &qname {
writer.write_event(Event::End(BytesEnd::from(qname.to_owned())))?;
if let Some(tagname) = tagname {
writer.write_event(Event::End(BytesEnd::new(tagname)))?;
}
Ok(())
}

View File

@@ -275,7 +275,7 @@ fn test_xml_cdata() {
<document>
<![CDATA[some text]]>
<href><![CDATA[some stuff]]></href>
<okay>&gt;</okay>
<okay>nice&gt;text</okay>
</document>
"#,
)
@@ -285,11 +285,25 @@ fn test_xml_cdata() {
Document {
hello: "some text".to_owned(),
href: "some stuff".to_owned(),
okay: ">".to_owned()
okay: "nice>text".to_owned()
}
);
}
#[test]
fn test_quickxml_bytesref() {
let gt = quick_xml::events::BytesRef::new("gt");
assert!(!gt.is_char_ref());
let result = if !gt.is_char_ref() {
quick_xml::escape::resolve_xml_entity(&gt.xml_content().unwrap())
.unwrap()
.to_string()
} else {
gt.xml_content().unwrap().to_string()
};
assert_eq!(result, ">");
}
#[test]
fn test_struct_xml_decl() {
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
@@ -307,14 +321,14 @@ fn test_struct_xml_decl() {
let doc = Document::parse_str(
r#"
<?xml version="1.0" encoding="utf-8"?>
<document><child>Hello!</child></document>"#,
<document><child>Hello!&amp;</child></document>"#,
)
.unwrap();
assert_eq!(
doc,
Document {
child: Child {
text: "Hello!".to_owned()
text: "Hello!&".to_owned()
}
}
);

View File

@@ -3,10 +3,10 @@
a CalDAV/CardDAV server
!!! warning
RustiCal is under **active development**!
While I've been successfully using RustiCal productively for a few weeks now,
you'd still be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
RustiCal is under **active development**!
While I've been successfully using RustiCal productively for some months now and there seems to be a growing user base,
you'd still be one of the first testers so expect bugs and rough edges.
If you still want to use it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
[Installation](installation/index.md){ .md-button }
@@ -14,6 +14,7 @@ a CalDAV/CardDAV server
- easy to backup, everything saved in one SQLite database
- also export feature in the frontend
- Import your existing calendars in the frontend
- **[WebDAV Push](https://github.com/bitfireAT/webdav-push/)** support, so near-instant synchronisation to DAVx5
- lightweight (the container image contains only one binary)
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)

View File

@@ -9,7 +9,7 @@ docker run \
-p 4000:4000 \
-v YOUR_DATA_DIR:/var/lib/rustical/ \
-v OPTIONAL_YOUR_CONFIG_TOML:/etc/rustical/config.toml \ # (1)!
-e RUSTICAL__CONFIG_OPTION="asd" \ # (2)!
-e RUSTICAL_CONFIG_OPTION="asd" \ # (2)!
ghcr.io/lennart-k/rustical
```

View File

@@ -0,0 +1,11 @@
# Notes
## Kubernetes setup
If you setup RustiCal with Kubernetes and call the deployment `rustical`
Kubernetes will by default expose some environment variables starting with `RUSTICAL_`
that will be rejected by RustiCal.
So for now the solutions are either not calling the deployment `rustical` or setting
`enableServiceLinks: false`, see <https://kubernetes.io/docs/tutorials/services/connect-applications-service/#accessing-the-service>.
For the corresponding issue see <https://github.com/lennart-k/rustical/issues/122>

View File

@@ -68,6 +68,7 @@ nav:
- Installation:
- installation/index.md
- Configuration: installation/configuration.md
- Notes: installation/notes.md
- Client Setup: setup/client.md
- OpenID Connect: setup/oidc.md
- Developers:

View File

@@ -1,7 +1,7 @@
use crate::config::NextcloudLoginConfig;
use axum::Router;
use axum::body::{Body, HttpBody};
use axum::extract::Request;
use axum::extract::{DefaultBodyLimit, Request};
use axum::middleware::Next;
use axum::response::{Redirect, Response};
use axum::routing::{any, options};
@@ -39,11 +39,11 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
nextcloud_login_config: NextcloudLoginConfig,
dav_push_enabled: bool,
session_cookie_samesite_strict: bool,
payload_limit_mb: usize,
) -> Router<()> {
let combined_cal_store = Arc::new(CombinedCalendarStore::new(
cal_store.clone(),
ContactBirthdayStore::new(addr_store.clone()).into(),
));
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone()));
let combined_cal_store =
Arc::new(CombinedCalendarStore::new(cal_store.clone()).with_store(birthday_store));
let mut router = Router::new()
.merge(caldav_router(
@@ -203,4 +203,5 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
response
},
))
.layer(DefaultBodyLimit::max(payload_limit_mb * 1000 * 1000))
}

View File

@@ -8,6 +8,7 @@ pub struct HttpConfig {
pub host: String,
pub port: u16,
pub session_cookie_samesite_strict: bool,
pub payload_limit_mb: usize,
}
impl Default for HttpConfig {
@@ -16,6 +17,7 @@ impl Default for HttpConfig {
host: "0.0.0.0".to_owned(),
port: 4000,
session_cookie_samesite_strict: false,
payload_limit_mb: 4,
}
}
}

View File

@@ -117,6 +117,7 @@ async fn main() -> Result<()> {
config.nextcloud_login.clone(),
config.dav_push.enabled,
config.http.session_cookie_samesite_strict,
config.http.payload_limit_mb,
);
let app = ServiceExt::<Request>::into_make_service(
NormalizePathLayer::trim_trailing_slash().layer(app),