Move ical-related stuff to rustical_ical crate

This commit is contained in:
Lennart
2025-06-03 18:15:26 +02:00
parent 5a6ffd3c19
commit 7f3ce01c2b
35 changed files with 121 additions and 68 deletions

4
Cargo.lock generated
View File

@@ -3114,6 +3114,7 @@ dependencies = [
"quick-xml",
"rustical_dav",
"rustical_dav_push",
"rustical_ical",
"rustical_store",
"rustical_xml",
"serde",
@@ -3198,6 +3199,7 @@ dependencies = [
name = "rustical_ical"
version = "0.1.0"
dependencies = [
"actix-web",
"chrono",
"chrono-tz",
"derive_more 2.0.1",
@@ -3206,6 +3208,8 @@ dependencies = [
"regex",
"rrule",
"rustical_xml",
"serde",
"sha2",
"strum",
"strum_macros",
"thiserror 2.0.12",

View File

@@ -103,7 +103,7 @@ 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/" }
rustical_ical = { path = "./crates/ical/", features = ["actix"] }
chrono-tz = "0.10"
chrono-humanize = "0.2"
rand = "0.8"

View File

@@ -2,8 +2,8 @@ use crate::Error;
use crate::calendar::prop::SupportedCalendarComponentSet;
use actix_web::HttpResponse;
use actix_web::web::{Data, Path};
use rustical_ical::CalendarObjectType;
use rustical_store::auth::User;
use rustical_store::calendar::CalendarObjectType;
use rustical_store::{Calendar, CalendarStore};
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument;

View File

@@ -11,7 +11,8 @@ use rustical_dav::{
resource::{PrincipalUri, Resource},
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
};
use rustical_store::{CalendarObject, CalendarStore, auth::User};
use rustical_ical::CalendarObject;
use rustical_store::{CalendarStore, auth::User};
use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]

View File

@@ -2,8 +2,8 @@ use rustical_dav::{
resource::{PrincipalUri, Resource},
xml::{MultistatusElement, PropfindType},
};
use rustical_ical::UtcDateTime;
use rustical_store::{CalendarObject, CalendarStore, auth::User, calendar_store::CalendarQuery};
use rustical_ical::{CalendarObject, UtcDateTime};
use rustical_store::{CalendarStore, auth::User, calendar_store::CalendarQuery};
use rustical_xml::XmlDeserialize;
use std::ops::Deref;

View File

@@ -1,5 +1,5 @@
use derive_more::derive::{From, Into};
use rustical_store::calendar::CalendarObjectType;
use rustical_ical::CalendarObjectType;
use rustical_xml::{XmlDeserialize, XmlSerialize};
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, From, Into)]

View File

@@ -4,8 +4,9 @@ use actix_web::HttpResponse;
use actix_web::http::header;
use actix_web::http::header::HeaderValue;
use actix_web::web::{Data, Path};
use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
use rustical_store::auth::User;
use rustical_store::{CalendarObject, CalendarStore};
use tracing::instrument;
use tracing_actix_web::RootSpan;

View File

@@ -9,7 +9,8 @@ use rustical_dav::{
resource::{PrincipalUri, Resource, ResourceService},
xml::Resourcetype,
};
use rustical_store::{CalendarObject, CalendarStore, auth::User};
use rustical_ical::CalendarObject;
use rustical_store::{CalendarStore, auth::User};
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
use serde::Deserialize;
use std::sync::Arc;

View File

@@ -23,6 +23,9 @@ pub enum Error {
#[error(transparent)]
XmlDecodeError(#[from] rustical_xml::XmlError),
#[error(transparent)]
IcalError(#[from] rustical_ical::Error),
}
impl actix_web::ResponseError for Error {
@@ -30,9 +33,7 @@ impl actix_web::ResponseError for Error {
match self {
Error::StoreError(err) => match err {
rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
rustical_store::Error::InvalidData(_) => StatusCode::BAD_REQUEST,
rustical_store::Error::AlreadyExists => StatusCode::CONFLICT,
rustical_store::Error::ParserError(_) => StatusCode::BAD_REQUEST,
rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR,
},
@@ -42,12 +43,14 @@ impl actix_web::ResponseError for Error {
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
Error::NotFound => StatusCode::NOT_FOUND,
Error::IcalError(err) => err.status_code(),
}
}
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
error!("Error: {self}");
match self {
Error::DavError(err) => err.error_response(),
Error::IcalError(err) => err.error_response(),
_ => HttpResponse::build(self.status_code()).body(self.to_string()),
}
}

View File

@@ -26,3 +26,4 @@ chrono = { workspace = true }
rustical_xml.workspace = true
uuid.workspace = true
rustical_dav_push.workspace = true
rustical_ical.workspace = true

View File

@@ -8,8 +8,9 @@ use actix_web::http::header::HeaderValue;
use actix_web::web::{Data, Path};
use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource;
use rustical_ical::AddressObject;
use rustical_store::AddressbookStore;
use rustical_store::auth::User;
use rustical_store::{AddressObject, AddressbookStore};
use tracing::instrument;
use tracing_actix_web::RootSpan;

View File

@@ -8,7 +8,8 @@ use rustical_dav::{
resource::{PrincipalUri, Resource, ResourceService},
xml::Resourcetype,
};
use rustical_store::{AddressObject, AddressbookStore, auth::User};
use rustical_ical::AddressObject;
use rustical_store::{AddressbookStore, auth::User};
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
use serde::Deserialize;
use std::sync::Arc;

View File

@@ -10,7 +10,8 @@ use rustical_dav::{
resource::{PrincipalUri, Resource},
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
};
use rustical_store::{AddressObject, AddressbookStore, auth::User};
use rustical_ical::AddressObject;
use rustical_store::{AddressbookStore, auth::User};
use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]

View File

@@ -23,6 +23,9 @@ pub enum Error {
#[error(transparent)]
XmlDecodeError(#[from] rustical_xml::XmlError),
#[error(transparent)]
IcalError(#[from] rustical_ical::Error),
}
impl actix_web::ResponseError for Error {
@@ -30,9 +33,7 @@ impl actix_web::ResponseError for Error {
match self {
Error::StoreError(err) => match err {
rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
rustical_store::Error::InvalidData(_) => StatusCode::BAD_REQUEST,
rustical_store::Error::AlreadyExists => StatusCode::CONFLICT,
rustical_store::Error::ParserError(_) => StatusCode::BAD_REQUEST,
rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR,
},
@@ -42,12 +43,14 @@ impl actix_web::ResponseError for Error {
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
Error::NotFound => StatusCode::NOT_FOUND,
Self::IcalError(err) => err.status_code(),
}
}
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
error!("Error: {self}");
match self {
Error::DavError(err) => err.error_response(),
Error::IcalError(err) => err.error_response(),
_ => HttpResponse::build(self.status_code()).body(self.to_string()),
}
}

View File

@@ -5,6 +5,9 @@ edition.workspace = true
description.workspace = true
repository.workspace = true
[features]
actix = ["dep:actix-web"]
[dependencies]
chrono.workspace = true
chrono-tz.workspace = true
@@ -17,3 +20,6 @@ regex.workspace = true
strum.workspace = true
strum_macros.workspace = true
rrule = "0.14"
serde.workspace = true
sha2.workspace = true
actix-web = { workspace = true, optional = true }

View File

@@ -1,10 +1,10 @@
use crate::{CalDateTime, LOCAL_DATE};
use crate::{CalendarObject, Error};
use chrono::Datelike;
use ical::parser::{
Component,
vcard::{self, component::VcardContact},
};
use rustical_ical::{CalDateTime, LOCAL_DATE};
use sha2::{Digest, Sha256};
use std::{collections::HashMap, io::BufReader};
@@ -18,7 +18,7 @@ pub struct AddressObject {
impl AddressObject {
pub fn from_vcf(object_id: String, vcf: String) -> Result<Self, Error> {
let mut parser = vcard::VcardParser::new(BufReader::new(vcf.as_bytes()));
let vcard = parser.next().ok_or(Error::NotFound)??;
let vcard = parser.next().ok_or(Error::MissingContact)??;
if parser.next().is_some() {
return Err(Error::InvalidData(
"multiple vcards, only one allowed".to_owned(),

35
crates/ical/src/error.rs Normal file
View File

@@ -0,0 +1,35 @@
use crate::CalDateTimeError;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid ics/vcf input: {0}")]
InvalidData(String),
#[error("Missing calendar")]
MissingCalendar,
#[error("Missing contact")]
MissingContact,
#[error(transparent)]
ParserError(#[from] ical::parser::ParserError),
#[error(transparent)]
CalDateTimeError(#[from] CalDateTimeError),
#[error(transparent)]
RRuleError(#[from] rrule::RRuleError),
}
#[cfg(feature = "actix")]
impl actix_web::ResponseError for Error {
fn status_code(&self) -> actix_web::http::StatusCode {
match self {
Self::InvalidData(_) => actix_web::http::StatusCode::BAD_REQUEST,
Self::MissingCalendar | Self::MissingContact => {
actix_web::http::StatusCode::BAD_REQUEST
}
_ => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -1,4 +1,5 @@
use crate::Error;
use crate::{CalDateTime, ComponentMut, parse_duration};
use chrono::{DateTime, Duration};
use ical::{
generator::IcalEvent,
@@ -6,7 +7,6 @@ use ical::{
property::Property,
};
use rrule::{RRule, RRuleSet};
use rustical_ical::{CalDateTime, ComponentMut, parse_duration};
use std::{collections::HashMap, str::FromStr};
#[derive(Debug, Clone)]

View File

@@ -1,10 +1,8 @@
mod calendar;
mod event;
mod journal;
mod object;
mod todo;
pub use calendar::*;
pub use event::*;
pub use journal::*;
pub use object::*;

View File

@@ -1,10 +1,10 @@
use super::{EventObject, JournalObject, TodoObject};
use crate::CalDateTime;
use crate::Error;
use ical::{
generator::{Emitter, IcalCalendar},
parser::{Component, ical::component::IcalTimeZone},
};
use rustical_ical::CalDateTime;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::{collections::HashMap, io::BufReader};
@@ -66,7 +66,7 @@ pub struct CalendarObject {
impl CalendarObject {
pub fn from_ics(object_id: String, ics: String) -> Result<Self, Error> {
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
let cal = parser.next().ok_or(Error::NotFound)??;
let cal = parser.next().ok_or(Error::MissingCalendar)??;
if parser.next().is_some() {
return Err(Error::InvalidData(
"multiple calendars, only one allowed".to_owned(),

View File

@@ -8,3 +8,12 @@ pub use timezone::*;
mod duration;
pub use duration::parse_duration;
mod icalendar;
pub use icalendar::*;
mod error;
pub use error::Error;
mod address_object;
pub use address_object::AddressObject;

View File

@@ -1,5 +0,0 @@
pub mod address_object;
pub mod addressbook;
pub use address_object::*;
pub use addressbook::*;

View File

@@ -1,8 +1,6 @@
use crate::{
Error,
addressbook::{AddressObject, Addressbook},
};
use crate::{Error, addressbook::Addressbook};
use async_trait::async_trait;
use rustical_ical::AddressObject;
#[async_trait]
pub trait AddressbookStore: Send + Sync + 'static {

View File

@@ -38,7 +38,7 @@ impl TryFrom<&str> for PrincipalType {
"ROOM" => Self::Room,
"UNKNOWN" => Self::Unknown,
_ => {
return Err(crate::Error::InvalidData(
return Err(crate::Error::InvalidPrincipalType(
"Invalid principal type".to_owned(),
));
}

View File

@@ -1,6 +1,6 @@
use super::CalendarObjectType;
use crate::synctoken::format_synctoken;
use chrono::NaiveDateTime;
use rustical_ical::CalendarObjectType;
use serde::Serialize;
#[derive(Debug, Default, Clone, Serialize)]

View File

@@ -1,7 +1,7 @@
use crate::calendar::{Calendar, CalendarObject};
use crate::error::Error;
use crate::{Calendar, error::Error};
use async_trait::async_trait;
use chrono::NaiveDate;
use rustical_ical::CalendarObject;
#[derive(Default, Debug, Clone)]
pub struct CalendarQuery {

View File

@@ -1,12 +1,9 @@
use std::{collections::HashMap, sync::Arc};
use crate::{
AddressObject, Addressbook, AddressbookStore, Calendar, CalendarObject, CalendarStore, Error,
calendar::CalendarObjectType,
};
use crate::{Addressbook, AddressbookStore, Calendar, CalendarStore, Error};
use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
use sha2::{Digest, Sha256};
use std::{collections::HashMap, sync::Arc};
#[derive(Constructor, Clone)]
pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>);
@@ -85,7 +82,7 @@ impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
let (objects, deleted_objects, new_synctoken) =
self.0.sync_changes(principal, cal_id, synctoken).await?;
let objects: Result<Vec<Option<CalendarObject>>, Error> = objects
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
.iter()
.map(AddressObject::get_birthday_object)
.collect();
@@ -99,8 +96,8 @@ impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, Error> = self
.0
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
self.0
.get_objects(principal, cal_id)
.await?
.iter()

View File

@@ -1,5 +1,4 @@
use actix_web::{ResponseError, http::StatusCode};
use rustical_ical::CalDateTimeError;
#[derive(Debug, thiserror::Error)]
pub enum Error {
@@ -9,8 +8,8 @@ pub enum Error {
#[error("Resource already exists and overwrite=false")]
AlreadyExists,
#[error("Invalid ics/vcf input: {0}")]
InvalidData(String),
#[error("Invalid principal type: {0}")]
InvalidPrincipalType(String),
#[error("Read-only")]
ReadOnly,
@@ -21,17 +20,11 @@ pub enum Error {
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
ParserError(#[from] ical::parser::ParserError),
#[error(transparent)]
Other(#[from] anyhow::Error),
#[error(transparent)]
CalDateTimeError(#[from] CalDateTimeError),
#[error(transparent)]
RRuleError(#[from] rrule::RRuleError),
IcalError(#[from] rustical_ical::Error),
}
impl ResponseError for Error {
@@ -39,8 +32,9 @@ impl ResponseError for Error {
match self {
Self::NotFound => StatusCode::NOT_FOUND,
Self::AlreadyExists => StatusCode::CONFLICT,
Self::InvalidData(_) => StatusCode::BAD_REQUEST,
Self::ReadOnly => StatusCode::FORBIDDEN,
Self::IcalError(err) => err.status_code(),
Self::InvalidPrincipalType(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}

View File

@@ -4,7 +4,7 @@ pub mod calendar_store;
pub mod error;
pub use error::Error;
pub mod auth;
pub mod calendar;
mod calendar;
mod contact_birthday_store;
mod secret;
mod subscription_store;
@@ -16,8 +16,8 @@ pub use contact_birthday_store::ContactBirthdayStore;
pub use secret::Secret;
pub use subscription_store::*;
pub use addressbook::{AddressObject, Addressbook};
pub use calendar::{Calendar, CalendarObject};
pub use addressbook::Addressbook;
pub use calendar::Calendar;
#[derive(Debug, Clone)]
pub enum CollectionOperationType {

View File

@@ -1,8 +1,9 @@
use super::ChangeOperation;
use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_ical::AddressObject;
use rustical_store::{
AddressObject, Addressbook, AddressbookStore, CollectionOperation, CollectionOperationDomain,
Addressbook, AddressbookStore, CollectionOperation, CollectionOperationDomain,
CollectionOperationType, Error, synctoken::format_synctoken,
};
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};

View File

@@ -2,11 +2,10 @@ use super::ChangeOperation;
use async_trait::async_trait;
use chrono::TimeDelta;
use derive_more::derive::Constructor;
use rustical_ical::CalDateTime;
use rustical_store::calendar::CalendarObjectType;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
use rustical_store::calendar_store::CalendarQuery;
use rustical_store::synctoken::format_synctoken;
use rustical_store::{Calendar, CalendarObject, CalendarStore, Error};
use rustical_store::{Calendar, CalendarStore, Error};
use rustical_store::{CollectionOperation, CollectionOperationType};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
@@ -23,7 +22,7 @@ impl TryFrom<CalendarObjectRow> for CalendarObject {
type Error = rustical_store::Error;
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
CalendarObject::from_ics(value.id, value.ics)
Ok(CalendarObject::from_ics(value.id, value.ics)?)
}
}

View File

@@ -5,6 +5,9 @@ pub enum Error {
#[error(transparent)]
StoreError(rustical_store::Error),
#[error(transparent)]
IcalError(#[from] rustical_ical::Error),
}
impl From<sqlx::Error> for Error {
@@ -27,6 +30,7 @@ impl From<Error> for rustical_store::Error {
fn from(value: Error) -> Self {
match value {
Error::SqlxError(err) => Self::Other(err.into()),
Error::IcalError(err) => Self::Other(err.into()),
Error::StoreError(err) => err,
}
}