Compare commits

...

5 Commits

Author SHA1 Message Date
Lennart
99287f85f4 version 0.12.0 2026-01-19 15:48:56 +01:00
Lennart
df3143cd4c Fix status code for failed preconditions 2026-01-19 15:37:41 +01:00
Lennart Kämmle
92a3418f8e Merge pull request #164 from lennart-k/feat/ical-rewrite
ical-rs overhaul
2026-01-19 15:14:14 +01:00
Lennart
ea2f841269 ical-rs: Pin version to Git commit 2026-01-19 15:04:54 +01:00
Lennart
15e1509fe3 sqlite_store: Add option to skip broken objects and add validation on start-up 2026-01-19 14:48:21 +01:00
15 changed files with 184 additions and 144 deletions

26
Cargo.lock generated
View File

@@ -1771,7 +1771,7 @@ dependencies = [
[[package]] [[package]]
name = "ical" name = "ical"
version = "0.12.0-dev" version = "0.12.0-dev"
source = "git+https://github.com/lennart-k/ical-rs?branch=dev#8697656303f182ce173efdaf6aa7e842ffdb3f33" source = "git+https://github.com/lennart-k/ical-rs?rev=f1ad6456fd6cbd1e6da095297febddd2cfe61422#f1ad6456fd6cbd1e6da095297febddd2cfe61422"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz", "chrono-tz",
@@ -3317,7 +3317,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3364,7 +3364,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3406,7 +3406,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3440,7 +3440,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3466,7 +3466,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3491,7 +3491,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3527,7 +3527,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -3546,7 +3546,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3562,7 +3562,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3595,7 +3595,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3620,7 +3620,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.11.17" version = "0.12.0"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.18", "thiserror 2.0.18",
@@ -5442,7 +5442,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]] [[package]]
name = "xml_derive" name = "xml_derive"
version = "0.11.17" version = "0.12.0"
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.12.0"
rust-version = "1.92" rust-version = "1.92"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
@@ -107,7 +107,7 @@ strum = "0.27"
strum_macros = "0.27" strum_macros = "0.27"
serde_json = { version = "1.0", features = ["raw_value"] } serde_json = { version = "1.0", features = ["raw_value"] }
sqlx-sqlite = { version = "0.8", features = ["bundled"] } sqlx-sqlite = { version = "0.8", features = ["bundled"] }
ical = { git = "https://github.com/lennart-k/ical-rs", branch = "dev", features = [ ical = { git = "https://github.com/lennart-k/ical-rs", rev = "f1ad6456fd6cbd1e6da095297febddd2cfe61422", features = [
"chrono-tz", "chrono-tz",
] } ] }
toml = "0.9" toml = "0.9"

View File

@@ -98,7 +98,7 @@ pub async fn put_event<C: CalendarStore>(
Ok(object) => object, Ok(object) => object,
Err(err) => { Err(err) => {
warn!("invalid calendar data:\n{body}"); warn!("invalid calendar data:\n{body}");
warn!("{err:#?}"); warn!("{err}");
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData)); return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
} }
}; };

View File

@@ -23,7 +23,7 @@ impl IntoResponse for Precondition {
if let Err(err) = error.serialize_root(&mut writer) { if let Err(err) = error.serialize_root(&mut writer) {
return rustical_dav::Error::from(err).into_response(); return rustical_dav::Error::from(err).into_response();
} }
let mut res = Response::builder().status(StatusCode::PRECONDITION_FAILED); let mut res = Response::builder().status(StatusCode::FORBIDDEN);
res.headers_mut().unwrap().typed_insert(ContentType::xml()); res.headers_mut().unwrap().typed_insert(ContentType::xml());
res.body(Body::from(output)).unwrap() res.body(Body::from(output)).unwrap()
} }
@@ -72,7 +72,10 @@ impl Error {
Self::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Self::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
Self::ChronoParseError(_) | Self::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Self::ChronoParseError(_) | Self::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotFound => StatusCode::NOT_FOUND, Self::NotFound => StatusCode::NOT_FOUND,
Self::PreconditionFailed(_err) => StatusCode::PRECONDITION_FAILED, // The correct status code for a failed precondition is not PreconditionFailed but
// Forbidden (or Conflict):
// https://datatracker.ietf.org/doc/html/rfc4791#section-1.3
Self::PreconditionFailed(_err) => StatusCode::FORBIDDEN,
} }
} }
} }
@@ -82,10 +85,7 @@ impl IntoResponse for Error {
if let Self::PreconditionFailed(precondition) = self { if let Self::PreconditionFailed(precondition) = self {
return precondition.into_response(); return precondition.into_response();
} }
if matches!( if matches!(self.status_code(), StatusCode::INTERNAL_SERVER_ERROR) {
self.status_code(),
StatusCode::INTERNAL_SERVER_ERROR | StatusCode::PRECONDITION_FAILED
) {
error!("{self}"); error!("{self}");
} }
(self.status_code(), self.to_string()).into_response() (self.status_code(), self.to_string()).into_response()

View File

@@ -51,19 +51,18 @@ impl Error {
_ => StatusCode::BAD_REQUEST, _ => StatusCode::BAD_REQUEST,
}, },
Self::PropReadOnly => StatusCode::CONFLICT, Self::PropReadOnly => StatusCode::CONFLICT,
Self::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
Self::InternalError | Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::InternalError | Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Forbidden => StatusCode::FORBIDDEN, // The correct status code for a failed precondition is not PreconditionFailed but
// Forbidden (or Conflict):
// https://datatracker.ietf.org/doc/html/rfc4791#section-1.3
Self::PreconditionFailed | Self::Forbidden => StatusCode::FORBIDDEN,
} }
} }
} }
impl axum::response::IntoResponse for Error { impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
if matches!( if matches!(self.status_code(), StatusCode::INTERNAL_SERVER_ERROR) {
self.status_code(),
StatusCode::INTERNAL_SERVER_ERROR | StatusCode::PRECONDITION_FAILED
) {
error!("{self}"); error!("{self}");
} }

View File

@@ -53,9 +53,7 @@ impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
if matches!( if matches!(
self.status_code(), self.status_code(),
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR | StatusCode::CONFLICT
| StatusCode::PRECONDITION_FAILED
| StatusCode::CONFLICT
) { ) {
error!("{self}"); error!("{self}");
} }

View File

@@ -2,6 +2,7 @@ use super::ChangeOperation;
use crate::BEGIN_IMMEDIATE; use crate::BEGIN_IMMEDIATE;
use async_trait::async_trait; use async_trait::async_trait;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use ical::parser::ParserError;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::{ use rustical_store::{
Addressbook, AddressbookStore, CollectionMetadata, CollectionOperation, Addressbook, AddressbookStore, CollectionMetadata, CollectionOperation,
@@ -9,7 +10,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_span, instrument, warn}; use tracing::{error, error_span, instrument, warn};
pub mod birthday_calendar; pub mod birthday_calendar;
@@ -18,6 +19,12 @@ struct AddressObjectRow {
id: String, id: String,
vcf: String, vcf: String,
} }
impl From<AddressObjectRow> for (String, Result<AddressObject, ParserError>) {
fn from(row: AddressObjectRow) -> Self {
let result = AddressObject::from_vcf(row.vcf);
(row.id, result)
}
}
impl TryFrom<AddressObjectRow> for (String, AddressObject) { impl TryFrom<AddressObjectRow> for (String, AddressObject) {
type Error = rustical_store::Error; type Error = rustical_store::Error;
@@ -31,6 +38,7 @@ impl TryFrom<AddressObjectRow> for (String, AddressObject) {
pub struct SqliteAddressbookStore { pub struct SqliteAddressbookStore {
db: SqlitePool, db: SqlitePool,
sender: Sender<CollectionOperation>, sender: Sender<CollectionOperation>,
skip_broken: bool,
} }
impl SqliteAddressbookStore { impl SqliteAddressbookStore {
@@ -88,6 +96,36 @@ impl SqliteAddressbookStore {
Ok(()) Ok(())
} }
#[allow(clippy::missing_panics_doc)]
pub async fn validate_objects(&self, principal: &str) -> Result<(), Error> {
let mut success = true;
for addressbook in self.get_addressbooks(principal).await? {
for (object_id, res) in Self::_get_objects(&self.db, principal, &addressbook.id).await?
{
if let Err(err) = res {
warn!(
"Invalid address object found at {principal}/{addr_id}/{object_id}.vcf. Error: {err}",
addr_id = addressbook.id
);
success = false;
}
}
}
if !success {
if self.skip_broken {
error!(
"Not all address objects are valid. Since data_store.sqlite.skip_broken=true they will be hidden. You are still advised to manually remove or repair the object. If you need help feel free to open up an issue on GitHub."
);
} else {
error!(
"Not all address objects are valid. Since data_store.sqlite.skip_broken=false this causes a panic. Remove or repair the broken objects manually or set data_store.sqlite.skip_broken=false as a temporary solution to ignore the error. If you need help feel free to open up an issue on GitHub."
);
panic!();
}
}
Ok(())
}
// Logs an operation to an address object // Logs an operation to an address object
async fn log_object_operation( async fn log_object_operation(
tx: &mut Transaction<'_, Sqlite>, tx: &mut Transaction<'_, Sqlite>,
@@ -134,7 +172,7 @@ impl SqliteAddressbookStore {
if let Err(err) = self.sender.try_send(CollectionOperation { topic, data }) { if let Err(err) = self.sender.try_send(CollectionOperation { topic, data }) {
error_span!( error_span!(
"Error trying to send addressbook update notification:", "Error trying to send addressbook update notification:",
err = format!("{err:?}"), err = format!("{err}"),
); );
} }
} }
@@ -353,8 +391,8 @@ impl SqliteAddressbookStore {
executor: E, executor: E,
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
) -> Result<Vec<(String, AddressObject)>, rustical_store::Error> { ) -> Result<impl Iterator<Item = (String, Result<AddressObject, ParserError>)>, Error> {
sqlx::query_as!( Ok(sqlx::query_as!(
AddressObjectRow, AddressObjectRow,
"SELECT id, vcf FROM addressobjects WHERE principal = ? AND addressbook_id = ? AND deleted_at IS NULL", "SELECT id, vcf FROM addressobjects WHERE principal = ? AND addressbook_id = ? AND deleted_at IS NULL",
principal, principal,
@@ -363,8 +401,8 @@ impl SqliteAddressbookStore {
.fetch_all(executor) .fetch_all(executor)
.await.map_err(crate::Error::from)? .await.map_err(crate::Error::from)?
.into_iter() .into_iter()
.map(std::convert::TryInto::try_into) .map(Into::into)
.collect() )
} }
async fn _get_object<'e, E: Executor<'e, Database = Sqlite>>( async fn _get_object<'e, E: Executor<'e, Database = Sqlite>>(
@@ -607,7 +645,16 @@ impl AddressbookStore for SqliteAddressbookStore {
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
) -> Result<Vec<(String, AddressObject)>, rustical_store::Error> { ) -> Result<Vec<(String, AddressObject)>, rustical_store::Error> {
Self::_get_objects(&self.db, principal, addressbook_id).await let objects = Self::_get_objects(&self.db, principal, addressbook_id).await?;
if self.skip_broken {
Ok(objects
.filter_map(|(id, res)| Some((id, res.ok()?)))
.collect())
} else {
Ok(objects
.map(|(id, res)| res.map(|obj| (id, obj)))
.collect::<Result<Vec<_>, _>>()?)
}
} }
#[instrument] #[instrument]

View File

@@ -3,6 +3,7 @@ use crate::BEGIN_IMMEDIATE;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::TimeDelta; use chrono::TimeDelta;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use ical::parser::ParserError;
use ical::types::CalDateTime; use ical::types::CalDateTime;
use regex::Regex; use regex::Regex;
use rustical_ical::{CalendarObject, CalendarObjectType}; use rustical_ical::{CalendarObject, CalendarObjectType};
@@ -13,7 +14,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_span, instrument, warn}; use tracing::{error, error_span, instrument, warn};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct CalendarObjectRow { struct CalendarObjectRow {
@@ -22,6 +23,23 @@ struct CalendarObjectRow {
uid: String, uid: String,
} }
impl From<CalendarObjectRow> for (String, Result<CalendarObject, ParserError>) {
fn from(row: CalendarObjectRow) -> Self {
let result = CalendarObject::from_ics(row.ics).inspect(|object| {
if object.get_uid() != row.uid {
warn!(
"Calendar object {}.ics: UID={} and row uid={} do not match",
row.id,
object.get_uid(),
row.uid
);
}
});
(row.id, result)
}
}
impl TryFrom<CalendarObjectRow> for (String, CalendarObject) { impl TryFrom<CalendarObjectRow> for (String, CalendarObject) {
type Error = rustical_store::Error; type Error = rustical_store::Error;
@@ -92,6 +110,7 @@ impl From<CalendarRow> for Calendar {
pub struct SqliteCalendarStore { pub struct SqliteCalendarStore {
db: SqlitePool, db: SqlitePool,
sender: Sender<CollectionOperation>, sender: Sender<CollectionOperation>,
skip_broken: bool,
} }
impl SqliteCalendarStore { impl SqliteCalendarStore {
@@ -141,11 +160,40 @@ impl SqliteCalendarStore {
if let Err(err) = self.sender.try_send(CollectionOperation { topic, data }) { if let Err(err) = self.sender.try_send(CollectionOperation { topic, data }) {
error_span!( error_span!(
"Error trying to send calendar update notification:", "Error trying to send calendar update notification:",
err = format!("{err:?}"), err = format!("{err}"),
); );
} }
} }
#[allow(clippy::missing_panics_doc)]
pub async fn validate_objects(&self, principal: &str) -> Result<(), Error> {
let mut success = true;
for calendar in self.get_calendars(principal).await? {
for (object_id, res) in Self::_get_objects(&self.db, principal, &calendar.id).await? {
if let Err(err) = res {
warn!(
"Invalid calendar object found at {principal}/{cal_id}/{object_id}.ics. Error: {err}",
cal_id = calendar.id
);
success = false;
}
}
}
if !success {
if self.skip_broken {
error!(
"Not all calendar objects are valid. Since data_store.sqlite.skip_broken=true they will be hidden. You are still advised to manually remove or repair the object. If you need help feel free to open up an issue on GitHub."
);
} else {
error!(
"Not all calendar objects are valid. Since data_store.sqlite.skip_broken=false this causes a panic. Remove or repair the broken objects manually or set data_store.sqlite.skip_broken=false as a temporary solution to ignore the error. If you need help feel free to open up an issue on GitHub."
);
panic!();
}
}
Ok(())
}
/// In the past exports generated objects with invalid VERSION:4.0 /// In the past exports generated objects with invalid VERSION:4.0
/// This repair sets them to VERSION:2.0 /// This repair sets them to VERSION:2.0
#[allow(clippy::missing_panics_doc)] #[allow(clippy::missing_panics_doc)]
@@ -456,8 +504,8 @@ impl SqliteCalendarStore {
executor: E, executor: E,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
) -> Result<Vec<(String, CalendarObject)>, Error> { ) -> Result<impl Iterator<Item = (String, Result<CalendarObject, ParserError>)>, Error> {
sqlx::query_as!( Ok(sqlx::query_as!(
CalendarObjectRow, CalendarObjectRow,
"SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL", "SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
principal, principal,
@@ -466,8 +514,8 @@ impl SqliteCalendarStore {
.fetch_all(executor) .fetch_all(executor)
.await.map_err(crate::Error::from)? .await.map_err(crate::Error::from)?
.into_iter() .into_iter()
.map(std::convert::TryInto::try_into) .map(Into::into)
.collect() )
} }
async fn _calendar_query<'e, E: Executor<'e, Database = Sqlite>>( async fn _calendar_query<'e, E: Executor<'e, Database = Sqlite>>(
@@ -475,14 +523,14 @@ impl SqliteCalendarStore {
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
query: CalendarQuery, query: CalendarQuery,
) -> Result<Vec<(String, CalendarObject)>, Error> { ) -> Result<impl Iterator<Item = (String, Result<CalendarObject, ParserError>)>, Error> {
// We extend our query interval by one day in each direction since we really don't want to // We extend our query interval by one day in each direction since we really don't want to
// miss any objects because of timezone differences // miss any objects because of timezone differences
// I've previously tried NaiveDate::MIN,MAX, but it seems like sqlite cannot handle these // I've previously tried NaiveDate::MIN,MAX, but it seems like sqlite cannot handle these
let start = query.time_start.map(|start| start - TimeDelta::days(1)); let start = query.time_start.map(|start| start - TimeDelta::days(1));
let end = query.time_end.map(|end| end + TimeDelta::days(1)); let end = query.time_end.map(|end| end + TimeDelta::days(1));
sqlx::query_as!( Ok(sqlx::query_as!(
CalendarObjectRow, CalendarObjectRow,
r"SELECT id, uid, ics FROM calendarobjects r"SELECT id, uid, ics FROM calendarobjects
WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL
@@ -500,8 +548,7 @@ impl SqliteCalendarStore {
.await .await
.map_err(crate::Error::from)? .map_err(crate::Error::from)?
.into_iter() .into_iter()
.map(std::convert::TryInto::try_into) .map(Into::into))
.collect()
} }
async fn _get_object<'e, E: Executor<'e, Database = Sqlite>>( async fn _get_object<'e, E: Executor<'e, Database = Sqlite>>(
@@ -641,6 +688,7 @@ impl SqliteCalendarStore {
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
synctoken: i64, synctoken: i64,
skip_broken: bool,
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> { ) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> {
struct Row { struct Row {
object_id: String, object_id: String,
@@ -670,6 +718,8 @@ impl SqliteCalendarStore {
match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await { match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await {
Ok(object) => objects.push((object_id, object)), Ok(object) => objects.push((object_id, object)),
Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id), Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id),
// Skip broken object
Err(rustical_store::Error::IcalError(_)) if skip_broken => (),
Err(err) => return Err(err), Err(err) => return Err(err),
} }
} }
@@ -820,7 +870,16 @@ impl CalendarStore for SqliteCalendarStore {
cal_id: &str, cal_id: &str,
query: CalendarQuery, query: CalendarQuery,
) -> Result<Vec<(String, CalendarObject)>, Error> { ) -> Result<Vec<(String, CalendarObject)>, Error> {
Self::_calendar_query(&self.db, principal, cal_id, query).await let objects = Self::_calendar_query(&self.db, principal, cal_id, query).await?;
if self.skip_broken {
Ok(objects
.filter_map(|(id, res)| Some((id, res.ok()?)))
.collect())
} else {
Ok(objects
.map(|(id, res)| res.map(|obj| (id, obj)))
.collect::<Result<Vec<_>, _>>()?)
}
} }
async fn calendar_metadata( async fn calendar_metadata(
@@ -851,7 +910,16 @@ impl CalendarStore for SqliteCalendarStore {
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
) -> Result<Vec<(String, CalendarObject)>, Error> { ) -> Result<Vec<(String, CalendarObject)>, Error> {
Self::_get_objects(&self.db, principal, cal_id).await let objects = Self::_get_objects(&self.db, principal, cal_id).await?;
if self.skip_broken {
Ok(objects
.filter_map(|(id, res)| Some((id, res.ok()?)))
.collect())
} else {
Ok(objects
.map(|(id, res)| res.map(|obj| (id, obj)))
.collect::<Result<Vec<_>, _>>()?)
}
} }
#[instrument] #[instrument]
@@ -974,7 +1042,7 @@ impl CalendarStore for SqliteCalendarStore {
cal_id: &str, cal_id: &str,
synctoken: i64, synctoken: i64,
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> { ) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> {
Self::_sync_changes(&self.db, principal, cal_id, synctoken).await Self::_sync_changes(&self.db, principal, cal_id, synctoken, self.skip_broken).await
} }
fn is_read_only(&self, _cal_id: &str) -> bool { fn is_read_only(&self, _cal_id: &str) -> bool {

View File

@@ -18,7 +18,7 @@ impl From<sqlx::Error> for Error {
sqlx::Error::RowNotFound => Self::StoreError(rustical_store::Error::NotFound), sqlx::Error::RowNotFound => Self::StoreError(rustical_store::Error::NotFound),
sqlx::Error::Database(err) => { sqlx::Error::Database(err) => {
if err.is_unique_violation() { if err.is_unique_violation() {
warn!("{err:?}"); warn!("{err}");
Self::StoreError(rustical_store::Error::AlreadyExists) Self::StoreError(rustical_store::Error::AlreadyExists)
} else { } else {
Self::SqlxError(sqlx::Error::Database(err)) Self::SqlxError(sqlx::Error::Database(err))

View File

@@ -52,8 +52,8 @@ pub async fn test_store_context() -> TestStoreContext {
let db = get_test_db().await; let db = get_test_db().await;
TestStoreContext { TestStoreContext {
db: db.clone(), db: db.clone(),
addr_store: SqliteAddressbookStore::new(db.clone(), send_addr), addr_store: SqliteAddressbookStore::new(db.clone(), send_addr, false),
cal_store: SqliteCalendarStore::new(db.clone(), send_cal), cal_store: SqliteCalendarStore::new(db.clone(), send_cal, false),
principal_store: SqlitePrincipalStore::new(db.clone()), principal_store: SqlitePrincipalStore::new(db.clone()),
sub_store: SqliteStore::new(db), sub_store: SqliteStore::new(db),
} }

View File

@@ -18,6 +18,7 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
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, run_repairs: true,
skip_broken: true,
}), }),
tracing: TracingConfig::default(), tracing: TracingConfig::default(),
frontend: FrontendConfig { frontend: FrontendConfig {

View File

@@ -28,6 +28,8 @@ pub struct SqliteDataStoreConfig {
pub db_url: String, pub db_url: String,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub run_repairs: bool, pub run_repairs: bool,
#[serde(default = "default_true")]
pub skip_broken: bool,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]

View File

@@ -66,7 +66,7 @@ END:VCALENDAR";
.typed_insert(Authorization::basic("user", "pass")); .typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap(); let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED); assert_eq!(response.status(), StatusCode::FORBIDDEN);
let body = response.extract_string().await; let body = response.extract_string().await;
insta::assert_snapshot!(body, @r#" insta::assert_snapshot!(body, @r#"
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>

View File

@@ -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 {
@@ -73,13 +70,18 @@ async fn get_data_stores(
DataStoreConfig::Sqlite(SqliteDataStoreConfig { DataStoreConfig::Sqlite(SqliteDataStoreConfig {
db_url, db_url,
run_repairs, run_repairs,
skip_broken,
}) => { }) => {
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(
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send)); db.clone(),
send.clone(),
*skip_broken,
));
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send, *skip_broken));
if *run_repairs { if *run_repairs {
info!("Running repair tasks"); info!("Running repair tasks");
addressbook_store.repair_orphans().await?; addressbook_store.repair_orphans().await?;
@@ -88,6 +90,13 @@ async fn get_data_stores(
} }
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));
// Validate all calendar objects
for principal in principal_store.get_principals().await? {
cal_store.validate_objects(&principal.id).await?;
addressbook_store.validate_objects(&principal.id).await?;
}
( (
addressbook_store, addressbook_store,
cal_store, cal_store,
@@ -125,14 +134,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(())
}