Compare commits

..

19 Commits

Author SHA1 Message Date
Lennart K
b9c2a4cc27 address_object resource: Implement displayname 2026-01-16 14:49:19 +01:00
Lennart K
c91205558e Fix comp-filter 2026-01-16 14:45:34 +01:00
Lennart K
cd9e3ed8d6 simplify handling of ical-related errors 2026-01-16 14:16:22 +01:00
Lennart K
200d5e7170 Update ical-rs 2026-01-13 16:01:59 +01:00
Lennart K
5cb538d3fb build MVP for birthday calendar 2026-01-13 12:41:03 +01:00
Lennart K
d9da123ff4 Remove calendar-query integration test for now 2026-01-12 14:06:23 +01:00
Lennart K
eba2f0da9f update ical-rs 2026-01-12 14:04:35 +01:00
Lennart K
291bd967da Re-add get_last_occurence for sqlite store 2026-01-09 10:32:50 +01:00
Lennart K
002814a564 Remove unused code 2026-01-08 23:24:47 +01:00
Lennart K
ba13aaa703 Re-implement calendar imports 2026-01-08 23:17:39 +01:00
Lennart K
7a02bfeffc Calendar export: Fix PRODID 2026-01-08 16:17:39 +01:00
Lennart K
1b69148d6f Re-implement calendar export 2026-01-08 15:36:02 +01:00
Lennart K
f4de80c6b9 clean up ical-related stuff 2026-01-08 14:31:28 +01:00
Lennart K
7a1ec3e351 make calendar object id extrinsic 2026-01-07 13:14:50 +01:00
Lennart K
eb7bdd0018 Make AddressObject object_id an extrinsic property 2026-01-07 12:19:30 +01:00
Lennart K
8e583e24cb small fixes 2026-01-07 11:58:02 +01:00
Lennart K
5e5017a185 Decrease folder nesting 2026-01-07 11:46:28 +01:00
Lennart K
3c87191f69 incorporate get_first_occurenec 2026-01-07 11:44:55 +01:00
Lennart K
d1947a159b migrate to new ical-rs version 2026-01-07 11:32:53 +01:00
16 changed files with 26 additions and 267 deletions

View File

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

View File

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

View File

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

View File

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

30
Cargo.lock generated
View File

@@ -2870,9 +2870,9 @@ dependencies = [
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.39.0" version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2e3bf4aa9d243beeb01a7b3bc30b77cfe2c44e24ec02d751a7104a53c2c49a1" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -3317,7 +3317,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3328,7 +3328,6 @@ dependencies = [
"figment", "figment",
"headers", "headers",
"http", "http",
"ical",
"insta", "insta",
"opentelemetry", "opentelemetry",
"opentelemetry-otlp", "opentelemetry-otlp",
@@ -3363,7 +3362,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3405,7 +3404,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3439,7 +3438,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3465,7 +3464,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3490,7 +3489,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3526,7 +3525,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -3545,7 +3544,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3561,7 +3560,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3594,7 +3593,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3604,7 +3603,6 @@ dependencies = [
"password-auth", "password-auth",
"password-hash", "password-hash",
"pbkdf2", "pbkdf2",
"regex",
"rstest", "rstest",
"rustical_ical", "rustical_ical",
"rustical_store", "rustical_store",
@@ -3619,7 +3617,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -5441,7 +5439,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]] [[package]]
name = "xml_derive" name = "xml_derive"
version = "0.11.17" version = "0.11.10"
dependencies = [ dependencies = [
"darling 0.23.0", "darling 0.23.0",
"heck", "heck",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.11.17" version = "0.11.10"
rust-version = "1.92" rust-version = "1.92"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
@@ -73,7 +73,7 @@ tokio = { version = "1.48", features = [
url = "2.5" url = "2.5"
base64 = "0.22" base64 = "0.22"
thiserror = "2.0" thiserror = "2.0"
quick-xml = { version = "0.39" } quick-xml = { version = "0.38" }
rust-embed = "8.9" rust-embed = "8.9"
tower-sessions = "0.14" tower-sessions = "0.14"
futures-core = "0.3" futures-core = "0.3"
@@ -160,7 +160,6 @@ 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
ical.workspace = true
toml.workspace = true toml.workspace = true
serde.workspace = true serde.workspace = true
tokio.workspace = true tokio.workspace = true

View File

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

View File

@@ -45,7 +45,7 @@ impl<PN: XmlDeserialize> XmlDeserialize for PropElement<PN> {
// start of a child element // start of a child element
Event::Start(start) | Event::Empty(start) => { Event::Start(start) | Event::Empty(start) => {
let empty = matches!(event, Event::Empty(_)); let empty = matches!(event, Event::Empty(_));
let (ns, name) = reader.resolver().resolve_element(start.name()); let (ns, name) = reader.resolve_element(start.name());
let ns = match ns { let ns = match ns {
ResolveResult::Bound(ns) => Some(NamespaceOwned::from(ns)), ResolveResult::Bound(ns) => Some(NamespaceOwned::from(ns)),
ResolveResult::Unknown(_ns) => todo!("handle error"), ResolveResult::Unknown(_ns) => todo!("handle error"),

View File

@@ -37,4 +37,3 @@ pbkdf2.workspace = true
rustical_ical.workspace = true rustical_ical.workspace = true
rstest = { workspace = true, optional = true } rstest = { workspace = true, optional = true }
sha2.workspace = true sha2.workspace = true
regex.workspace = true

View File

@@ -4,7 +4,6 @@ use async_trait::async_trait;
use chrono::TimeDelta; use chrono::TimeDelta;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use ical::types::CalDateTime; use ical::types::CalDateTime;
use regex::Regex;
use rustical_ical::{CalendarObject, CalendarObjectType}; use rustical_ical::{CalendarObject, CalendarObjectType};
use rustical_store::calendar_store::CalendarQuery; use rustical_store::calendar_store::CalendarQuery;
use rustical_store::synctoken::format_synctoken; use rustical_store::synctoken::format_synctoken;
@@ -146,83 +145,6 @@ impl SqliteCalendarStore {
} }
} }
/// In the past exports generated objects with invalid VERSION:4.0
/// This repair sets them to VERSION:2.0
#[allow(clippy::missing_panics_doc)]
pub async fn repair_invalid_version_4_0(&self) -> Result<(), Error> {
struct Row {
principal: String,
cal_id: String,
id: String,
ics: String,
}
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
#[allow(clippy::missing_panics_doc)]
let version_pattern = Regex::new(r"(?mi)^VERSION:4.0").unwrap();
let repairs: Vec<Row> = sqlx::query_as!(
Row,
r#"SELECT principal, cal_id, id, ics FROM calendarobjects WHERE ics LIKE '%VERSION:4.0%';"#
)
.fetch_all(&mut *tx)
.await
.map_err(crate::Error::from)?
.into_iter()
.filter_map(|mut row| {
version_pattern.find(&row.ics)?;
let new_ics = version_pattern.replace(&row.ics, "VERSION:2.0");
// Safeguard that we really only changed the version
assert_eq!(row.ics.len(), new_ics.len());
row.ics = new_ics.to_string();
Some(row)
})
.collect();
if repairs.is_empty() {
return Ok(());
}
warn!(
"Found {} calendar objects with invalid VERSION:4.0. Repairing by setting to VERSION:2.0",
repairs.len()
);
for repair in &repairs {
// calendarobjectchangelog is used by sync-collection to fetch changes
// By deleting entries we will later regenerate new entries such that clients will notice
// the objects have changed
warn!(
"Repairing VERSION for {}/{}/{}.ics",
repair.principal, repair.cal_id, repair.id
);
sqlx::query!(
"DELETE FROM calendarobjectchangelog WHERE (principal, cal_id, object_id) = (?, ?, ?);",
repair.principal, repair.cal_id, repair.id
).execute(&mut *tx).await
.map_err(crate::Error::from)?;
sqlx::query!(
"UPDATE calendarobjects SET ics = ? WHERE (principal, cal_id, id) = (?, ?, ?);",
repair.ics,
repair.principal,
repair.cal_id,
repair.id
)
.execute(&mut *tx)
.await
.map_err(crate::Error::from)?;
}
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
// Commit "orphaned" objects to the changelog table // Commit "orphaned" objects to the changelog table
pub async fn repair_orphans(&self) -> Result<(), Error> { pub async fn repair_orphans(&self) -> Result<(), Error> {
struct Row { struct Row {

View File

@@ -136,7 +136,7 @@ impl NamedStruct {
#(#builder_field_inits),* #(#builder_field_inits),*
}; };
let (ns, name) = reader.resolver().resolve_element(start.name()); let (ns, name) = reader.resolve_element(start.name());
#(#tagname_field_branches);* #(#tagname_field_branches);*
#(#namespace_field_branches);* #(#namespace_field_branches);*
@@ -161,7 +161,7 @@ impl NamedStruct {
// start of a child element // start of a child element
Event::Start(start) | Event::Empty(start) => { Event::Start(start) | Event::Empty(start) => {
let empty = matches!(event, Event::Empty(_)); let empty = matches!(event, Event::Empty(_));
let (ns, name) = reader.resolver().resolve_element(start.name()); let (ns, name) = reader.resolve_element(start.name());
match (ns, name.as_ref()) { match (ns, name.as_ref()) {
#(#named_field_branches),* #(#named_field_branches),*
#(#untagged_field_branches),* #(#untagged_field_branches),*

View File

@@ -42,7 +42,7 @@ impl<T: XmlRootTag + XmlDeserialize> XmlDocument for T {
match event { match event {
Event::Decl(_) | Event::Comment(_) => { /* ignore this */ } Event::Decl(_) | Event::Comment(_) => { /* ignore this */ }
Event::Start(start) | Event::Empty(start) => { Event::Start(start) | Event::Empty(start) => {
let (ns, name) = reader.resolver().resolve_element(start.name()); let (ns, name) = reader.resolve_element(start.name());
let matches = match (Self::root_ns(), &ns, name) { let matches = match (Self::root_ns(), &ns, name) {
// Wrong tag // Wrong tag
(_, _, name) if name.as_ref() != Self::root_tag().as_bytes() => false, (_, _, name) if name.as_ref() != Self::root_tag().as_bytes() => false,

View File

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

View File

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

View File

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

View File

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