Compare commits

..

10 Commits

Author SHA1 Message Date
Lennart
873b40ad10 stylesheet: Add flex-wrap to actions 2025-11-05 16:05:55 +01:00
Lennart
5588137f73 sqlx prepare 2025-11-04 17:01:54 +01:00
Lennart
7bf00da0e5 implement deleting and restoring birthday calendars 2025-11-04 16:56:17 +01:00
Lennart
be08275cd3 Merge branch 'main' into feature/birthday-calendar 2025-11-04 16:28:08 +01:00
Lennart
381af1b877 run .sqlx prepare 2025-11-03 15:37:40 +01:00
Lennart
425d10cb99 CalendarStore::is_read_only now refers to its content only and not its metadata 2025-11-02 21:07:06 +01:00
Lennart
5cdbb3b9d3 migrate birthday store to sqlite 2025-11-02 21:06:43 +01:00
Lennart
547e477eca make sure a birthday calendar will be created for each addressbook 2025-11-02 21:05:31 +01:00
Lennart
c19c3492c3 frontend: Remove birthday calendar guard 2025-11-02 20:45:58 +01:00
Lennart
5878b93d62 add birthday_calendar table migrations 2025-11-02 20:45:31 +01:00
50 changed files with 908 additions and 876 deletions

View File

@@ -1,38 +0,0 @@
{
"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"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e"
}

View File

@@ -0,0 +1,74 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?)\n ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "displayname",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 7,
"type_info": "Datetime"
},
{
"name": "addr_synctoken",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "push_topic",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
true,
true,
false,
true,
true,
true,
false,
false
]
},
"hash": "525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc"
}

View File

@@ -0,0 +1,74 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE principal = ?\n AND (\n (deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted\n OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted\n )\n ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "displayname",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 7,
"type_info": "Datetime"
},
{
"name": "addr_synctoken",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "push_topic",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
true,
true,
false,
true,
true,
true,
false,
false
]
},
"hash": "66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO birthday_calendars (principal, id, displayname, push_topic)\n VALUES (?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c"
}

View File

@@ -1,38 +0,0 @@
{
"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"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a"
}

220
Cargo.lock generated
View File

@@ -73,22 +73,22 @@ dependencies = [
[[package]]
name = "anstyle-query"
version = "1.1.5"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -139,7 +139,7 @@ dependencies = [
"rustc-hash",
"serde",
"serde_derive",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -175,7 +175,7 @@ checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -310,7 +310,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -345,9 +345,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
version = "0.8.7"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
dependencies = [
"axum-core",
"bytes",
@@ -397,9 +397,9 @@ dependencies = [
[[package]]
name = "axum-extra"
version = "0.12.2"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbfe9f610fe4e99cf0cfcd03ccf8c63c28c616fe714d80475ef731f3b13dd21b"
checksum = "460c45604cb25834835e3b4d3468510322852783dac36261d642424d75562ff3"
dependencies = [
"axum",
"axum-core",
@@ -525,15 +525,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
version = "1.2.46"
version = "1.2.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -586,9 +586,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.52"
version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
dependencies = [
"clap_builder",
"clap_derive",
@@ -596,9 +596,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.52"
version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
dependencies = [
"anstream",
"anstyle",
@@ -615,7 +615,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -747,7 +747,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -771,7 +771,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -782,7 +782,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -823,7 +823,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
"unicode-xid",
]
@@ -847,7 +847,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -1048,9 +1048,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
[[package]]
name = "flume"
@@ -1178,7 +1178,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -1466,9 +1466,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.8.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
dependencies = [
"atomic-waker",
"bytes",
@@ -1519,9 +1519,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.18"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
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#474caf58acbc8ebefd90e6b848741d7ed5136d65"
source = "git+https://github.com/lennart-k/ical-rs#38e4201d5f653b07c9800cccec93996f542267b4"
dependencies = [
"chrono",
"chrono-tz",
@@ -1724,9 +1724,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.9"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
@@ -1957,9 +1957,9 @@ dependencies = [
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b"
dependencies = [
"lazy_static",
"libm",
@@ -2081,9 +2081,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.75"
version = "0.10.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
dependencies = [
"bitflags",
"cfg-if",
@@ -2102,7 +2102,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -2116,9 +2116,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.111"
version = "0.9.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
dependencies = [
"cc",
"libc",
@@ -2327,7 +2327,7 @@ dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -2400,7 +2400,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -2526,7 +2526,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
"version_check",
"yansi",
]
@@ -2551,14 +2551,14 @@ dependencies = [
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
name = "quick-xml"
version = "0.38.4"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
dependencies = [
"memchr",
]
@@ -2620,9 +2620,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.42"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
@@ -2718,7 +2718,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -2849,9 +2849,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.9"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
dependencies = [
"const-oid",
"digest",
@@ -2892,7 +2892,7 @@ dependencies = [
"regex",
"relative-path",
"rustc_version",
"syn 2.0.110",
"syn 2.0.108",
"unicode-ident",
]
@@ -2904,7 +2904,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
dependencies = [
"quote",
"rand 0.8.5",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -2919,9 +2919,9 @@ dependencies = [
[[package]]
name = "rust-embed"
version = "8.9.0"
version = "8.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca"
checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@@ -2930,22 +2930,22 @@ dependencies = [
[[package]]
name = "rust-embed-impl"
version = "8.9.0"
version = "8.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2"
checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.110",
"syn 2.0.108",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.9.0"
version = "8.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475"
checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185"
dependencies = [
"sha2",
"walkdir",
@@ -2974,7 +2974,7 @@ dependencies = [
[[package]]
name = "rustical"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"anyhow",
"argon2",
@@ -3017,7 +3017,7 @@ dependencies = [
[[package]]
name = "rustical_caldav"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"async-std",
"async-trait",
@@ -3057,7 +3057,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"async-trait",
"axum",
@@ -3089,7 +3089,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"async-trait",
"axum",
@@ -3114,7 +3114,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"async-trait",
"axum",
@@ -3139,7 +3139,7 @@ dependencies = [
[[package]]
name = "rustical_frontend"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"askama",
"askama_web",
@@ -3175,7 +3175,7 @@ dependencies = [
[[package]]
name = "rustical_ical"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"axum",
"chrono",
@@ -3192,7 +3192,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"async-trait",
"axum",
@@ -3208,7 +3208,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"anyhow",
"async-trait",
@@ -3230,7 +3230,6 @@ dependencies = [
"rustical_store_sqlite",
"rustical_xml",
"serde",
"sha2",
"thiserror 2.0.17",
"tokio",
"tower",
@@ -3241,7 +3240,7 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"async-trait",
"chrono",
@@ -3253,6 +3252,7 @@ dependencies = [
"rustical_ical",
"rustical_store",
"serde",
"sha2",
"sqlx",
"thiserror 2.0.17",
"tokio",
@@ -3262,7 +3262,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.10.5"
version = "0.10.1"
dependencies = [
"quick-xml",
"thiserror 2.0.17",
@@ -3284,9 +3284,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.35"
version = "0.23.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
dependencies = [
"once_cell",
"ring",
@@ -3352,9 +3352,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "1.1.0"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
dependencies = [
"dyn-clone",
"ref-cast",
@@ -3425,7 +3425,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -3493,9 +3493,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.16.0"
version = "3.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1"
checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04"
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.1.0",
"schemars 1.0.4",
"serde_core",
"serde_json",
"serde_with_macros",
@@ -3512,14 +3512,14 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.16.0"
version = "3.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b"
checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -3687,7 +3687,7 @@ dependencies = [
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -3710,7 +3710,7 @@ dependencies = [
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.110",
"syn 2.0.108",
"tokio",
"url",
]
@@ -3862,7 +3862,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -3884,9 +3884,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.110"
version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [
"proc-macro2",
"quote",
@@ -3910,7 +3910,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -3939,7 +3939,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -3950,7 +3950,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -4044,7 +4044,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -4070,9 +4070,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.17"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
dependencies = [
"bytes",
"futures-core",
@@ -4356,7 +4356,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -4639,7 +4639,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
"wasm-bindgen-shared",
]
@@ -4674,9 +4674,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "1.0.4"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8"
dependencies = [
"rustls-pki-types",
]
@@ -4721,7 +4721,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -4732,7 +4732,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -5013,14 +5013,14 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "xml_derive"
version = "0.10.5"
version = "0.1.0"
dependencies = [
"darling",
"heck",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -5048,7 +5048,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
"synstructure",
]
@@ -5069,7 +5069,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]
[[package]]
@@ -5089,7 +5089,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
"synstructure",
]
@@ -5129,5 +5129,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
"syn 2.0.108",
]

View File

@@ -2,8 +2,7 @@
members = ["crates/*"]
[workspace.package]
version = "0.10.5"
rust-version = "1.91"
version = "0.10.1"
edition = "2024"
description = "A CalDAV server"
documentation = "https://lennart-k.github.io/rustical/"
@@ -13,7 +12,6 @@ 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
@@ -37,17 +35,6 @@ 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"
@@ -121,6 +108,16 @@ 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"
@@ -150,19 +147,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

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_caldav"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -18,21 +17,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

View File

@@ -26,18 +26,16 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
let mut not_found = vec![];
for href in &cal_query.href {
if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
&& let Some(filename) = href.strip_prefix(path)
{
if 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_string()),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
Err(err) => return Err(err.into()),
}
} else {
not_found.push(href.to_string());
not_found.push(href.to_owned());
}
} else {
not_found.push(href.to_owned());

View File

@@ -317,16 +317,11 @@ impl Resource for CalendarResource {
}
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
if self.cal.subscription_url.is_some() {
if self.cal.subscription_url.is_some() || self.read_only {
return Ok(UserPrivilegeSet::owner_write_properties(
user.is_principal(&self.cal.principal),
));
}
if self.read_only {
return Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.cal.principal),
));
}
Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.cal.principal),

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_carddav"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -12,19 +11,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

View File

@@ -34,9 +34,7 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
let mut not_found = vec![];
for href in &addressbook_multiget.href {
if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
&& let Some(filename) = href.strip_prefix(path)
{
if let Some(filename) = href.strip_prefix(path) {
let filename = filename.trim_start_matches('/');
if let Some(object_id) = filename.strip_suffix(".vcf") {
match store
@@ -44,11 +42,11 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
.await
{
Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
Err(err) => return Err(err.into()),
}
} else {
not_found.push(href.to_string());
not_found.push(href.to_owned());
}
} else {
not_found.push(href.to_owned());

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_dav"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_dav_push"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -10,15 +9,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

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_frontend"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true

View File

@@ -2,7 +2,6 @@ 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 {

View File

@@ -2,7 +2,6 @@ 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) => {

View File

@@ -2,7 +2,18 @@ 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";
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;
}
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {

View File

@@ -1,15 +0,0 @@
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
};

View File

@@ -282,6 +282,7 @@ ul.collection-list {
grid-area: actions;
width: fit-content;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
}

View File

@@ -24,7 +24,6 @@
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
<button type="submit">Download</button>
</form>
{% if !calendar.id.starts_with("_birthdays_") %}
<edit-calendar-form
principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}"
@@ -35,7 +34,6 @@
components="{{ calendar.components | json }}"
></edit-calendar-form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
{% endif %}
</div>
<div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_ical"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true

View File

@@ -2,7 +2,7 @@ use axum::{http::StatusCode, response::IntoResponse};
use crate::CalDateTimeError;
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid ics/vcf input: {0}")]
InvalidData(String),

View File

@@ -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, PartialEq, Eq)]
#[derive(Debug, thiserror::Error)]
pub enum CalDateTimeError {
#[error(
"Timezone has X-LIC-LOCATION property to specify a timezone from the Olson database, however its value {0} is invalid"

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_oidc"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_store"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -9,16 +8,15 @@ license.workspace = true
publish = false
[dependencies]
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
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 }
derive_more = { workspace = true, features = ["as_ref"] }
rustical_xml.workspace = true
tokio.workspace = true
@@ -35,7 +33,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

View File

@@ -2,7 +2,7 @@ use crate::synctoken::format_synctoken;
use chrono::NaiveDateTime;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize)]
pub struct Addressbook {
pub id: String,
pub principal: String,

View File

@@ -4,7 +4,7 @@ use rustical_ical::CalendarObjectType;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CalendarMetadata {
// Attributes that may be outsourced
pub displayname: Option<String>,
@@ -13,7 +13,7 @@ pub struct CalendarMetadata {
pub color: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Calendar {
// Attributes that may be outsourced
#[serde(flatten)]

View File

@@ -98,5 +98,6 @@ pub trait CalendarStore: Send + Sync + 'static {
object_id: &str,
) -> Result<(), Error>;
// read_only refers to objects, metadata may still be updated
fn is_read_only(&self, cal_id: &str) -> bool;
}

View File

@@ -1,209 +0,0 @@
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};
use sha2::{Digest, Sha256};
use std::{collections::HashMap, sync::Arc};
pub 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),
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,
subscription_url: None,
push_topic: {
let mut hasher = Sha256::new();
hasher.update("birthdays");
hasher.update(addressbook.push_topic);
format!("{:x}", hasher.finalize())
},
components: vec![CalendarObjectType::Event],
}
}
/// Objects are all prefixed with `BIRTHDAYS_PREFIX`
#[async_trait]
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
let addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
Ok(birthday_calendar(addressbook))
}
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let addressbooks = self.0.get_addressbooks(principal).await?;
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
}
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let addressbooks = self.0.get_deleted_addressbooks(principal).await?;
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
}
async fn update_calendar(
&self,
_principal: String,
_id: String,
_calendar: Calendar,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn delete_calendar(
&self,
_principal: &str,
_name: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn import_calendar(
&self,
_calendar: Calendar,
_objects: Vec<CalendarObject>,
_merge_existing: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn sync_changes(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (objects, deleted_objects, new_synctoken) =
self.0.sync_changes(principal, cal_id, synctoken).await?;
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
.iter()
.map(AddressObject::get_birthday_object)
.collect();
let objects = objects?.into_iter().flatten().collect();
Ok((objects, deleted_objects, new_synctoken))
}
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<crate::CollectionMetadata, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
self.0.addressbook_metadata(principal, cal_id).await
}
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
self.0
.get_objects(principal, cal_id)
.await?
.iter()
.map(AddressObject::get_significant_dates)
.collect();
let objects = objects?
.into_iter()
.flat_map(HashMap::into_values)
.collect();
Ok(objects)
}
async fn get_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
self.0
.get_object(principal, cal_id, addressobject_id, show_deleted)
.await?
.get_significant_dates()?
.remove(date_type)
.ok_or(Error::NotFound)
}
async fn put_object(
&self,
_principal: String,
_cal_id: String,
_object: CalendarObject,
_overwrite: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn delete_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn restore_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
fn is_read_only(&self, _cal_id: &str) -> bool {
true
}
}

View File

@@ -41,11 +41,6 @@ impl Error {
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
#[must_use]
pub const fn is_not_found(&self) -> bool {
matches!(self, Self::NotFound)
}
}
impl IntoResponse for Error {

View File

@@ -7,7 +7,6 @@ pub use error::Error;
pub mod auth;
mod calendar;
mod combined_calendar_store;
mod contact_birthday_store;
mod secret;
mod subscription_store;
pub mod synctoken;
@@ -17,8 +16,7 @@ pub mod tests;
pub use addressbook_store::AddressbookStore;
pub use calendar_store::CalendarStore;
pub use combined_calendar_store::CombinedCalendarStore;
pub use contact_birthday_store::ContactBirthdayStore;
pub use combined_calendar_store::{CombinedCalendarStore, PrefixedCalendarStore};
pub use secret::Secret;
pub use subscription_store::*;

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_store_sqlite"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -16,12 +15,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
@@ -30,3 +29,4 @@ uuid.workspace = true
pbkdf2.workspace = true
rustical_ical.workspace = true
rstest = { workspace = true, optional = true }
sha2.workspace = true

View File

@@ -0,0 +1 @@
DROP TABLE birthday_calendars;

View File

@@ -0,0 +1,26 @@
CREATE TABLE birthday_calendars (
principal TEXT NOT NULL,
id TEXT NOT NULL,
displayname TEXT,
description TEXT,
"order" INT DEFAULT 0 NOT NULL,
color TEXT,
timezone_id TEXT,
deleted_at DATETIME,
push_topic TEXT NOT NULL,
PRIMARY KEY (principal, id),
CONSTRAINT fk_birthdays_addressbooks FOREIGN KEY (principal, id)
REFERENCES addressbooks (principal, id) ON DELETE CASCADE
-- birthday calendar stores no meaningful data so we can cascade
);
INSERT INTO birthday_calendars
(principal, id, displayname, deleted_at, push_topic)
SELECT
principal,
id,
displayname || ' birthdays' AS displayname,
deleted_at,
push_topic || substr(printf('%d', random()), -4) AS push_topic
-- jank suffix to ensure that new push_topic is different :D
FROM addressbooks;

View File

@@ -0,0 +1,403 @@
use crate::addressbook_store::SqliteAddressbookStore;
use async_trait::async_trait;
use chrono::NaiveDateTime;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarMetadata, CalendarStore, CollectionMetadata,
Error, PrefixedCalendarStore,
};
use sha2::{Digest, Sha256};
use sqlx::{Executor, Sqlite};
use std::collections::HashMap;
use tracing::instrument;
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
struct BirthdayCalendarJoinRow {
principal: String,
id: String,
displayname: Option<String>,
description: Option<String>,
order: i64,
color: Option<String>,
timezone_id: Option<String>,
deleted_at: Option<NaiveDateTime>,
push_topic: String,
addr_synctoken: i64,
}
impl From<BirthdayCalendarJoinRow> for Calendar {
fn from(value: BirthdayCalendarJoinRow) -> Self {
Self {
principal: value.principal,
id: format!("{}{}", BIRTHDAYS_PREFIX, value.id),
meta: CalendarMetadata {
displayname: value.displayname,
order: value.order,
description: value.description,
color: value.color,
},
deleted_at: value.deleted_at,
components: vec![CalendarObjectType::Event],
timezone_id: value.timezone_id,
synctoken: value.addr_synctoken,
subscription_url: None,
push_topic: value.push_topic,
}
}
}
impl PrefixedCalendarStore for SqliteAddressbookStore {
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
}
impl SqliteAddressbookStore {
#[instrument]
pub async fn _get_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let cal = sqlx::query_as!(
BirthdayCalendarJoinRow,
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
FROM birthday_calendars
INNER JOIN (
SELECT principal AS addr_principal,
id AS addr_id,
synctoken AS addr_synctoken
FROM addressbooks
) ON (principal, id) = (addr_principal, addr_id)
WHERE (principal, id) = (?, ?)
AND ((deleted_at IS NULL) OR ?)
"#,
principal,
id,
show_deleted
)
.fetch_one(executor)
.await
.map_err(crate::Error::from)?;
Ok(cal.into())
}
#[instrument]
pub async fn _get_birthday_calendars<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
deleted: bool,
) -> Result<Vec<Calendar>, Error> {
Ok(
sqlx::query_as!(
BirthdayCalendarJoinRow,
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
FROM birthday_calendars
INNER JOIN (
SELECT principal AS addr_principal,
id AS addr_id,
synctoken AS addr_synctoken
FROM addressbooks
) ON (principal, id) = (addr_principal, addr_id)
WHERE principal = ?
AND (
(deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted
OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted
)
"#,
principal,
deleted,
deleted
)
.fetch_all(executor)
.await
.map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
}
#[instrument]
pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
addressbook: Addressbook,
) -> Result<(), rustical_store::Error> {
let birthday_name = addressbook
.displayname
.map(|name| format!("{name} birthdays"));
let birthday_push_topic = {
let mut hasher = Sha256::new();
hasher.update("birthdays");
hasher.update(addressbook.push_topic);
format!("{:x}", hasher.finalize())
};
sqlx::query!(
r#"INSERT INTO birthday_calendars (principal, id, displayname, push_topic)
VALUES (?, ?, ?, ?)"#,
addressbook.principal,
addressbook.id,
birthday_name,
birthday_push_topic,
)
.execute(executor)
.await
.map_err(crate::Error::from)?;
Ok(())
}
async fn _delete_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
if use_trashbin {
sqlx::query!(
r#"UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#,
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?
} else {
sqlx::query!(
r#"DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)"#,
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?
};
Ok(())
}
async fn _restore_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
) -> Result<(), Error> {
sqlx::query!(
r"UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?;
Ok(())
}
#[instrument]
async fn _update_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
calendar: &Calendar,
) -> Result<(), Error> {
let result = sqlx::query!(
r#"UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?
WHERE (principal, id) = (?, ?)"#,
calendar.principal,
calendar.id,
calendar.meta.displayname,
calendar.meta.description,
calendar.meta.order,
calendar.meta.color,
calendar.timezone_id,
calendar.push_topic,
principal,
calendar.id,
).execute(executor).await.map_err(crate::Error::from)?;
if result.rows_affected() == 0 {
return Err(rustical_store::Error::NotFound);
}
Ok(())
}
}
#[async_trait]
impl CalendarStore for SqliteAddressbookStore {
#[instrument]
async fn get_calendar(
&self,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
Self::_get_birthday_calendar(&self.db, principal, id, show_deleted).await
}
#[instrument]
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Self::_get_birthday_calendars(&self.db, principal, false).await
}
#[instrument]
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Self::_get_birthday_calendars(&self.db, principal, true).await
}
#[instrument]
async fn update_calendar(
&self,
principal: String,
id: String,
mut calendar: Calendar,
) -> Result<(), Error> {
assert_eq!(id, calendar.id);
calendar.id = calendar
.id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?
.to_string();
Self::_update_birthday_calendar(&self.db, &principal, &calendar).await
}
#[instrument]
async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn delete_calendar(
&self,
principal: &str,
id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
return Ok(());
};
Self::_delete_birthday_calendar(&self.db, principal, id, use_trashbin).await
}
#[instrument]
async fn restore_calendar(&self, principal: &str, id: &str) -> Result<(), Error> {
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
return Err(Error::NotFound);
};
Self::_restore_birthday_calendar(&self.db, principal, id).await
}
#[instrument]
async fn import_calendar(
&self,
_calendar: Calendar,
_objects: Vec<CalendarObject>,
_merge_existing: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn sync_changes(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (objects, deleted_objects, new_synctoken) =
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
.iter()
.map(AddressObject::get_birthday_object)
.collect();
let objects = objects?.into_iter().flatten().collect();
Ok((objects, deleted_objects, new_synctoken))
}
#[instrument]
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<CollectionMetadata, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
self.addressbook_metadata(principal, cal_id).await
}
#[instrument]
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
AddressbookStore::get_objects(self, principal, cal_id)
.await?
.iter()
.map(AddressObject::get_significant_dates)
.collect();
let objects = objects?
.into_iter()
.flat_map(HashMap::into_values)
.collect();
Ok(objects)
}
#[instrument]
async fn get_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
.await?
.get_significant_dates()?
.remove(date_type)
.ok_or(Error::NotFound)
}
#[instrument]
async fn put_object(
&self,
_principal: String,
_cal_id: String,
_object: CalendarObject,
_overwrite: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn delete_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn restore_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
fn is_read_only(&self, _cal_id: &str) -> bool {
true
}
}

View File

@@ -9,7 +9,9 @@ use rustical_store::{
};
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender;
use tracing::{error, instrument, warn};
use tracing::{error, instrument};
pub mod birthday_calendar;
#[derive(Debug, Clone)]
struct AddressObjectRow {
@@ -32,60 +34,6 @@ 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,
@@ -144,9 +92,9 @@ impl SqliteAddressbookStore {
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
addressbook: &Addressbook,
principal: String,
id: String,
addressbook: Addressbook,
) -> Result<(), rustical_store::Error> {
let result = sqlx::query!(
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
@@ -451,7 +399,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]
@@ -459,7 +407,15 @@ impl AddressbookStore for SqliteAddressbookStore {
&self,
addressbook: Addressbook,
) -> Result<(), rustical_store::Error> {
Self::_insert_addressbook(&self.db, &addressbook).await
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
Self::_insert_birthday_calendar(&mut *tx, addressbook).await?;
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
#[instrument]
@@ -675,7 +631,7 @@ impl AddressbookStore for SqliteAddressbookStore {
.await?
.push_topic,
}) {
error!("Push notification about restored addressbook object failed: {err}");
error!("Push notification about deleted addressbook failed: {err}");
}
Ok(())
@@ -709,7 +665,6 @@ 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,
@@ -719,31 +674,9 @@ 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(())
}
}
@@ -755,7 +688,7 @@ async fn log_object_operation(
addressbook_id: &str,
object_id: &str,
operation: ChangeOperation,
) -> Result<String, Error> {
) -> Result<String, sqlx::Error> {
struct Synctoken {
synctoken: i64,
}
@@ -770,8 +703,7 @@ async fn log_object_operation(
addressbook_id
)
.fetch_one(&mut **tx)
.await
.map_err(crate::Error::from)?;
.await?;
sqlx::query!(
r#"
@@ -785,7 +717,6 @@ async fn log_object_operation(
operation
)
.execute(&mut **tx)
.await
.map_err(crate::Error::from)?;
.await?;
Ok(format_synctoken(synctoken))
}

View File

@@ -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, warn};
use tracing::{error, instrument};
#[derive(Debug, Clone)]
struct CalendarObjectRow {
@@ -94,53 +94,6 @@ 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,
@@ -398,9 +351,9 @@ impl SqliteCalendarStore {
#[instrument]
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
cal_id: &str,
object: &CalendarObject,
principal: String,
cal_id: String,
object: CalendarObject,
overwrite: bool,
) -> Result<(), Error> {
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
@@ -647,35 +600,18 @@ 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, &calendar.id, &object, false).await?;
sync_token = Some(
log_object_operation(
&mut tx,
&calendar.principal,
&calendar.id,
object.get_id(),
ChangeOperation::Add,
)
.await?,
);
Self::_put_object(
&mut *tx,
calendar.principal.clone(),
calendar.id.clone(),
object,
false,
)
.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(())
}
@@ -753,7 +689,14 @@ impl CalendarStore for SqliteCalendarStore {
return Err(Error::ReadOnly);
}
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
Self::_put_object(
&mut *tx,
principal.clone(),
cal_id.clone(),
object,
overwrite,
)
.await?;
let sync_token = log_object_operation(
&mut tx,
@@ -831,7 +774,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 restored calendar object failed: {err}");
error!("Push notification about deleted calendar failed: {err}");
}
Ok(())
}
@@ -852,7 +795,6 @@ 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,

View File

@@ -1,81 +0,0 @@
#[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());
}
}

View File

@@ -1,76 +0,0 @@
#[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());
}
}

View File

@@ -8,9 +8,6 @@ use tokio::sync::OnceCell;
static DB: OnceCell<SqlitePool> = 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();

View File

@@ -1,7 +1,6 @@
[package]
name = "rustical_xml"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true

View File

@@ -1,8 +1,7 @@
[package]
name = "xml_derive"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
version = "0.1.0"
edition = "2024"
license.workspace = true
[lib]

View File

@@ -1 +0,0 @@
google-site-verification: googlec55e08580a46745c.html

View File

@@ -16,7 +16,8 @@ use rustical_frontend::{FrontendConfig, frontend_router};
use rustical_oidc::OidcConfig;
use rustical_store::auth::AuthenticationProvider;
use rustical_store::{
AddressbookStore, CalendarStore, CombinedCalendarStore, ContactBirthdayStore, SubscriptionStore,
AddressbookStore, CalendarStore, CombinedCalendarStore, PrefixedCalendarStore,
SubscriptionStore,
};
use std::sync::Arc;
use std::time::Duration;
@@ -33,7 +34,11 @@ use tracing::field::display;
clippy::too_many_lines,
clippy::cognitive_complexity
)]
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
pub fn make_app<
AS: AddressbookStore + PrefixedCalendarStore,
CS: CalendarStore,
S: SubscriptionStore,
>(
addr_store: Arc<AS>,
cal_store: Arc<CS>,
subscription_store: Arc<S>,
@@ -45,7 +50,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
session_cookie_samesite_strict: bool,
payload_limit_mb: usize,
) -> Router<()> {
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone()));
let birthday_store = addr_store.clone();
let combined_cal_store =
Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store));
@@ -173,9 +178,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
tracing::debug!("unauthorized");
}
StatusCode::NOT_FOUND => {
// Clients like GNOME Calendar will try to reach /remote.php/webdav
// quite often clogging up the logs
tracing::info!("client error");
tracing::warn!("client error");
}
_ => {
tracing::error!("client error");

View File

@@ -13,7 +13,9 @@ use figment::Figment;
use figment::providers::{Env, Format, Toml};
use rustical_dav_push::DavPushController;
use rustical_store::auth::AuthenticationProvider;
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
use rustical_store::{
AddressbookStore, CalendarStore, CollectionOperation, PrefixedCalendarStore, SubscriptionStore,
};
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
use rustical_store_sqlite::principal_store::SqlitePrincipalStore;
@@ -56,7 +58,7 @@ async fn get_data_stores(
migrate: bool,
config: &DataStoreConfig,
) -> Result<(
Arc<impl AddressbookStore>,
Arc<impl AddressbookStore + PrefixedCalendarStore>,
Arc<impl CalendarStore>,
Arc<impl SubscriptionStore>,
Arc<impl AuthenticationProvider>,
@@ -69,9 +71,7 @@ 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));
(