diff --git a/.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json b/.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json new file mode 100644 index 0000000..226adc1 --- /dev/null +++ b/.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM calendarobjects\n WHERE (principal, cal_id, id) NOT IN (\n SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog\n )\n ;\n ", + "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": "deleted: bool", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a" +} diff --git a/.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json b/.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json new file mode 100644 index 0000000..6c2ab3a --- /dev/null +++ b/.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM addressobjects\n WHERE (principal, addressbook_id, id) NOT IN (\n SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog\n )\n ;\n ", + "describe": { + "columns": [ + { + "name": "principal", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "addressbook_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "deleted: bool", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d" +} diff --git a/Cargo.lock b/Cargo.lock index 54d58d7..2d3e5a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,22 +73,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -139,7 +139,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -175,7 +175,7 @@ checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -310,7 +310,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -345,9 +345,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "bytes", @@ -397,9 +397,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.12.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460c45604cb25834835e3b4d3468510322852783dac36261d642424d75562ff3" +checksum = "dbfe9f610fe4e99cf0cfcd03ccf8c63c28c616fe714d80475ef731f3b13dd21b" dependencies = [ "axum", "axum-core", @@ -525,15 +525,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.44" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "shlex", @@ -586,9 +586,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" dependencies = [ "clap_builder", "clap_derive", @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" dependencies = [ "anstream", "anstyle", @@ -615,7 +615,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -747,7 +747,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -771,7 +771,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -782,7 +782,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -823,7 +823,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "unicode-xid", ] @@ -847,7 +847,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1048,9 +1048,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flume" @@ -1178,7 +1178,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1466,9 +1466,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -1519,9 +1519,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -1568,7 +1568,7 @@ dependencies = [ [[package]] name = "ical" version = "0.11.0" -source = "git+https://github.com/lennart-k/ical-rs#38e4201d5f653b07c9800cccec93996f542267b4" +source = "git+https://github.com/lennart-k/ical-rs#474caf58acbc8ebefd90e6b848741d7ed5136d65" dependencies = [ "chrono", "chrono-tz", @@ -1724,9 +1724,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -1957,9 +1957,9 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ "lazy_static", "libm", @@ -2081,9 +2081,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.74" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", @@ -2102,7 +2102,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2116,9 +2116,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -2327,7 +2327,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2400,7 +2400,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2526,7 +2526,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "version_check", "yansi", ] @@ -2551,14 +2551,14 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] @@ -2620,9 +2620,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -2718,7 +2718,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2849,9 +2849,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", @@ -2892,7 +2892,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.108", + "syn 2.0.110", "unicode-ident", ] @@ -2904,7 +2904,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" dependencies = [ "quote", "rand 0.8.5", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2919,9 +2919,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.8.0" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -2930,22 +2930,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.8.0" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.108", + "syn 2.0.110", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.8.0" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ "sha2", "walkdir", @@ -2974,7 +2974,7 @@ dependencies = [ [[package]] name = "rustical" -version = "0.10.1" +version = "0.10.5" dependencies = [ "anyhow", "argon2", @@ -3017,7 +3017,7 @@ dependencies = [ [[package]] name = "rustical_caldav" -version = "0.10.1" +version = "0.10.5" dependencies = [ "async-std", "async-trait", @@ -3057,7 +3057,7 @@ dependencies = [ [[package]] name = "rustical_carddav" -version = "0.10.1" +version = "0.10.5" dependencies = [ "async-trait", "axum", @@ -3089,7 +3089,7 @@ dependencies = [ [[package]] name = "rustical_dav" -version = "0.10.1" +version = "0.10.5" dependencies = [ "async-trait", "axum", @@ -3114,7 +3114,7 @@ dependencies = [ [[package]] name = "rustical_dav_push" -version = "0.10.1" +version = "0.10.5" dependencies = [ "async-trait", "axum", @@ -3139,7 +3139,7 @@ dependencies = [ [[package]] name = "rustical_frontend" -version = "0.10.1" +version = "0.10.5" dependencies = [ "askama", "askama_web", @@ -3175,7 +3175,7 @@ dependencies = [ [[package]] name = "rustical_ical" -version = "0.10.1" +version = "0.10.5" dependencies = [ "axum", "chrono", @@ -3192,7 +3192,7 @@ dependencies = [ [[package]] name = "rustical_oidc" -version = "0.10.1" +version = "0.10.5" dependencies = [ "async-trait", "axum", @@ -3208,7 +3208,7 @@ dependencies = [ [[package]] name = "rustical_store" -version = "0.10.1" +version = "0.10.5" dependencies = [ "anyhow", "async-trait", @@ -3240,7 +3240,7 @@ dependencies = [ [[package]] name = "rustical_store_sqlite" -version = "0.10.1" +version = "0.10.5" dependencies = [ "async-trait", "chrono", @@ -3262,7 +3262,7 @@ dependencies = [ [[package]] name = "rustical_xml" -version = "0.10.1" +version = "0.10.5" dependencies = [ "quick-xml", "thiserror 2.0.17", @@ -3284,9 +3284,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.34" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "ring", @@ -3352,9 +3352,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -3425,7 +3425,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -3493,9 +3493,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "base64 0.22.1", "chrono", @@ -3503,7 +3503,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.1.0", "serde_core", "serde_json", "serde_with_macros", @@ -3512,14 +3512,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -3687,7 +3687,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -3710,7 +3710,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.108", + "syn 2.0.110", "tokio", "url", ] @@ -3862,7 +3862,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -3884,9 +3884,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -3910,7 +3910,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -3939,7 +3939,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -3950,7 +3950,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -4044,7 +4044,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -4070,9 +4070,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -4356,7 +4356,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -4639,7 +4639,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "wasm-bindgen-shared", ] @@ -4674,9 +4674,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -4721,7 +4721,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -4732,7 +4732,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -5013,14 +5013,14 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xml_derive" -version = "0.1.0" +version = "0.10.5" dependencies = [ "darling", "heck", "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -5048,7 +5048,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "synstructure", ] @@ -5069,7 +5069,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -5089,7 +5089,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "synstructure", ] @@ -5129,5 +5129,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] diff --git a/Cargo.toml b/Cargo.toml index 18c96d1..2c4851b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ members = ["crates/*"] [workspace.package] -version = "0.10.1" +version = "0.10.5" +rust-version = "1.91" edition = "2024" description = "A CalDAV server" documentation = "https://lennart-k.github.io/rustical/" @@ -12,6 +13,7 @@ license = "AGPL-3.0-or-later" [package] name = "rustical" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true @@ -35,6 +37,17 @@ opentelemetry = [ debug = 0 [workspace.dependencies] +rustical_dav = { path = "./crates/dav/" } +rustical_dav_push = { path = "./crates/dav_push/" } +rustical_store = { path = "./crates/store/" } +rustical_store_sqlite = { path = "./crates/store_sqlite/" } +rustical_caldav = { path = "./crates/caldav/" } +rustical_carddav = { path = "./crates/carddav/" } +rustical_frontend = { path = "./crates/frontend/" } +rustical_xml = { path = "./crates/xml/" } +rustical_oidc = { path = "./crates/oidc/" } +rustical_ical = { path = "./crates/ical/" } + matchit = "0.9" uuid = { version = "1.11", features = ["v4", "fast-rng"] } async-trait = "0.1" @@ -108,16 +121,6 @@ tower-http = { version = "0.6", features = [ "catch-panic", ] } percent-encoding = "2.3" -rustical_dav = { path = "./crates/dav/" } -rustical_dav_push = { path = "./crates/dav_push/" } -rustical_store = { path = "./crates/store/" } -rustical_store_sqlite = { path = "./crates/store_sqlite/" } -rustical_caldav = { path = "./crates/caldav/" } -rustical_carddav = { path = "./crates/carddav/" } -rustical_frontend = { path = "./crates/frontend/" } -rustical_xml = { path = "./crates/xml/" } -rustical_oidc = { path = "./crates/oidc/" } -rustical_ical = { path = "./crates/ical/" } chrono-tz = "0.10" chrono-humanize = "0.2" rand = "0.9" @@ -147,19 +150,19 @@ openssl = { version = "0.10", features = ["vendored"] } async-std = { version = "1.13", features = ["attributes"] } [dependencies] -rustical_store = { workspace = true } -rustical_store_sqlite = { workspace = true } -rustical_caldav = { workspace = true } +rustical_store.workspace = true +rustical_store_sqlite.workspace = true +rustical_caldav.workspace = true rustical_carddav.workspace = true -rustical_frontend = { workspace = true } -toml = { workspace = true } -serde = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -anyhow = { workspace = true } +rustical_frontend.workspace = true +toml.workspace = true +serde.workspace = true +tokio.workspace = true +tracing.workspace = true +anyhow.workspace = true clap.workspace = true -sqlx = { workspace = true } -async-trait = { workspace = true } +sqlx.workspace = true +async-trait.workspace = true uuid.workspace = true axum.workspace = true diff --git a/crates/caldav/Cargo.toml b/crates/caldav/Cargo.toml index 2903282..a5bce0f 100644 --- a/crates/caldav/Cargo.toml +++ b/crates/caldav/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_caldav" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true @@ -17,21 +18,21 @@ serde_json.workspace = true axum.workspace = true axum-extra.workspace = true tower.workspace = true -async-trait = { workspace = true } -thiserror = { workspace = true } -quick-xml = { workspace = true } -tracing = { workspace = true } -futures-util = { workspace = true } -derive_more = { workspace = true } -base64 = { workspace = true } -serde = { workspace = true } -tokio = { workspace = true } -url = { workspace = true } -rustical_dav = { workspace = true } -rustical_store = { workspace = true } -chrono = { workspace = true } -chrono-tz = { workspace = true } -sha2 = { workspace = true } +async-trait.workspace = true +thiserror.workspace = true +quick-xml.workspace = true +tracing.workspace = true +futures-util.workspace = true +derive_more.workspace = true +base64.workspace = true +serde.workspace = true +tokio.workspace = true +url.workspace = true +rustical_dav.workspace = true +rustical_store.workspace = true +chrono.workspace = true +chrono-tz.workspace = true +sha2.workspace = true ical.workspace = true percent-encoding.workspace = true rustical_xml.workspace = true diff --git a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs index ae5277a..2cf61ff 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_multiget.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_multiget.rs @@ -26,16 +26,18 @@ pub async fn get_objects_calendar_multiget( let mut not_found = vec![]; for href in &cal_query.href { - if let Some(filename) = href.strip_prefix(path) { + if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8() + && let Some(filename) = href.strip_prefix(path) + { let filename = filename.trim_start_matches('/'); if let Some(object_id) = filename.strip_suffix(".ics") { match store.get_object(principal, cal_id, object_id, false).await { Ok(object) => result.push(object), - Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), + Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()), Err(err) => return Err(err.into()), } } else { - not_found.push(href.to_owned()); + not_found.push(href.to_string()); } } else { not_found.push(href.to_owned()); diff --git a/crates/carddav/Cargo.toml b/crates/carddav/Cargo.toml index aaf28f3..ef26313 100644 --- a/crates/carddav/Cargo.toml +++ b/crates/carddav/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_carddav" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true @@ -11,19 +12,19 @@ publish = false axum.workspace = true axum-extra.workspace = true tower.workspace = true -async-trait = { workspace = true } -thiserror = { workspace = true } -quick-xml = { workspace = true } -tracing = { workspace = true } -futures-util = { workspace = true } -derive_more = { workspace = true } -base64 = { workspace = true } -serde = { workspace = true } -tokio = { workspace = true } -url = { workspace = true } -rustical_dav = { workspace = true } -rustical_store = { workspace = true } -chrono = { workspace = true } +async-trait.workspace = true +thiserror.workspace = true +quick-xml.workspace = true +tracing.workspace = true +futures-util.workspace = true +derive_more.workspace = true +base64.workspace = true +serde.workspace = true +tokio.workspace = true +url.workspace = true +rustical_dav.workspace = true +rustical_store.workspace = true +chrono.workspace = true rustical_xml.workspace = true uuid.workspace = true rustical_dav_push.workspace = true diff --git a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs index 16e46ce..53fff13 100644 --- a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs +++ b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs @@ -34,7 +34,9 @@ pub async fn get_objects_addressbook_multiget( let mut not_found = vec![]; for href in &addressbook_multiget.href { - if let Some(filename) = href.strip_prefix(path) { + if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8() + && let Some(filename) = href.strip_prefix(path) + { let filename = filename.trim_start_matches('/'); if let Some(object_id) = filename.strip_suffix(".vcf") { match store @@ -42,11 +44,11 @@ pub async fn get_objects_addressbook_multiget( .await { Ok(object) => result.push(object), - Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), + Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()), Err(err) => return Err(err.into()), } } else { - not_found.push(href.to_owned()); + not_found.push(href.to_string()); } } else { not_found.push(href.to_owned()); diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 1cfbb79..e54df5b 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_dav" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true diff --git a/crates/dav_push/Cargo.toml b/crates/dav_push/Cargo.toml index ff740f6..f47954e 100644 --- a/crates/dav_push/Cargo.toml +++ b/crates/dav_push/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_dav_push" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true @@ -9,15 +10,15 @@ publish = false [dependencies] rustical_xml.workspace = true -async-trait = { workspace = true } -futures-util = { workspace = true } -quick-xml = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } -itertools = { workspace = true } -log = { workspace = true } -derive_more = { workspace = true } -tracing = { workspace = true } +async-trait.workspace = true +futures-util.workspace = true +quick-xml.workspace = true +serde.workspace = true +thiserror.workspace = true +itertools.workspace = true +log.workspace = true +derive_more.workspace = true +tracing.workspace = true reqwest.workspace = true tokio.workspace = true rustical_dav.workspace = true diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 009de98..2aa2148 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_frontend" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true diff --git a/crates/frontend/js-components/lib/create-calendar-form.ts b/crates/frontend/js-components/lib/create-calendar-form.ts index 1bbbc3d..8cbfd3b 100644 --- a/crates/frontend/js-components/lib/create-calendar-form.ts +++ b/crates/frontend/js-components/lib/create-calendar-form.ts @@ -2,6 +2,7 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { escapeXml } from "."; +import { getTimezones } from "./timezones.ts"; @customElement("create-calendar-form") export class CreateCalendarForm extends LitElement { diff --git a/crates/frontend/public/assets/js/create-calendar-form.mjs b/crates/frontend/public/assets/js/create-calendar-form.mjs index 816642a..f265d63 100644 --- a/crates/frontend/public/assets/js/create-calendar-form.mjs +++ b/crates/frontend/public/assets/js/create-calendar-form.mjs @@ -2,6 +2,7 @@ import { i, x } from "./lit-DkXrt_Iv.mjs"; import { n as n$1, t } from "./property-B8WoKf1Y.mjs"; import { e, n } from "./ref-BwbQvJBB.mjs"; import { e as escapeXml } from "./index-_IB1wMbZ.mjs"; +import { g as getTimezones } from "./timezones-B0vBBzCP.mjs"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __decorateClass = (decorators, target, key, kind) => { diff --git a/crates/frontend/public/assets/js/edit-calendar-form.mjs b/crates/frontend/public/assets/js/edit-calendar-form.mjs index 256d594..9ed9a8d 100644 --- a/crates/frontend/public/assets/js/edit-calendar-form.mjs +++ b/crates/frontend/public/assets/js/edit-calendar-form.mjs @@ -2,18 +2,7 @@ import { i, x } from "./lit-DkXrt_Iv.mjs"; import { n as n$1, t } from "./property-B8WoKf1Y.mjs"; import { e, n } from "./ref-BwbQvJBB.mjs"; import { e as escapeXml } from "./index-_IB1wMbZ.mjs"; -let timezonesPromise = null; -async function getTimezones() { - timezonesPromise ||= new Promise(async (resolve, reject) => { - try { - let response = await fetch("/frontend/_timezones.json"); - resolve(await response.json()); - } catch (e2) { - reject(e2); - } - }); - return await timezonesPromise; -} +import { g as getTimezones } from "./timezones-B0vBBzCP.mjs"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __decorateClass = (decorators, target, key, kind) => { diff --git a/crates/frontend/public/assets/js/timezones-B0vBBzCP.mjs b/crates/frontend/public/assets/js/timezones-B0vBBzCP.mjs new file mode 100644 index 0000000..986af83 --- /dev/null +++ b/crates/frontend/public/assets/js/timezones-B0vBBzCP.mjs @@ -0,0 +1,15 @@ +let timezonesPromise = null; +async function getTimezones() { + timezonesPromise ||= new Promise(async (resolve, reject) => { + try { + let response = await fetch("/frontend/_timezones.json"); + resolve(await response.json()); + } catch (e) { + reject(e); + } + }); + return await timezonesPromise; +} +export { + getTimezones as g +}; diff --git a/crates/ical/Cargo.toml b/crates/ical/Cargo.toml index a02cb26..8318de8 100644 --- a/crates/ical/Cargo.toml +++ b/crates/ical/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_ical" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true diff --git a/crates/ical/src/error.rs b/crates/ical/src/error.rs index 52ff7e7..94b41fe 100644 --- a/crates/ical/src/error.rs +++ b/crates/ical/src/error.rs @@ -2,7 +2,7 @@ use axum::{http::StatusCode, response::IntoResponse}; use crate::CalDateTimeError; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum Error { #[error("Invalid ics/vcf input: {0}")] InvalidData(String), diff --git a/crates/ical/src/timestamp.rs b/crates/ical/src/timestamp.rs index f96ad9d..623bc26 100644 --- a/crates/ical/src/timestamp.rs +++ b/crates/ical/src/timestamp.rs @@ -13,7 +13,7 @@ const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S"; const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ"; pub const LOCAL_DATE: &str = "%Y%m%d"; -#[derive(Debug, thiserror::Error)] +#[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" diff --git a/crates/oidc/Cargo.toml b/crates/oidc/Cargo.toml index 14ce514..40335b2 100644 --- a/crates/oidc/Cargo.toml +++ b/crates/oidc/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_oidc" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 2426311..eaaba56 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_store" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true @@ -8,15 +9,16 @@ license.workspace = true publish = false [dependencies] -anyhow = { workspace = true } -async-trait = { workspace = true } -serde = { workspace = true } -ical = { workspace = true } -chrono = { workspace = true } -regex = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } -chrono-tz = { workspace = true } +anyhow.workspace = true +async-trait.workspace = true +serde.workspace = true +sha2.workspace = true +ical.workspace = true +chrono.workspace = true +regex.workspace = true +thiserror.workspace = true +tracing.workspace = true +chrono-tz.workspace = true derive_more = { workspace = true, features = ["as_ref"] } rustical_xml.workspace = true tokio.workspace = true @@ -33,7 +35,7 @@ tower-sessions.workspace = true vtimezones-rs.workspace = true [dev-dependencies] -rstest = { workspace = true } -rstest_reuse = { workspace = true } +rstest.workspace = true +rstest_reuse.workspace = true rustical_store_sqlite.workspace = true tokio.workspace = true diff --git a/crates/store/src/addressbook.rs b/crates/store/src/addressbook.rs index 05016b8..2874221 100644 --- a/crates/store/src/addressbook.rs +++ b/crates/store/src/addressbook.rs @@ -2,7 +2,7 @@ use crate::synctoken::format_synctoken; use chrono::NaiveDateTime; use serde::Serialize; -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct Addressbook { pub id: String, pub principal: String, diff --git a/crates/store/src/calendar.rs b/crates/store/src/calendar.rs index da3be24..4849003 100644 --- a/crates/store/src/calendar.rs +++ b/crates/store/src/calendar.rs @@ -4,7 +4,7 @@ use rustical_ical::CalendarObjectType; use serde::{Deserialize, Serialize}; use std::str::FromStr; -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CalendarMetadata { // Attributes that may be outsourced pub displayname: Option, @@ -13,7 +13,7 @@ pub struct CalendarMetadata { pub color: Option, } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Calendar { // Attributes that may be outsourced #[serde(flatten)] diff --git a/crates/store/src/error.rs b/crates/store/src/error.rs index 649105c..9ace70f 100644 --- a/crates/store/src/error.rs +++ b/crates/store/src/error.rs @@ -41,6 +41,11 @@ impl Error { _ => StatusCode::INTERNAL_SERVER_ERROR, } } + + #[must_use] + pub const fn is_not_found(&self) -> bool { + matches!(self, Self::NotFound) + } } impl IntoResponse for Error { diff --git a/crates/store_sqlite/Cargo.toml b/crates/store_sqlite/Cargo.toml index 084c4cf..4011d25 100644 --- a/crates/store_sqlite/Cargo.toml +++ b/crates/store_sqlite/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_store_sqlite" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true @@ -15,12 +16,12 @@ rstest.workspace = true [dependencies] tokio.workspace = true -rustical_store = { workspace = true } -async-trait = { workspace = true } -serde = { workspace = true } -sqlx = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } +rustical_store.workspace = true +async-trait.workspace = true +serde.workspace = true +sqlx.workspace = true +thiserror.workspace = true +tracing.workspace = true derive_more.workspace = true chrono.workspace = true password-auth.workspace = true diff --git a/crates/store_sqlite/src/addressbook_store/mod.rs b/crates/store_sqlite/src/addressbook_store/mod.rs index e4794dd..56a9c90 100644 --- a/crates/store_sqlite/src/addressbook_store/mod.rs +++ b/crates/store_sqlite/src/addressbook_store/mod.rs @@ -9,7 +9,7 @@ use rustical_store::{ }; use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction}; use tokio::sync::mpsc::Sender; -use tracing::{error, instrument}; +use tracing::{error, instrument, warn}; pub mod birthday_calendar; @@ -34,6 +34,60 @@ pub struct SqliteAddressbookStore { } impl SqliteAddressbookStore { + // Commit "orphaned" objects to the changelog table + pub async fn repair_orphans(&self) -> Result<(), Error> { + struct Row { + principal: String, + addressbook_id: String, + id: String, + deleted: bool, + } + + let mut tx = self + .db + .begin_with(BEGIN_IMMEDIATE) + .await + .map_err(crate::Error::from)?; + + let rows = sqlx::query_as!( + Row, + r#" + SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS "deleted: bool" + FROM addressobjects + WHERE (principal, addressbook_id, id) NOT IN ( + SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog + ) + ; + "#, + ) + .fetch_all(&mut *tx) + .await + .map_err(crate::Error::from)?; + + for row in rows { + let operation = if row.deleted { + ChangeOperation::Delete + } else { + ChangeOperation::Add + }; + warn!( + "Commiting orphaned addressbook object ({},{},{}), deleted={}", + &row.principal, &row.addressbook_id, &row.id, &row.deleted + ); + log_object_operation( + &mut tx, + &row.principal, + &row.addressbook_id, + &row.id, + operation, + ) + .await?; + } + tx.commit().await.map_err(crate::Error::from)?; + + Ok(()) + } + async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>( executor: E, principal: &str, @@ -92,9 +146,9 @@ impl SqliteAddressbookStore { async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>( executor: E, - principal: String, - id: String, - addressbook: Addressbook, + principal: &str, + id: &str, + addressbook: &Addressbook, ) -> Result<(), rustical_store::Error> { let result = sqlx::query!( r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ? @@ -399,7 +453,7 @@ impl AddressbookStore for SqliteAddressbookStore { id: String, addressbook: Addressbook, ) -> Result<(), rustical_store::Error> { - Self::_update_addressbook(&self.db, principal, id, addressbook).await + Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await } #[instrument] @@ -631,7 +685,7 @@ impl AddressbookStore for SqliteAddressbookStore { .await? .push_topic, }) { - error!("Push notification about deleted addressbook failed: {err}"); + error!("Push notification about restored addressbook object failed: {err}"); } Ok(()) @@ -665,6 +719,7 @@ impl AddressbookStore for SqliteAddressbookStore { Self::_insert_addressbook(&mut *tx, &addressbook).await?; } + let mut sync_token = None; for object in objects { Self::_put_object( &mut *tx, @@ -674,9 +729,31 @@ impl AddressbookStore for SqliteAddressbookStore { false, ) .await?; + + sync_token = Some( + log_object_operation( + &mut tx, + &addressbook.principal, + &addressbook.id, + object.get_id(), + ChangeOperation::Add, + ) + .await?, + ); } tx.commit().await.map_err(crate::Error::from)?; + if let Some(sync_token) = sync_token + && let Err(err) = self.sender.try_send(CollectionOperation { + data: CollectionOperationInfo::Content { sync_token }, + topic: self + .get_addressbook(&addressbook.principal, &addressbook.id, true) + .await? + .push_topic, + }) + { + error!("Push notification about imported addressbook failed: {err}"); + } Ok(()) } } @@ -688,7 +765,7 @@ async fn log_object_operation( addressbook_id: &str, object_id: &str, operation: ChangeOperation, -) -> Result { +) -> Result { struct Synctoken { synctoken: i64, } @@ -703,7 +780,8 @@ async fn log_object_operation( addressbook_id ) .fetch_one(&mut **tx) - .await?; + .await + .map_err(crate::Error::from)?; sqlx::query!( r#" @@ -717,6 +795,7 @@ async fn log_object_operation( operation ) .execute(&mut **tx) - .await?; + .await + .map_err(crate::Error::from)?; Ok(format_synctoken(synctoken)) } diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index 5c47d84..1787672 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -11,7 +11,7 @@ use rustical_store::{CollectionOperation, CollectionOperationInfo}; use sqlx::types::chrono::NaiveDateTime; use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction}; use tokio::sync::mpsc::Sender; -use tracing::{error, instrument}; +use tracing::{error, instrument, warn}; #[derive(Debug, Clone)] struct CalendarObjectRow { @@ -94,6 +94,53 @@ pub struct SqliteCalendarStore { } impl SqliteCalendarStore { + // Commit "orphaned" objects to the changelog table + pub async fn repair_orphans(&self) -> Result<(), Error> { + struct Row { + principal: String, + cal_id: String, + id: String, + deleted: bool, + } + + let mut tx = self + .db + .begin_with(BEGIN_IMMEDIATE) + .await + .map_err(crate::Error::from)?; + + let rows = sqlx::query_as!( + Row, + r#" + SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS "deleted: bool" + FROM calendarobjects + WHERE (principal, cal_id, id) NOT IN ( + SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog + ) + ; + "#, + ) + .fetch_all(&mut *tx) + .await + .map_err(crate::Error::from)?; + + for row in rows { + let operation = if row.deleted { + ChangeOperation::Delete + } else { + ChangeOperation::Add + }; + warn!( + "Commiting orphaned calendar object ({},{},{}), deleted={}", + &row.principal, &row.cal_id, &row.id, &row.deleted + ); + log_object_operation(&mut tx, &row.principal, &row.cal_id, &row.id, operation).await?; + } + tx.commit().await.map_err(crate::Error::from)?; + + Ok(()) + } + async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>( executor: E, principal: &str, @@ -351,9 +398,9 @@ impl SqliteCalendarStore { #[instrument] async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>( executor: E, - principal: String, - cal_id: String, - object: CalendarObject, + principal: &str, + cal_id: &str, + object: &CalendarObject, overwrite: bool, ) -> Result<(), Error> { let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics()); @@ -600,18 +647,35 @@ impl CalendarStore for SqliteCalendarStore { Self::_insert_calendar(&mut *tx, calendar.clone()).await?; } + let mut sync_token = None; for object in objects { - Self::_put_object( - &mut *tx, - calendar.principal.clone(), - calendar.id.clone(), - object, - false, - ) - .await?; + Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?; + + sync_token = Some( + log_object_operation( + &mut tx, + &calendar.principal, + &calendar.id, + object.get_id(), + ChangeOperation::Add, + ) + .await?, + ); } tx.commit().await.map_err(crate::Error::from)?; + + if let Some(sync_token) = sync_token + && let Err(err) = self.sender.try_send(CollectionOperation { + data: CollectionOperationInfo::Content { sync_token }, + topic: self + .get_calendar(&calendar.principal, &calendar.id, true) + .await? + .push_topic, + }) + { + error!("Push notification about imported calendar failed: {err}"); + } Ok(()) } @@ -689,14 +753,7 @@ impl CalendarStore for SqliteCalendarStore { return Err(Error::ReadOnly); } - Self::_put_object( - &mut *tx, - principal.clone(), - cal_id.clone(), - object, - overwrite, - ) - .await?; + Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?; let sync_token = log_object_operation( &mut tx, @@ -774,7 +831,7 @@ impl CalendarStore for SqliteCalendarStore { data: CollectionOperationInfo::Content { sync_token }, topic: self.get_calendar(principal, cal_id, true).await?.push_topic, }) { - error!("Push notification about deleted calendar failed: {err}"); + error!("Push notification about restored calendar object failed: {err}"); } Ok(()) } @@ -795,6 +852,7 @@ impl CalendarStore for SqliteCalendarStore { } // Logs an operation to the events +// TODO: Log multiple updates async fn log_object_operation( tx: &mut Transaction<'_, Sqlite>, principal: &str, diff --git a/crates/store_sqlite/src/tests/addressbook_store.rs b/crates/store_sqlite/src/tests/addressbook_store.rs new file mode 100644 index 0000000..c61c87e --- /dev/null +++ b/crates/store_sqlite/src/tests/addressbook_store.rs @@ -0,0 +1,81 @@ +#[cfg(test)] +mod tests { + use crate::{addressbook_store::SqliteAddressbookStore, tests::get_test_addressbook_store}; + use rstest::rstest; + use rustical_store::{Addressbook, AddressbookStore}; + + #[rstest] + #[tokio::test] + async fn test_addressbook_store( + #[from(get_test_addressbook_store)] + #[future] + addr_store: SqliteAddressbookStore, + ) { + let addr_store = addr_store.await; + + let cal = Addressbook { + id: "addr".to_string(), + principal: "fake-user".to_string(), + displayname: None, + description: None, + deleted_at: None, + synctoken: 0, + push_topic: "alskdj".to_string(), + }; + + assert!( + addr_store.insert_addressbook(cal).await.is_err(), + "This should fail due to the user not existing " + ); + + let addr = Addressbook { + id: "addr".to_string(), + principal: "user".to_string(), + displayname: None, + description: None, + deleted_at: None, + synctoken: 0, + push_topic: "alskdj".to_string(), + }; + + addr_store.insert_addressbook(addr.clone()).await.unwrap(); + + assert_eq!( + addr_store + .get_addressbook("user", "addr", false) + .await + .unwrap(), + addr + ); + + addr_store + .delete_addressbook("user", "addr", true) + .await + .unwrap(); + + let Err(err) = addr_store.get_addressbook("user", "addr", false).await else { + panic!() + }; + assert!(err.is_not_found()); + + addr_store + .get_addressbook("user", "addr", true) + .await + .unwrap(); + + addr_store + .restore_addressbook("user", "addr") + .await + .unwrap(); + + addr_store + .delete_addressbook("user", "addr", false) + .await + .unwrap(); + + let Err(err) = addr_store.get_addressbook("user", "addr", true).await else { + panic!() + }; + assert!(err.is_not_found()); + } +} diff --git a/crates/store_sqlite/src/tests/calendar_store.rs b/crates/store_sqlite/src/tests/calendar_store.rs new file mode 100644 index 0000000..e12cefd --- /dev/null +++ b/crates/store_sqlite/src/tests/calendar_store.rs @@ -0,0 +1,76 @@ +#[cfg(test)] +mod tests { + use crate::{calendar_store::SqliteCalendarStore, tests::get_test_calendar_store}; + use rstest::rstest; + use rustical_store::{Calendar, CalendarMetadata, CalendarStore}; + + #[rstest] + #[tokio::test] + async fn test_calendar_store( + #[from(get_test_calendar_store)] + #[future] + cal_store: SqliteCalendarStore, + ) { + let cal_store = cal_store.await; + + let cal = Calendar { + principal: "fake-user".to_string(), + timezone_id: None, + deleted_at: None, + meta: CalendarMetadata::default(), + id: "cal".to_string(), + synctoken: 0, + subscription_url: None, + push_topic: "alskdj".to_string(), + components: vec![], + }; + + assert!( + cal_store.insert_calendar(cal).await.is_err(), + "This should fail due to the user not existing " + ); + + let cal = Calendar { + principal: "user".to_string(), + timezone_id: None, + deleted_at: None, + meta: CalendarMetadata::default(), + id: "cal".to_string(), + synctoken: 0, + subscription_url: None, + push_topic: "alskdj".to_string(), + components: vec![], + }; + + cal_store.insert_calendar(cal.clone()).await.unwrap(); + + assert_eq!( + cal_store.get_calendar("user", "cal", false).await.unwrap(), + cal + ); + + cal_store + .delete_calendar("user", "cal", true) + .await + .unwrap(); + + let Err(err) = cal_store.get_calendar("user", "cal", false).await else { + panic!() + }; + assert!(err.is_not_found()); + + cal_store.get_calendar("user", "cal", true).await.unwrap(); + + cal_store.restore_calendar("user", "cal").await.unwrap(); + + cal_store + .delete_calendar("user", "cal", false) + .await + .unwrap(); + + let Err(err) = cal_store.get_calendar("user", "cal", true).await else { + panic!() + }; + assert!(err.is_not_found()); + } +} diff --git a/crates/store_sqlite/src/tests/mod.rs b/crates/store_sqlite/src/tests/mod.rs index 4715cab..c175657 100644 --- a/crates/store_sqlite/src/tests/mod.rs +++ b/crates/store_sqlite/src/tests/mod.rs @@ -8,6 +8,9 @@ use tokio::sync::OnceCell; static DB: OnceCell = OnceCell::const_new(); +mod addressbook_store; +mod calendar_store; + async fn get_test_db() -> SqlitePool { DB.get_or_init(async || { let db = SqlitePool::connect("sqlite::memory:").await.unwrap(); diff --git a/crates/xml/Cargo.toml b/crates/xml/Cargo.toml index a4497ec..fa03c71 100644 --- a/crates/xml/Cargo.toml +++ b/crates/xml/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rustical_xml" version.workspace = true +rust-version.workspace = true edition.workspace = true description.workspace = true repository.workspace = true diff --git a/crates/xml/derive/Cargo.toml b/crates/xml/derive/Cargo.toml index ffb04f3..ef6c293 100644 --- a/crates/xml/derive/Cargo.toml +++ b/crates/xml/derive/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "xml_derive" -version = "0.1.0" -edition = "2024" +version.workspace = true +rust-version.workspace = true +edition.workspace = true license.workspace = true [lib] diff --git a/docs/googlec55e08580a46745c.html b/docs/googlec55e08580a46745c.html new file mode 100644 index 0000000..56e0497 --- /dev/null +++ b/docs/googlec55e08580a46745c.html @@ -0,0 +1 @@ +google-site-verification: googlec55e08580a46745c.html diff --git a/src/app.rs b/src/app.rs index 2a16b91..4b2f6a1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -178,7 +178,9 @@ pub fn make_app< tracing::debug!("unauthorized"); } StatusCode::NOT_FOUND => { - tracing::warn!("client error"); + // Clients like GNOME Calendar will try to reach /remote.php/webdav + // quite often clogging up the logs + tracing::info!("client error"); } _ => { tracing::error!("client error"); diff --git a/src/main.rs b/src/main.rs index 706fa20..a02ef16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,7 +71,9 @@ async fn get_data_stores( let (send, recv) = tokio::sync::mpsc::channel(1000); let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone())); + addressbook_store.repair_orphans().await?; let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send)); + cal_store.repair_orphans().await?; let subscription_store = Arc::new(SqliteStore::new(db.clone())); let principal_store = Arc::new(SqlitePrincipalStore::new(db)); (