mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
Compare commits
33 Commits
873b40ad10
...
v0.11.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d81074de3b | ||
|
|
42386adcfa | ||
|
|
d2f5f7c89b | ||
|
|
15e431ce12 | ||
|
|
96a16951f4 | ||
|
|
a32b766c0c | ||
|
|
7a101b7364 | ||
|
|
57275a10b4 | ||
|
|
af239e34bf | ||
|
|
e99b1d9123 | ||
|
|
e39657eb29 | ||
|
|
607db62859 | ||
|
|
eba377b980 | ||
|
|
d5c1ddc590 | ||
|
|
a79e1901b8 | ||
|
|
f29c8fa925 | ||
|
|
54f1ee0788 | ||
|
|
96f221f721 | ||
|
|
ba3b64a9c4 | ||
|
|
22a0337375 | ||
|
|
21902e108a | ||
|
|
08f526fa5b | ||
|
|
ac73f3aaff | ||
|
|
9fdc8434db | ||
|
|
85f3d89235 | ||
|
|
092604694a | ||
|
|
8ef24668ba | ||
|
|
416658d069 | ||
|
|
80eae5db9e | ||
|
|
66f541f1c7 | ||
|
|
ea7196501e | ||
|
|
33d14a9ba0 | ||
|
|
d843909084 |
38
.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json
generated
Normal file
38
.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
12
.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
generated
Normal file
12
.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO birthday_calendars (principal, id, displayname, description, \"order\", color, push_topic)\n VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 7
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a"
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO birthday_calendars (principal, id, displayname, push_topic)\n VALUES (?, ?, ?, ?)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 4
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "bfdf662cd03e741b7a36f5e2ac01d32ac367c52ce41bd70394f754248b29749c"
|
|
||||||
}
|
|
||||||
38
.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json
generated
Normal file
38
.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
459
Cargo.lock
generated
459
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
50
Cargo.toml
@@ -2,7 +2,8 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.10.1"
|
version = "0.11.1"
|
||||||
|
rust-version = "1.91"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A CalDAV server"
|
description = "A CalDAV server"
|
||||||
documentation = "https://lennart-k.github.io/rustical/"
|
documentation = "https://lennart-k.github.io/rustical/"
|
||||||
@@ -12,6 +13,7 @@ license = "AGPL-3.0-or-later"
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical"
|
name = "rustical"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
@@ -35,6 +37,17 @@ opentelemetry = [
|
|||||||
debug = 0
|
debug = 0
|
||||||
|
|
||||||
[workspace.dependencies]
|
[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"
|
matchit = "0.9"
|
||||||
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
@@ -108,16 +121,6 @@ tower-http = { version = "0.6", features = [
|
|||||||
"catch-panic",
|
"catch-panic",
|
||||||
] }
|
] }
|
||||||
percent-encoding = "2.3"
|
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-tz = "0.10"
|
||||||
chrono-humanize = "0.2"
|
chrono-humanize = "0.2"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
@@ -130,7 +133,7 @@ syn = { version = "2.0", features = ["full"] }
|
|||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
heck = "0.5"
|
heck = "0.5"
|
||||||
darling = "0.21"
|
darling = "0.23"
|
||||||
reqwest = { version = "0.12", features = [
|
reqwest = { version = "0.12", features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"charset",
|
"charset",
|
||||||
@@ -145,21 +148,22 @@ ece = { version = "2.3", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
async-std = { version = "1.13", features = ["attributes"] }
|
async-std = { version = "1.13", features = ["attributes"] }
|
||||||
|
similar-asserts = "1.7"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_store = { workspace = true }
|
rustical_store.workspace = true
|
||||||
rustical_store_sqlite = { workspace = true }
|
rustical_store_sqlite.workspace = true
|
||||||
rustical_caldav = { workspace = true }
|
rustical_caldav.workspace = true
|
||||||
rustical_carddav.workspace = true
|
rustical_carddav.workspace = true
|
||||||
rustical_frontend = { workspace = true }
|
rustical_frontend.workspace = true
|
||||||
toml = { workspace = true }
|
toml.workspace = true
|
||||||
serde = { workspace = true }
|
serde.workspace = true
|
||||||
tokio = { workspace = true }
|
tokio.workspace = true
|
||||||
tracing = { workspace = true }
|
tracing.workspace = true
|
||||||
anyhow = { workspace = true }
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
sqlx = { workspace = true }
|
sqlx.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_caldav"
|
name = "rustical_caldav"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
@@ -17,21 +18,21 @@ serde_json.workspace = true
|
|||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
axum-extra.workspace = true
|
axum-extra.workspace = true
|
||||||
tower.workspace = true
|
tower.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait.workspace = true
|
||||||
thiserror = { workspace = true }
|
thiserror.workspace = true
|
||||||
quick-xml = { workspace = true }
|
quick-xml.workspace = true
|
||||||
tracing = { workspace = true }
|
tracing.workspace = true
|
||||||
futures-util = { workspace = true }
|
futures-util.workspace = true
|
||||||
derive_more = { workspace = true }
|
derive_more.workspace = true
|
||||||
base64 = { workspace = true }
|
base64.workspace = true
|
||||||
serde = { workspace = true }
|
serde.workspace = true
|
||||||
tokio = { workspace = true }
|
tokio.workspace = true
|
||||||
url = { workspace = true }
|
url.workspace = true
|
||||||
rustical_dav = { workspace = true }
|
rustical_dav.workspace = true
|
||||||
rustical_store = { workspace = true }
|
rustical_store.workspace = true
|
||||||
chrono = { workspace = true }
|
chrono.workspace = true
|
||||||
chrono-tz = { workspace = true }
|
chrono-tz.workspace = true
|
||||||
sha2 = { workspace = true }
|
sha2.workspace = true
|
||||||
ical.workspace = true
|
ical.workspace = true
|
||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
@@ -44,3 +45,4 @@ tower-http.workspace = true
|
|||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
vtimezones-rs.workspace = true
|
vtimezones-rs.workspace = true
|
||||||
|
similar-asserts.workspace = true
|
||||||
|
|||||||
@@ -26,16 +26,18 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
|||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
for href in &cal_query.href {
|
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('/');
|
let filename = filename.trim_start_matches('/');
|
||||||
if let Some(object_id) = filename.strip_suffix(".ics") {
|
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||||
match store.get_object(principal, cal_id, object_id, false).await {
|
match store.get_object(principal, cal_id, object_id, false).await {
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push(object),
|
||||||
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()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_string());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_owned());
|
||||||
|
|||||||
@@ -188,9 +188,6 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
|
fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
|
||||||
if self.read_only {
|
|
||||||
return Err(rustical_dav::Error::PropReadOnly);
|
|
||||||
}
|
|
||||||
match prop {
|
match prop {
|
||||||
CalendarPropWrapper::Calendar(prop) => match prop {
|
CalendarPropWrapper::Calendar(prop) => match prop {
|
||||||
CalendarProp::CalendarColor(color) => {
|
CalendarProp::CalendarColor(color) => {
|
||||||
@@ -263,9 +260,6 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
|
fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
|
||||||
if self.read_only {
|
|
||||||
return Err(rustical_dav::Error::PropReadOnly);
|
|
||||||
}
|
|
||||||
match prop {
|
match prop {
|
||||||
CalendarPropWrapperName::Calendar(prop) => match prop {
|
CalendarPropWrapperName::Calendar(prop) => match prop {
|
||||||
CalendarPropName::CalendarColor => {
|
CalendarPropName::CalendarColor => {
|
||||||
|
|||||||
@@ -211,6 +211,9 @@ END:VCALENDAR
|
|||||||
<privilege>
|
<privilege>
|
||||||
<read/>
|
<read/>
|
||||||
</privilege>
|
</privilege>
|
||||||
|
<privilege>
|
||||||
|
<write-properties/>
|
||||||
|
</privilege>
|
||||||
<privilege>
|
<privilege>
|
||||||
<read-acl/>
|
<read-acl/>
|
||||||
</privilege>
|
</privilege>
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ async fn test_propfind() {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.trim()
|
.trim()
|
||||||
.replace("\r\n", "\n");
|
.replace("\r\n", "\n");
|
||||||
println!("{output}");
|
similar_asserts::assert_eq!(expected_output, output);
|
||||||
println!("{}, {} \n\n\n", output.len(), expected_output.len());
|
|
||||||
assert_eq!(output, expected_output);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_carddav"
|
name = "rustical_carddav"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
@@ -11,19 +12,19 @@ publish = false
|
|||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
axum-extra.workspace = true
|
axum-extra.workspace = true
|
||||||
tower.workspace = true
|
tower.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait.workspace = true
|
||||||
thiserror = { workspace = true }
|
thiserror.workspace = true
|
||||||
quick-xml = { workspace = true }
|
quick-xml.workspace = true
|
||||||
tracing = { workspace = true }
|
tracing.workspace = true
|
||||||
futures-util = { workspace = true }
|
futures-util.workspace = true
|
||||||
derive_more = { workspace = true }
|
derive_more.workspace = true
|
||||||
base64 = { workspace = true }
|
base64.workspace = true
|
||||||
serde = { workspace = true }
|
serde.workspace = true
|
||||||
tokio = { workspace = true }
|
tokio.workspace = true
|
||||||
url = { workspace = true }
|
url.workspace = true
|
||||||
rustical_dav = { workspace = true }
|
rustical_dav.workspace = true
|
||||||
rustical_store = { workspace = true }
|
rustical_store.workspace = true
|
||||||
chrono = { workspace = true }
|
chrono.workspace = true
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
rustical_dav_push.workspace = true
|
rustical_dav_push.workspace = true
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
for href in &addressbook_multiget.href {
|
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('/');
|
let filename = filename.trim_start_matches('/');
|
||||||
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
||||||
match store
|
match store
|
||||||
@@ -42,11 +44,11 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(object) => result.push(object),
|
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()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_string());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_owned());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_dav"
|
name = "rustical_dav"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ pub async fn route_proppatch<R: ResourceService>(
|
|||||||
.get_resource(path_components, false)
|
.get_resource(path_components, false)
|
||||||
.await?;
|
.await?;
|
||||||
let privileges = resource.get_user_privileges(principal)?;
|
let privileges = resource.get_user_privileges(principal)?;
|
||||||
if !privileges.has(&UserPrivilege::Write) {
|
if !privileges.has(&UserPrivilege::WriteProperties) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_dav_push"
|
name = "rustical_dav_push"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
@@ -9,15 +10,15 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait.workspace = true
|
||||||
futures-util = { workspace = true }
|
futures-util.workspace = true
|
||||||
quick-xml = { workspace = true }
|
quick-xml.workspace = true
|
||||||
serde = { workspace = true }
|
serde.workspace = true
|
||||||
thiserror = { workspace = true }
|
thiserror.workspace = true
|
||||||
itertools = { workspace = true }
|
itertools.workspace = true
|
||||||
log = { workspace = true }
|
log.workspace = true
|
||||||
derive_more = { workspace = true }
|
derive_more.workspace = true
|
||||||
tracing = { workspace = true }
|
tracing.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
rustical_dav.workspace = true
|
rustical_dav.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_frontend"
|
name = "rustical_frontend"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
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-birthday-calendar-form")
|
||||||
|
export class CreateCalendarForm extends LitElement {
|
||||||
|
protected override createRenderRoot() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
principal: string = ''
|
||||||
|
@property()
|
||||||
|
addr_id: string = ''
|
||||||
|
@property()
|
||||||
|
displayname: string = ''
|
||||||
|
@property()
|
||||||
|
description: string = ''
|
||||||
|
@property()
|
||||||
|
color: string = ''
|
||||||
|
|
||||||
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
@property()
|
||||||
|
timezones: Array<String> = []
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
|
||||||
|
<dialog ${ref(this.dialog)}>
|
||||||
|
<h3>Create calendar</h3>
|
||||||
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
<label>
|
||||||
|
Displayname
|
||||||
|
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<input type="text" name="description" @change=${e => this.description = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Color
|
||||||
|
<input type="color" name="color" @change=${e => this.color = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!this.addr_id) {
|
||||||
|
alert("Empty id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.displayname) {
|
||||||
|
alert("Empty displayname")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
|
||||||
|
method: 'MKCOL',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml'
|
||||||
|
},
|
||||||
|
body: `
|
||||||
|
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
|
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
|
||||||
|
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
|
||||||
|
<CAL:supported-calendar-component-set>
|
||||||
|
<CAL:comp name="VEVENT" />
|
||||||
|
</CAL:supported-calendar-component-set>
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
</mkcol>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
window.location.reload()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'create-calendar-form': CreateCalendarForm
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { html, LitElement } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
import { escapeXml } from ".";
|
import { escapeXml } from ".";
|
||||||
|
import { getTimezones } from "./timezones.ts";
|
||||||
|
|
||||||
@customElement("create-calendar-form")
|
@customElement("create-calendar-form")
|
||||||
export class CreateCalendarForm extends LitElement {
|
export class CreateCalendarForm extends LitElement {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: [
|
input: [
|
||||||
|
"lib/create-birthday-calendar-form.ts",
|
||||||
"lib/create-calendar-form.ts",
|
"lib/create-calendar-form.ts",
|
||||||
"lib/edit-calendar-form.ts",
|
"lib/edit-calendar-form.ts",
|
||||||
"lib/import-calendar-form.ts",
|
"lib/import-calendar-form.ts",
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
let CreateCalendarForm = class extends i {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.principal = "";
|
||||||
|
this.addr_id = "";
|
||||||
|
this.displayname = "";
|
||||||
|
this.description = "";
|
||||||
|
this.color = "";
|
||||||
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
|
this.timezones = [];
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return x`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
|
||||||
|
<dialog ${n(this.dialog)}>
|
||||||
|
<h3>Create calendar</h3>
|
||||||
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
<label>
|
||||||
|
Displayname
|
||||||
|
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Color
|
||||||
|
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
<button type="submit" @click=${(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.dialog.value.close();
|
||||||
|
this.form.value.reset();
|
||||||
|
}} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
async submit(e2) {
|
||||||
|
e2.preventDefault();
|
||||||
|
if (!this.addr_id) {
|
||||||
|
alert("Empty id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.displayname) {
|
||||||
|
alert("Empty displayname");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
|
||||||
|
method: "MKCOL",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
},
|
||||||
|
body: `
|
||||||
|
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
|
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
|
||||||
|
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
|
||||||
|
<CAL:supported-calendar-component-set>
|
||||||
|
<CAL:comp name="VEVENT" />
|
||||||
|
</CAL:supported-calendar-component-set>
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
</mkcol>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "principal", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "addr_id", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "displayname", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "description", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "color", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "timezones", 2);
|
||||||
|
CreateCalendarForm = __decorateClass([
|
||||||
|
t("create-birthday-calendar-form")
|
||||||
|
], CreateCalendarForm);
|
||||||
|
export {
|
||||||
|
CreateCalendarForm
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
|
|||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BwbQvJBB.mjs";
|
||||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||||
|
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
|||||||
@@ -2,18 +2,7 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
|
|||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BwbQvJBB.mjs";
|
||||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||||
let timezonesPromise = null;
|
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
|
||||||
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 __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
|||||||
15
crates/frontend/public/assets/js/timezones-B0vBBzCP.mjs
Normal file
15
crates/frontend/public/assets/js/timezones-B0vBBzCP.mjs
Normal file
@@ -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
|
||||||
|
};
|
||||||
@@ -53,6 +53,11 @@ a {
|
|||||||
color: var(--text-on-background-color);
|
color: var(--text-on-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: var(--background-darker);
|
background: var(--background-darker);
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
@@ -239,10 +244,8 @@ ul.collection-list {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
a,
|
|
||||||
button {
|
button {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<h2>{{user.id }}'s Addressbooks</h2>
|
<h2>{{user.id }}'s Addressbooks</h2>
|
||||||
<ul class="collection-list">
|
<ul class="collection-list">
|
||||||
{% for (meta, addressbook) in addressbooks %}
|
{% for (meta, birthday_cal, addressbook) in addressbooks %}
|
||||||
<li class="collection-list-item">
|
<li class="collection-list-item">
|
||||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
@@ -24,6 +24,13 @@
|
|||||||
></edit-addressbook-form>
|
></edit-addressbook-form>
|
||||||
<delete-button trash
|
<delete-button trash
|
||||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||||
|
{% if !birthday_cal.is_some() %}
|
||||||
|
<create-birthday-calendar-form
|
||||||
|
principal="{{ addressbook.principal }}"
|
||||||
|
addr_id="{{ addressbook.id }}"
|
||||||
|
displayname="{{ addressbook.displayname.as_deref().unwrap_or_default() }} birthdays"
|
||||||
|
></create-birthday-calendar-form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="metadata">
|
<div class="metadata">
|
||||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||||
@@ -37,7 +44,7 @@
|
|||||||
{%if !deleted_addressbooks.is_empty() %}
|
{%if !deleted_addressbooks.is_empty() %}
|
||||||
<h3>Deleted Addressbooks</h3>
|
<h3>Deleted Addressbooks</h3>
|
||||||
<ul class="collection-list">
|
<ul class="collection-list">
|
||||||
{% for (meta, addressbook) in deleted_addressbooks %}
|
{% for (meta, birthday_cal, addressbook) in deleted_addressbooks %}
|
||||||
<li class="collection-list-item">
|
<li class="collection-list-item">
|
||||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
|
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="/frontend/assets/js/create-birthday-calendar-form.mjs" async></script>
|
||||||
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
|
||||||
<script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script>
|
||||||
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use http::{Method, StatusCode};
|
|||||||
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
|
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
|
||||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
AddressbookStore, CalendarStore,
|
AddressbookStore, CalendarStore, PrefixedCalendarStore,
|
||||||
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
|
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -39,7 +39,11 @@ use crate::routes::{
|
|||||||
#[cfg(not(feature = "dev"))]
|
#[cfg(not(feature = "dev"))]
|
||||||
use assets::{Assets, EmbedService};
|
use assets::{Assets, EmbedService};
|
||||||
|
|
||||||
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
pub fn frontend_router<
|
||||||
|
AP: AuthenticationProvider,
|
||||||
|
CS: CalendarStore,
|
||||||
|
AS: AddressbookStore + PrefixedCalendarStore,
|
||||||
|
>(
|
||||||
prefix: &'static str,
|
prefix: &'static str,
|
||||||
auth_provider: Arc<AP>,
|
auth_provider: Arc<AP>,
|
||||||
cal_store: Arc<CS>,
|
cal_store: Arc<CS>,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_web::WebTemplate;
|
use askama_web::WebTemplate;
|
||||||
use axum::{Extension, extract::Path, response::IntoResponse};
|
use axum::{Extension, extract::Path, response::IntoResponse};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
|
use rustical_store::{
|
||||||
|
Addressbook, AddressbookStore, Calendar, CollectionMetadata, PrefixedCalendarStore,
|
||||||
|
auth::Principal,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::pages::user::{Section, UserPage};
|
use crate::pages::user::{Section, UserPage};
|
||||||
|
|
||||||
@@ -18,11 +20,11 @@ impl Section for AddressbooksSection {
|
|||||||
#[template(path = "components/sections/addressbooks_section.html")]
|
#[template(path = "components/sections/addressbooks_section.html")]
|
||||||
pub struct AddressbooksSection {
|
pub struct AddressbooksSection {
|
||||||
pub user: Principal,
|
pub user: Principal,
|
||||||
pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
pub addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
|
||||||
pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
pub deleted_addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn route_addressbooks<AS: AddressbookStore>(
|
pub async fn route_addressbooks<AS: AddressbookStore + PrefixedCalendarStore>(
|
||||||
Path(user_id): Path<String>,
|
Path(user_id): Path<String>,
|
||||||
Extension(addr_store): Extension<Arc<AS>>,
|
Extension(addr_store): Extension<Arc<AS>>,
|
||||||
user: Principal,
|
user: Principal,
|
||||||
@@ -43,22 +45,42 @@ pub async fn route_addressbooks<AS: AddressbookStore>(
|
|||||||
|
|
||||||
let mut addressbook_infos = vec![];
|
let mut addressbook_infos = vec![];
|
||||||
for addressbook in addressbooks {
|
for addressbook in addressbooks {
|
||||||
|
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
|
||||||
|
let birthday_cal = match addr_store
|
||||||
|
.get_calendar(&addressbook.principal, &birthday_id, true)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(cal) => Some(cal),
|
||||||
|
Err(rustical_store::Error::NotFound) => None,
|
||||||
|
err => Some(err.unwrap()),
|
||||||
|
};
|
||||||
addressbook_infos.push((
|
addressbook_infos.push((
|
||||||
addr_store
|
addr_store
|
||||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
birthday_cal,
|
||||||
addressbook,
|
addressbook,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut deleted_addressbook_infos = vec![];
|
let mut deleted_addressbook_infos = vec![];
|
||||||
for addressbook in deleted_addressbooks {
|
for addressbook in deleted_addressbooks {
|
||||||
|
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
|
||||||
|
let birthday_cal = match addr_store
|
||||||
|
.get_calendar(&addressbook.principal, &birthday_id, true)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(cal) => Some(cal),
|
||||||
|
Err(rustical_store::Error::NotFound) => None,
|
||||||
|
err => Some(err.unwrap()),
|
||||||
|
};
|
||||||
deleted_addressbook_infos.push((
|
deleted_addressbook_infos.push((
|
||||||
addr_store
|
addr_store
|
||||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
birthday_cal,
|
||||||
addressbook,
|
addressbook,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_ical"
|
name = "rustical_ical"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use axum::{http::StatusCode, response::IntoResponse};
|
|||||||
|
|
||||||
use crate::CalDateTimeError;
|
use crate::CalDateTimeError;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Invalid ics/vcf input: {0}")]
|
#[error("Invalid ics/vcf input: {0}")]
|
||||||
InvalidData(String),
|
InvalidData(String),
|
||||||
|
|||||||
@@ -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";
|
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
|
||||||
pub const LOCAL_DATE: &str = "%Y%m%d";
|
pub const LOCAL_DATE: &str = "%Y%m%d";
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||||
pub enum CalDateTimeError {
|
pub enum CalDateTimeError {
|
||||||
#[error(
|
#[error(
|
||||||
"Timezone has X-LIC-LOCATION property to specify a timezone from the Olson database, however its value {0} is invalid"
|
"Timezone has X-LIC-LOCATION property to specify a timezone from the Olson database, however its value {0} is invalid"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_oidc"
|
name = "rustical_oidc"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_store"
|
name = "rustical_store"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
@@ -8,15 +9,16 @@ license.workspace = true
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait.workspace = true
|
||||||
serde = { workspace = true }
|
serde.workspace = true
|
||||||
ical = { workspace = true }
|
sha2.workspace = true
|
||||||
chrono = { workspace = true }
|
ical.workspace = true
|
||||||
regex = { workspace = true }
|
chrono.workspace = true
|
||||||
thiserror = { workspace = true }
|
regex.workspace = true
|
||||||
tracing = { workspace = true }
|
thiserror.workspace = true
|
||||||
chrono-tz = { workspace = true }
|
tracing.workspace = true
|
||||||
|
chrono-tz.workspace = true
|
||||||
derive_more = { workspace = true, features = ["as_ref"] }
|
derive_more = { workspace = true, features = ["as_ref"] }
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
@@ -33,7 +35,7 @@ tower-sessions.workspace = true
|
|||||||
vtimezones-rs.workspace = true
|
vtimezones-rs.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = { workspace = true }
|
rstest.workspace = true
|
||||||
rstest_reuse = { workspace = true }
|
rstest_reuse.workspace = true
|
||||||
rustical_store_sqlite.workspace = true
|
rustical_store_sqlite.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::synctoken::format_synctoken;
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||||
pub struct Addressbook {
|
pub struct Addressbook {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub principal: String,
|
pub principal: String,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use rustical_ical::CalendarObjectType;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct CalendarMetadata {
|
pub struct CalendarMetadata {
|
||||||
// Attributes that may be outsourced
|
// Attributes that may be outsourced
|
||||||
pub displayname: Option<String>,
|
pub displayname: Option<String>,
|
||||||
@@ -13,7 +13,7 @@ pub struct CalendarMetadata {
|
|||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct Calendar {
|
pub struct Calendar {
|
||||||
// Attributes that may be outsourced
|
// Attributes that may be outsourced
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ impl Error {
|
|||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_not_found(&self) -> bool {
|
||||||
|
matches!(self, Self::NotFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
impl IntoResponse for Error {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_store_sqlite"
|
name = "rustical_store_sqlite"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
@@ -15,12 +16,12 @@ rstest.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
rustical_store = { workspace = true }
|
rustical_store.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait.workspace = true
|
||||||
serde = { workspace = true }
|
serde.workspace = true
|
||||||
sqlx = { workspace = true }
|
sqlx.workspace = true
|
||||||
thiserror = { workspace = true }
|
thiserror.workspace = true
|
||||||
tracing = { workspace = true }
|
tracing.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
password-auth.workspace = true
|
password-auth.workspace = true
|
||||||
|
|||||||
@@ -115,28 +115,56 @@ impl SqliteAddressbookStore {
|
|||||||
.map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
|
.map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[must_use]
|
||||||
pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
pub fn default_birthday_calendar(addressbook: Addressbook) -> Calendar {
|
||||||
executor: E,
|
|
||||||
addressbook: Addressbook,
|
|
||||||
) -> Result<(), rustical_store::Error> {
|
|
||||||
let birthday_name = addressbook
|
let birthday_name = addressbook
|
||||||
.displayname
|
.displayname
|
||||||
|
.as_ref()
|
||||||
.map(|name| format!("{name} birthdays"));
|
.map(|name| format!("{name} birthdays"));
|
||||||
let birthday_push_topic = {
|
let birthday_push_topic = {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update("birthdays");
|
hasher.update("birthdays");
|
||||||
hasher.update(addressbook.push_topic);
|
hasher.update(&addressbook.push_topic);
|
||||||
format!("{:x}", hasher.finalize())
|
format!("{:x}", hasher.finalize())
|
||||||
};
|
};
|
||||||
|
Calendar {
|
||||||
|
principal: addressbook.principal,
|
||||||
|
meta: CalendarMetadata {
|
||||||
|
displayname: birthday_name,
|
||||||
|
order: 0,
|
||||||
|
description: None,
|
||||||
|
color: None,
|
||||||
|
},
|
||||||
|
id: format!("{}{}", Self::PREFIX, addressbook.id),
|
||||||
|
components: vec![CalendarObjectType::Event],
|
||||||
|
timezone_id: None,
|
||||||
|
deleted_at: None,
|
||||||
|
synctoken: Default::default(),
|
||||||
|
subscription_url: None,
|
||||||
|
push_topic: birthday_push_topic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
calendar: &Calendar,
|
||||||
|
) -> Result<(), rustical_store::Error> {
|
||||||
|
let id = calendar
|
||||||
|
.id
|
||||||
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
|
.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO birthday_calendars (principal, id, displayname, push_topic)
|
r#"INSERT INTO birthday_calendars (principal, id, displayname, description, "order", color, push_topic)
|
||||||
VALUES (?, ?, ?, ?)"#,
|
VALUES (?, ?, ?, ?, ?, ?, ?)"#,
|
||||||
addressbook.principal,
|
calendar.principal,
|
||||||
addressbook.id,
|
id,
|
||||||
birthday_name,
|
calendar.meta.displayname,
|
||||||
birthday_push_topic,
|
calendar.meta.description,
|
||||||
|
calendar.meta.order,
|
||||||
|
calendar.meta.color,
|
||||||
|
calendar.push_topic,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
@@ -255,8 +283,8 @@ impl CalendarStore for SqliteAddressbookStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
|
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> {
|
||||||
Err(Error::ReadOnly)
|
Self::_insert_birthday_calendar(&self.db, &calendar).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use rustical_store::{
|
|||||||
};
|
};
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument, warn};
|
||||||
|
|
||||||
pub mod birthday_calendar;
|
pub mod birthday_calendar;
|
||||||
|
|
||||||
@@ -34,6 +34,60 @@ pub struct SqliteAddressbookStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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>>(
|
async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -92,9 +146,9 @@ impl SqliteAddressbookStore {
|
|||||||
|
|
||||||
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: String,
|
principal: &str,
|
||||||
id: String,
|
id: &str,
|
||||||
addressbook: Addressbook,
|
addressbook: &Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
|
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
|
||||||
@@ -399,7 +453,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
id: String,
|
id: String,
|
||||||
addressbook: Addressbook,
|
addressbook: Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
Self::_update_addressbook(&self.db, principal, id, addressbook).await
|
Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@@ -413,7 +467,8 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
.await
|
.await
|
||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
||||||
Self::_insert_birthday_calendar(&mut *tx, addressbook).await?;
|
let birthday_cal = Self::default_birthday_calendar(addressbook);
|
||||||
|
Self::_insert_birthday_calendar(&mut *tx, &birthday_cal).await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -631,7 +686,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
error!("Push notification about restored addressbook object failed: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -665,6 +720,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut sync_token = None;
|
||||||
for object in objects {
|
for object in objects {
|
||||||
Self::_put_object(
|
Self::_put_object(
|
||||||
&mut *tx,
|
&mut *tx,
|
||||||
@@ -674,9 +730,31 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await?;
|
.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)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,7 +766,7 @@ async fn log_object_operation(
|
|||||||
addressbook_id: &str,
|
addressbook_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
operation: ChangeOperation,
|
operation: ChangeOperation,
|
||||||
) -> Result<String, sqlx::Error> {
|
) -> Result<String, Error> {
|
||||||
struct Synctoken {
|
struct Synctoken {
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
}
|
}
|
||||||
@@ -703,7 +781,8 @@ async fn log_object_operation(
|
|||||||
addressbook_id
|
addressbook_id
|
||||||
)
|
)
|
||||||
.fetch_one(&mut **tx)
|
.fetch_one(&mut **tx)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
@@ -717,6 +796,7 @@ async fn log_object_operation(
|
|||||||
operation
|
operation
|
||||||
)
|
)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
Ok(format_synctoken(synctoken))
|
Ok(format_synctoken(synctoken))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
|||||||
use sqlx::types::chrono::NaiveDateTime;
|
use sqlx::types::chrono::NaiveDateTime;
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument, warn};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct CalendarObjectRow {
|
struct CalendarObjectRow {
|
||||||
@@ -94,6 +94,53 @@ pub struct SqliteCalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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>>(
|
async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -351,9 +398,9 @@ impl SqliteCalendarStore {
|
|||||||
#[instrument]
|
#[instrument]
|
||||||
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: String,
|
principal: &str,
|
||||||
cal_id: String,
|
cal_id: &str,
|
||||||
object: CalendarObject,
|
object: &CalendarObject,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
let (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?;
|
Self::_insert_calendar(&mut *tx, calendar.clone()).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut sync_token = None;
|
||||||
for object in objects {
|
for object in objects {
|
||||||
Self::_put_object(
|
Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?;
|
||||||
&mut *tx,
|
|
||||||
calendar.principal.clone(),
|
sync_token = Some(
|
||||||
calendar.id.clone(),
|
log_object_operation(
|
||||||
object,
|
&mut tx,
|
||||||
false,
|
&calendar.principal,
|
||||||
)
|
&calendar.id,
|
||||||
.await?;
|
object.get_id(),
|
||||||
|
ChangeOperation::Add,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,14 +753,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
return Err(Error::ReadOnly);
|
return Err(Error::ReadOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::_put_object(
|
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
|
||||||
&mut *tx,
|
|
||||||
principal.clone(),
|
|
||||||
cal_id.clone(),
|
|
||||||
object,
|
|
||||||
overwrite,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let sync_token = log_object_operation(
|
let sync_token = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
@@ -774,7 +831,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
data: CollectionOperationInfo::Content { sync_token },
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -795,6 +852,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Logs an operation to the events
|
// Logs an operation to the events
|
||||||
|
// TODO: Log multiple updates
|
||||||
async fn log_object_operation(
|
async fn log_object_operation(
|
||||||
tx: &mut Transaction<'_, Sqlite>,
|
tx: &mut Transaction<'_, Sqlite>,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
|
|||||||
81
crates/store_sqlite/src/tests/addressbook_store.rs
Normal file
81
crates/store_sqlite/src/tests/addressbook_store.rs
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
76
crates/store_sqlite/src/tests/calendar_store.rs
Normal file
76
crates/store_sqlite/src/tests/calendar_store.rs
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ use tokio::sync::OnceCell;
|
|||||||
|
|
||||||
static DB: OnceCell<SqlitePool> = OnceCell::const_new();
|
static DB: OnceCell<SqlitePool> = OnceCell::const_new();
|
||||||
|
|
||||||
|
mod addressbook_store;
|
||||||
|
mod calendar_store;
|
||||||
|
|
||||||
async fn get_test_db() -> SqlitePool {
|
async fn get_test_db() -> SqlitePool {
|
||||||
DB.get_or_init(async || {
|
DB.get_or_init(async || {
|
||||||
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustical_xml"
|
name = "rustical_xml"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xml_derive"
|
name = "xml_derive"
|
||||||
version = "0.1.0"
|
version.workspace = true
|
||||||
edition = "2024"
|
rust-version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
1
docs/googlec55e08580a46745c.html
Normal file
1
docs/googlec55e08580a46745c.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
google-site-verification: googlec55e08580a46745c.html
|
||||||
@@ -178,7 +178,9 @@ pub fn make_app<
|
|||||||
tracing::debug!("unauthorized");
|
tracing::debug!("unauthorized");
|
||||||
}
|
}
|
||||||
StatusCode::NOT_FOUND => {
|
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");
|
tracing::error!("client error");
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ async fn get_data_stores(
|
|||||||
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
||||||
|
|
||||||
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
||||||
|
addressbook_store.repair_orphans().await?;
|
||||||
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
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 subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
||||||
let principal_store = Arc::new(SqlitePrincipalStore::new(db));
|
let principal_store = Arc::new(SqlitePrincipalStore::new(db));
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in New Issue
Block a user