mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
Migrate from Event type to CalendarObject
This is preparation to support other calendar components like VTODO and VJOURNAL
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
event::resource::{EventProp, EventResource},
|
calendar_object::resource::{CalendarObjectProp, CalendarObjectResource},
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
@@ -11,7 +11,7 @@ use rustical_dav::{
|
|||||||
resource::HandlePropfind,
|
resource::HandlePropfind,
|
||||||
xml::{multistatus::PropstatWrapper, MultistatusElement},
|
xml::{multistatus::PropstatWrapper, MultistatusElement},
|
||||||
};
|
};
|
||||||
use rustical_store::{model::Event, CalendarStore};
|
use rustical_store::{model::object::CalendarObject, CalendarStore};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ pub async fn get_events_calendar_multiget<C: CalendarStore + ?Sized>(
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
store: &RwLock<C>,
|
store: &RwLock<C>,
|
||||||
) -> Result<Vec<Event>, Error> {
|
) -> Result<Vec<CalendarObject>, Error> {
|
||||||
// TODO: add proper error results for single events
|
// TODO: add proper error results for single events
|
||||||
let resource_def =
|
let resource_def =
|
||||||
ResourceDef::prefix(prefix).join(&ResourceDef::new("/user/{principal}/{cid}/{uid}"));
|
ResourceDef::prefix(prefix).join(&ResourceDef::new("/user/{principal}/{cid}/{uid}"));
|
||||||
@@ -54,7 +54,7 @@ pub async fn get_events_calendar_multiget<C: CalendarStore + ?Sized>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let uid = path.get("uid").unwrap();
|
let uid = path.get("uid").unwrap();
|
||||||
result.push(store.get_event(principal, cid, uid).await?);
|
result.push(store.get_object(principal, cid, uid).await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -67,7 +67,7 @@ pub async fn handle_calendar_multiget<C: CalendarStore + ?Sized>(
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
cal_store: &RwLock<C>,
|
cal_store: &RwLock<C>,
|
||||||
) -> Result<MultistatusElement<PropstatWrapper<EventProp>, String>, Error> {
|
) -> Result<MultistatusElement<PropstatWrapper<CalendarObjectProp>, String>, Error> {
|
||||||
let events =
|
let events =
|
||||||
get_events_calendar_multiget(&cal_multiget, prefix, principal, cid, cal_store).await?;
|
get_events_calendar_multiget(&cal_multiget, prefix, principal, cid, cal_store).await?;
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ pub async fn handle_calendar_multiget<C: CalendarStore + ?Sized>(
|
|||||||
for event in events {
|
for event in events {
|
||||||
let path = format!("{}/{}", req.path(), event.get_uid());
|
let path = format!("{}/{}", req.path(), event.get_uid());
|
||||||
responses.push(
|
responses.push(
|
||||||
EventResource::from(event)
|
CalendarObjectResource::from(event)
|
||||||
.propfind(prefix, &path, props.clone())
|
.propfind(prefix, &path, props.clone())
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ use rustical_dav::{
|
|||||||
resource::HandlePropfind,
|
resource::HandlePropfind,
|
||||||
xml::{multistatus::PropstatWrapper, MultistatusElement},
|
xml::{multistatus::PropstatWrapper, MultistatusElement},
|
||||||
};
|
};
|
||||||
use rustical_store::{model::Event, CalendarStore};
|
use rustical_store::{model::object::CalendarObject, CalendarStore};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
event::resource::{EventProp, EventResource},
|
calendar_object::resource::{CalendarObjectProp, CalendarObjectResource},
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,9 +95,9 @@ pub async fn get_events_calendar_query<C: CalendarStore + ?Sized>(
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
store: &RwLock<C>,
|
store: &RwLock<C>,
|
||||||
) -> Result<Vec<Event>, Error> {
|
) -> Result<Vec<CalendarObject>, Error> {
|
||||||
// TODO: Implement filtering
|
// TODO: Implement filtering
|
||||||
Ok(store.read().await.get_events(principal, cid).await?)
|
Ok(store.read().await.get_objects(principal, cid).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_calendar_query<C: CalendarStore + ?Sized>(
|
pub async fn handle_calendar_query<C: CalendarStore + ?Sized>(
|
||||||
@@ -107,7 +107,7 @@ pub async fn handle_calendar_query<C: CalendarStore + ?Sized>(
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
cal_store: &RwLock<C>,
|
cal_store: &RwLock<C>,
|
||||||
) -> Result<MultistatusElement<PropstatWrapper<EventProp>, String>, Error> {
|
) -> Result<MultistatusElement<PropstatWrapper<CalendarObjectProp>, String>, Error> {
|
||||||
let events = get_events_calendar_query(&cal_query, principal, cid, cal_store).await?;
|
let events = get_events_calendar_query(&cal_query, principal, cid, cal_store).await?;
|
||||||
|
|
||||||
let props = match cal_query.prop {
|
let props = match cal_query.prop {
|
||||||
@@ -126,7 +126,7 @@ pub async fn handle_calendar_query<C: CalendarStore + ?Sized>(
|
|||||||
for event in events {
|
for event in events {
|
||||||
let path = format!("{}/{}", req.path(), event.get_uid());
|
let path = format!("{}/{}", req.path(), event.get_uid());
|
||||||
responses.push(
|
responses.push(
|
||||||
EventResource::from(event)
|
CalendarObjectResource::from(event)
|
||||||
.propfind(prefix, &path, props.clone())
|
.propfind(prefix, &path, props.clone())
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use serde::Deserialize;
|
|||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
event::resource::{EventProp, EventResource},
|
calendar_object::resource::{CalendarObjectProp, CalendarObjectResource},
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ pub async fn handle_sync_collection<C: CalendarStore + ?Sized>(
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
cal_store: &RwLock<C>,
|
cal_store: &RwLock<C>,
|
||||||
) -> Result<MultistatusElement<PropstatWrapper<EventProp>, String>, Error> {
|
) -> Result<MultistatusElement<PropstatWrapper<CalendarObjectProp>, String>, Error> {
|
||||||
let props = match sync_collection.prop {
|
let props = match sync_collection.prop {
|
||||||
PropfindType::Allprop => {
|
PropfindType::Allprop => {
|
||||||
vec!["allprop".to_owned()]
|
vec!["allprop".to_owned()]
|
||||||
@@ -73,7 +73,7 @@ pub async fn handle_sync_collection<C: CalendarStore + ?Sized>(
|
|||||||
for event in new_events {
|
for event in new_events {
|
||||||
let path = format!("{}/{}", req.path(), event.get_uid());
|
let path = format!("{}/{}", req.path(), event.get_uid());
|
||||||
responses.push(
|
responses.push(
|
||||||
EventResource::from(event)
|
CalendarObjectResource::from(event)
|
||||||
.propfind(prefix, &path, props.clone())
|
.propfind(prefix, &path, props.clone())
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::event::resource::EventResource;
|
use crate::calendar_object::resource::CalendarObjectResource;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use actix_web::{web::Data, HttpRequest};
|
use actix_web::{web::Data, HttpRequest};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -218,7 +218,7 @@ impl Resource for CalendarResource {
|
|||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
impl<C: CalendarStore + ?Sized> ResourceService for CalendarResourceService<C> {
|
impl<C: CalendarStore + ?Sized> ResourceService for CalendarResourceService<C> {
|
||||||
type MemberType = EventResource;
|
type MemberType = CalendarObjectResource;
|
||||||
type PathComponents = (String, String); // principal, calendar_id
|
type PathComponents = (String, String); // principal, calendar_id
|
||||||
type Resource = CalendarResource;
|
type Resource = CalendarResource;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
@@ -243,7 +243,7 @@ impl<C: CalendarStore + ?Sized> ResourceService for CalendarResourceService<C> {
|
|||||||
.cal_store
|
.cal_store
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
.get_events(&self.principal, &self.calendar_id)
|
.get_objects(&self.principal, &self.calendar_id)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|event| (format!("{}/{}", self.path, &event.get_uid()), event.into()))
|
.map(|event| (format!("{}/{}", self.path, &event.get_uid()), event.into()))
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub async fn get_event<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
|||||||
.store
|
.store
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
.get_event(&principal, &cid, &uid)
|
.get_object(&principal, &cid, &uid)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
@@ -79,7 +79,7 @@ pub async fn put_event<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
|||||||
|
|
||||||
if Some(&HeaderValue::from_static("*")) == req.headers().get(header::IF_NONE_MATCH) {
|
if Some(&HeaderValue::from_static("*")) == req.headers().get(header::IF_NONE_MATCH) {
|
||||||
// Only write if not existing
|
// Only write if not existing
|
||||||
match store.get_event(&principal, &cid, &uid).await {
|
match store.get_object(&principal, &cid, &uid).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Conflict
|
// Conflict
|
||||||
return Ok(HttpResponse::Conflict().body("Resource with this URI already existing"));
|
return Ok(HttpResponse::Conflict().body("Resource with this URI already existing"));
|
||||||
@@ -94,7 +94,7 @@ pub async fn put_event<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
store.put_event(principal, cid, uid, body).await?;
|
store.put_object(principal, cid, uid, body).await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Created().body(""))
|
Ok(HttpResponse::Created().body(""))
|
||||||
}
|
}
|
||||||
@@ -3,14 +3,14 @@ use actix_web::{web::Data, HttpRequest};
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
use rustical_dav::resource::{InvalidProperty, Resource, ResourceService};
|
use rustical_dav::resource::{InvalidProperty, Resource, ResourceService};
|
||||||
use rustical_store::model::Event;
|
use rustical_store::model::object::CalendarObject;
|
||||||
use rustical_store::CalendarStore;
|
use rustical_store::CalendarStore;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use strum::{EnumString, VariantNames};
|
use strum::{EnumString, VariantNames};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
pub struct EventResourceService<C: CalendarStore + ?Sized> {
|
pub struct CalendarObjectResourceService<C: CalendarStore + ?Sized> {
|
||||||
pub cal_store: Arc<RwLock<C>>,
|
pub cal_store: Arc<RwLock<C>>,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub principal: String,
|
pub principal: String,
|
||||||
@@ -20,7 +20,7 @@ pub struct EventResourceService<C: CalendarStore + ?Sized> {
|
|||||||
|
|
||||||
#[derive(EnumString, Debug, VariantNames, Clone)]
|
#[derive(EnumString, Debug, VariantNames, Clone)]
|
||||||
#[strum(serialize_all = "kebab-case")]
|
#[strum(serialize_all = "kebab-case")]
|
||||||
pub enum EventPropName {
|
pub enum CalendarObjectPropName {
|
||||||
Getetag,
|
Getetag,
|
||||||
CalendarData,
|
CalendarData,
|
||||||
Getcontenttype,
|
Getcontenttype,
|
||||||
@@ -28,7 +28,7 @@ pub enum EventPropName {
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum EventProp {
|
pub enum CalendarObjectProp {
|
||||||
Getetag(String),
|
Getetag(String),
|
||||||
#[serde(rename = "C:calendar-data")]
|
#[serde(rename = "C:calendar-data")]
|
||||||
CalendarData(String),
|
CalendarData(String),
|
||||||
@@ -37,36 +37,38 @@ pub enum EventProp {
|
|||||||
Invalid,
|
Invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InvalidProperty for EventProp {
|
impl InvalidProperty for CalendarObjectProp {
|
||||||
fn invalid_property(&self) -> bool {
|
fn invalid_property(&self) -> bool {
|
||||||
matches!(self, Self::Invalid)
|
matches!(self, Self::Invalid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, From, Into)]
|
#[derive(Clone, From, Into)]
|
||||||
pub struct EventResource(Event);
|
pub struct CalendarObjectResource(CalendarObject);
|
||||||
|
|
||||||
impl Resource for EventResource {
|
impl Resource for CalendarObjectResource {
|
||||||
type PropName = EventPropName;
|
type PropName = CalendarObjectPropName;
|
||||||
type Prop = EventProp;
|
type Prop = CalendarObjectProp;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn get_prop(&self, _prefix: &str, prop: Self::PropName) -> Result<Self::Prop, Self::Error> {
|
fn get_prop(&self, _prefix: &str, prop: Self::PropName) -> Result<Self::Prop, Self::Error> {
|
||||||
Ok(match prop {
|
Ok(match prop {
|
||||||
EventPropName::Getetag => EventProp::Getetag(self.0.get_etag()),
|
CalendarObjectPropName::Getetag => CalendarObjectProp::Getetag(self.0.get_etag()),
|
||||||
EventPropName::CalendarData => EventProp::CalendarData(self.0.get_ics().to_owned()),
|
CalendarObjectPropName::CalendarData => {
|
||||||
EventPropName::Getcontenttype => {
|
CalendarObjectProp::CalendarData(self.0.get_ics().to_owned())
|
||||||
EventProp::Getcontenttype("text/calendar;charset=utf-8".to_owned())
|
}
|
||||||
|
CalendarObjectPropName::Getcontenttype => {
|
||||||
|
CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8".to_owned())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
impl<C: CalendarStore + ?Sized> ResourceService for EventResourceService<C> {
|
impl<C: CalendarStore + ?Sized> ResourceService for CalendarObjectResourceService<C> {
|
||||||
type PathComponents = (String, String, String); // principal, calendar, event
|
type PathComponents = (String, String, String); // principal, calendar, event
|
||||||
type Resource = EventResource;
|
type Resource = CalendarObjectResource;
|
||||||
type MemberType = EventResource;
|
type MemberType = CalendarObjectResource;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
async fn new(
|
async fn new(
|
||||||
@@ -102,7 +104,7 @@ impl<C: CalendarStore + ?Sized> ResourceService for EventResourceService<C> {
|
|||||||
.cal_store
|
.cal_store
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
.get_event(&self.principal, &self.cid, &self.uid)
|
.get_object(&self.principal, &self.cid, &self.uid)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(event.into())
|
Ok(event.into())
|
||||||
}
|
}
|
||||||
@@ -115,7 +117,7 @@ impl<C: CalendarStore + ?Sized> ResourceService for EventResourceService<C> {
|
|||||||
self.cal_store
|
self.cal_store
|
||||||
.write()
|
.write()
|
||||||
.await
|
.await
|
||||||
.delete_event(&self.principal, &self.cid, &self.uid, use_trashbin)
|
.delete_object(&self.principal, &self.cid, &self.uid, use_trashbin)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ use actix_web::http::Method;
|
|||||||
use actix_web::web::{self, Data};
|
use actix_web::web::{self, Data};
|
||||||
use actix_web::{guard, HttpResponse, Responder};
|
use actix_web::{guard, HttpResponse, Responder};
|
||||||
use calendar::resource::CalendarResourceService;
|
use calendar::resource::CalendarResourceService;
|
||||||
use event::resource::EventResourceService;
|
use calendar_object::resource::CalendarObjectResourceService;
|
||||||
use principal::PrincipalResourceService;
|
use principal::PrincipalResourceService;
|
||||||
use root::RootResourceService;
|
use root::RootResourceService;
|
||||||
use rustical_auth::CheckAuthentication;
|
use rustical_auth::CheckAuthentication;
|
||||||
@@ -15,8 +15,8 @@ use std::sync::Arc;
|
|||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
|
pub mod calendar_object;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod event;
|
|
||||||
pub mod principal;
|
pub mod principal;
|
||||||
pub mod root;
|
pub mod root;
|
||||||
|
|
||||||
@@ -102,21 +102,23 @@ pub fn configure_dav<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
|||||||
web::resource("/{event}")
|
web::resource("/{event}")
|
||||||
.route(
|
.route(
|
||||||
propfind_method()
|
propfind_method()
|
||||||
.to(route_propfind::<A, EventResourceService<C>>),
|
.to(route_propfind::<A, CalendarObjectResourceService<C>>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
proppatch_method()
|
proppatch_method()
|
||||||
.to(route_proppatch::<A, EventResourceService<C>>),
|
.to(route_proppatch::<A, CalendarObjectResourceService<C>>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
web::method(Method::DELETE)
|
web::method(Method::DELETE)
|
||||||
.to(route_delete::<A, EventResourceService<C>>),
|
.to(route_delete::<A, CalendarObjectResourceService<C>>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
web::method(Method::GET).to(event::methods::get_event::<A, C>),
|
web::method(Method::GET)
|
||||||
|
.to(calendar_object::methods::get_event::<A, C>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
web::method(Method::PUT).to(event::methods::put_event::<A, C>),
|
web::method(Method::PUT)
|
||||||
|
.to(calendar_object::methods::put_event::<A, C>),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,96 +1,21 @@
|
|||||||
use crate::{
|
use std::str::FromStr;
|
||||||
timestamp::{parse_duration, CalDateTime},
|
|
||||||
Error,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use ical::parser::{ical::component::IcalCalendar, Component};
|
use ical::{generator::IcalEvent, parser::Component};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sha2::{Digest, Sha256};
|
use crate::timestamp::{parse_duration, CalDateTime};
|
||||||
use std::{io::BufReader, str::FromStr};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Event {
|
pub struct EventObject {
|
||||||
uid: String,
|
pub(crate) event: IcalEvent,
|
||||||
ics: String,
|
|
||||||
cal: IcalCalendar,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom implementation for Event (de)serialization
|
impl EventObject {
|
||||||
impl<'de> Deserialize<'de> for Event {
|
|
||||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Inner {
|
|
||||||
uid: String,
|
|
||||||
ics: String,
|
|
||||||
}
|
|
||||||
let Inner { uid, ics } = Inner::deserialize(deserializer)?;
|
|
||||||
Self::from_ics(uid, ics).map_err(serde::de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Serialize for Event {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Inner {
|
|
||||||
uid: String,
|
|
||||||
ics: String,
|
|
||||||
}
|
|
||||||
Inner::serialize(
|
|
||||||
&Inner {
|
|
||||||
uid: self.get_uid().to_string(),
|
|
||||||
ics: self.get_ics().to_string(),
|
|
||||||
},
|
|
||||||
serializer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Event {
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-4.1
|
|
||||||
// MUST NOT contain more than one calendar objects (VEVENT, VTODO, VJOURNAL)
|
|
||||||
pub fn from_ics(uid: 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)??;
|
|
||||||
if parser.next().is_some() {
|
|
||||||
return Err(Error::InvalidIcs(
|
|
||||||
"multiple calendars, only one allowed".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if cal.events.len() != 1 {
|
|
||||||
return Err(Error::InvalidIcs(
|
|
||||||
"ics calendar must contain exactly one event".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let event = Self { uid, cal, ics };
|
|
||||||
// Run getters now to validate the input and ensure that they'll work later on
|
|
||||||
event.get_first_occurence()?;
|
|
||||||
event.get_last_occurence()?;
|
|
||||||
Ok(event)
|
|
||||||
}
|
|
||||||
pub fn get_uid(&self) -> &str {
|
|
||||||
&self.uid
|
|
||||||
}
|
|
||||||
pub fn get_etag(&self) -> String {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(&self.uid);
|
|
||||||
hasher.update(self.get_ics());
|
|
||||||
format!("{:x}", hasher.finalize())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ics(&self) -> &str {
|
|
||||||
&self.ics
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_first_occurence(&self) -> Result<CalDateTime> {
|
pub fn get_first_occurence(&self) -> Result<CalDateTime> {
|
||||||
// This is safe since we enforce the event's existance in the constructor
|
// This is safe since we enforce the event's existance in the constructor
|
||||||
let event = self.cal.events.first().unwrap();
|
let dtstart = self
|
||||||
let dtstart = event
|
.event
|
||||||
.get_property("DTSTART")
|
.get_property("DTSTART")
|
||||||
.ok_or(anyhow!("DTSTART property missing!"))?
|
.ok_or(anyhow!("DTSTART property missing!"))?
|
||||||
.value
|
.value
|
||||||
@@ -101,14 +26,12 @@ impl Event {
|
|||||||
|
|
||||||
pub fn get_last_occurence(&self) -> Result<CalDateTime> {
|
pub fn get_last_occurence(&self) -> Result<CalDateTime> {
|
||||||
// This is safe since we enforce the event's existence in the constructor
|
// This is safe since we enforce the event's existence in the constructor
|
||||||
let event = self.cal.events.first().unwrap();
|
if self.event.get_property("RRULE").is_some() {
|
||||||
|
|
||||||
if event.get_property("RRULE").is_some() {
|
|
||||||
// TODO: understand recurrence rules
|
// TODO: understand recurrence rules
|
||||||
return Err(anyhow!("event is recurring, we cannot handle that yet"));
|
return Err(anyhow!("event is recurring, we cannot handle that yet"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dtend_prop) = event.get_property("DTEND") {
|
if let Some(dtend_prop) = self.event.get_property("DTEND") {
|
||||||
let dtend = dtend_prop
|
let dtend = dtend_prop
|
||||||
.value
|
.value
|
||||||
.to_owned()
|
.to_owned()
|
||||||
@@ -116,7 +39,7 @@ impl Event {
|
|||||||
return Ok(CalDateTime::from_str(&dtend)?);
|
return Ok(CalDateTime::from_str(&dtend)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dtend_prop) = event.get_property("DURATION") {
|
if let Some(dtend_prop) = self.event.get_property("DURATION") {
|
||||||
let duration = dtend_prop
|
let duration = dtend_prop
|
||||||
.value
|
.value
|
||||||
.to_owned()
|
.to_owned()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod object;
|
||||||
|
|
||||||
pub use calendar::Calendar;
|
pub use calendar::Calendar;
|
||||||
pub use event::Event;
|
pub use object::CalendarObject;
|
||||||
|
|||||||
129
crates/store/src/model/object.rs
Normal file
129
crates/store/src/model/object.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
use super::event::EventObject;
|
||||||
|
use crate::Error;
|
||||||
|
use anyhow::Result;
|
||||||
|
use ical::parser::ical::component::IcalTodo;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::io::BufReader;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
// specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6
|
||||||
|
pub enum CalendarObjectType {
|
||||||
|
Event,
|
||||||
|
Journal,
|
||||||
|
Todo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TodoObject {
|
||||||
|
todo: IcalTodo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CalendarObjectComponent {
|
||||||
|
Event(EventObject),
|
||||||
|
Todo(TodoObject),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CalendarObject {
|
||||||
|
uid: String,
|
||||||
|
ics: String,
|
||||||
|
data: CalendarObjectComponent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom implementation for CalendarObject (de)serialization
|
||||||
|
impl<'de> Deserialize<'de> for CalendarObject {
|
||||||
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Inner {
|
||||||
|
uid: String,
|
||||||
|
ics: String,
|
||||||
|
}
|
||||||
|
let Inner { uid, ics } = Inner::deserialize(deserializer)?;
|
||||||
|
Self::from_ics(uid, ics).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for CalendarObject {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Inner {
|
||||||
|
uid: String,
|
||||||
|
ics: String,
|
||||||
|
}
|
||||||
|
Inner::serialize(
|
||||||
|
&Inner {
|
||||||
|
uid: self.get_uid().to_string(),
|
||||||
|
ics: self.get_ics().to_string(),
|
||||||
|
},
|
||||||
|
serializer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalendarObject {
|
||||||
|
pub fn from_ics(uid: 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)??;
|
||||||
|
if parser.next().is_some() {
|
||||||
|
return Err(Error::InvalidIcs(
|
||||||
|
"multiple calendars, only one allowed".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if cal.events.len()
|
||||||
|
+ cal.alarms.len()
|
||||||
|
+ cal.todos.len()
|
||||||
|
+ cal.journals.len()
|
||||||
|
+ cal.free_busys.len()
|
||||||
|
!= 1
|
||||||
|
{
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4791#section-4.1
|
||||||
|
return Err(Error::InvalidIcs(
|
||||||
|
"iCalendar object is only allowed to have exactly one component".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(event) = cal.events.first() {
|
||||||
|
return Ok(CalendarObject {
|
||||||
|
uid,
|
||||||
|
ics,
|
||||||
|
data: CalendarObjectComponent::Event(EventObject {
|
||||||
|
event: event.clone(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(todo) = cal.todos.first() {
|
||||||
|
return Ok(CalendarObject {
|
||||||
|
uid,
|
||||||
|
ics,
|
||||||
|
data: CalendarObjectComponent::Todo(TodoObject { todo: todo.clone() }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::InvalidIcs(
|
||||||
|
"iCalendar component type not supported :(".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_uid(&self) -> &str {
|
||||||
|
&self.uid
|
||||||
|
}
|
||||||
|
pub fn get_etag(&self) -> String {
|
||||||
|
// TODO: Probably save it in the database so we don't have to recompute every time?
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&self.uid);
|
||||||
|
hasher.update(self.get_ics());
|
||||||
|
format!("{:x}", hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ics(&self) -> &str {
|
||||||
|
&self.ics
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::model::object::CalendarObject;
|
||||||
use crate::model::Calendar;
|
use crate::model::Calendar;
|
||||||
use crate::model::Event;
|
|
||||||
use crate::{CalendarStore, Error};
|
use crate::{CalendarStore, Error};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -19,16 +19,16 @@ impl SqliteCalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct EventRow {
|
struct CalendarObjectRow {
|
||||||
uid: String,
|
uid: String,
|
||||||
ics: String,
|
ics: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<EventRow> for Event {
|
impl TryFrom<CalendarObjectRow> for CalendarObject {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: EventRow) -> Result<Self, Error> {
|
fn try_from(value: CalendarObjectRow) -> Result<Self, Error> {
|
||||||
Event::from_ics(value.uid, value.ics)
|
CalendarObject::from_ics(value.uid, value.ics)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,9 +186,9 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_events(&self, principal: &str, cid: &str) -> Result<Vec<Event>, Error> {
|
async fn get_objects(&self, principal: &str, cid: &str) -> Result<Vec<CalendarObject>, Error> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
EventRow,
|
CalendarObjectRow,
|
||||||
"SELECT uid, ics FROM events WHERE principal = ? AND cid = ? AND deleted_at IS NULL",
|
"SELECT uid, ics FROM events WHERE principal = ? AND cid = ? AND deleted_at IS NULL",
|
||||||
principal,
|
principal,
|
||||||
cid
|
cid
|
||||||
@@ -200,9 +200,14 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_event(&self, principal: &str, cid: &str, uid: &str) -> Result<Event, Error> {
|
async fn get_object(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cid: &str,
|
||||||
|
uid: &str,
|
||||||
|
) -> Result<CalendarObject, Error> {
|
||||||
let event = sqlx::query_as!(
|
let event = sqlx::query_as!(
|
||||||
EventRow,
|
CalendarObjectRow,
|
||||||
"SELECT uid, ics FROM events WHERE (principal, cid, uid) = (?, ?, ?)",
|
"SELECT uid, ics FROM events WHERE (principal, cid, uid) = (?, ?, ?)",
|
||||||
principal,
|
principal,
|
||||||
cid,
|
cid,
|
||||||
@@ -214,7 +219,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Ok(event)
|
Ok(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn put_event(
|
async fn put_object(
|
||||||
&mut self,
|
&mut self,
|
||||||
principal: String,
|
principal: String,
|
||||||
cid: String,
|
cid: String,
|
||||||
@@ -224,7 +229,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
let mut tx = self.db.begin().await?;
|
let mut tx = self.db.begin().await?;
|
||||||
|
|
||||||
// input validation
|
// input validation
|
||||||
Event::from_ics(uid.to_owned(), ics.to_owned())?;
|
CalendarObject::from_ics(uid.to_owned(), ics.to_owned())?;
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"REPLACE INTO events (principal, cid, uid, ics) VALUES (?, ?, ?, ?)",
|
"REPLACE INTO events (principal, cid, uid, ics) VALUES (?, ?, ?, ?)",
|
||||||
principal,
|
principal,
|
||||||
@@ -247,7 +252,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_event(
|
async fn delete_object(
|
||||||
&mut self,
|
&mut self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
@@ -285,7 +290,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_event(&mut self, principal: &str, cid: &str, uid: &str) -> Result<(), Error> {
|
async fn restore_object(&mut self, principal: &str, cid: &str, uid: &str) -> Result<(), Error> {
|
||||||
let mut tx = self.db.begin().await?;
|
let mut tx = self.db.begin().await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -314,7 +319,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
) -> Result<(Vec<Event>, Vec<String>, i64), Error> {
|
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
||||||
struct Row {
|
struct Row {
|
||||||
uid: String,
|
uid: String,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
@@ -340,7 +345,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
for Row { uid, .. } in changes {
|
for Row { uid, .. } in changes {
|
||||||
match self.get_event(principal, cid, &uid).await {
|
match self.get_object(principal, cid, &uid).await {
|
||||||
Ok(event) => events.push(event),
|
Ok(event) => events.push(event),
|
||||||
Err(Error::NotFound) => deleted_events.push(uid),
|
Err(Error::NotFound) => deleted_events.push(uid),
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use anyhow::Result;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::model::{Calendar, Event};
|
use crate::model::object::CalendarObject;
|
||||||
|
use crate::model::Calendar;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait CalendarStore: Send + Sync + 'static {
|
pub trait CalendarStore: Send + Sync + 'static {
|
||||||
@@ -29,23 +30,28 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
) -> Result<(Vec<Event>, Vec<String>, i64), Error>;
|
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error>;
|
||||||
|
|
||||||
async fn get_events(&self, principal: &str, cid: &str) -> Result<Vec<Event>, Error>;
|
async fn get_objects(&self, principal: &str, cid: &str) -> Result<Vec<CalendarObject>, Error>;
|
||||||
async fn get_event(&self, principal: &str, cid: &str, uid: &str) -> Result<Event, Error>;
|
async fn get_object(
|
||||||
async fn put_event(
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cid: &str,
|
||||||
|
uid: &str,
|
||||||
|
) -> Result<CalendarObject, Error>;
|
||||||
|
async fn put_object(
|
||||||
&mut self,
|
&mut self,
|
||||||
principal: String,
|
principal: String,
|
||||||
cid: String,
|
cid: String,
|
||||||
uid: String,
|
uid: String,
|
||||||
ics: String,
|
ics: String,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
async fn delete_event(
|
async fn delete_object(
|
||||||
&mut self,
|
&mut self,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
cid: &str,
|
cid: &str,
|
||||||
uid: &str,
|
uid: &str,
|
||||||
use_trashbin: bool,
|
use_trashbin: bool,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
async fn restore_event(&mut self, principal: &str, cid: &str, uid: &str) -> Result<(), Error>;
|
async fn restore_object(&mut self, principal: &str, cid: &str, uid: &str) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async fn test_create_event<CS: CalendarStore>(mut store: CS) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
store
|
store
|
||||||
.put_event(
|
.put_object(
|
||||||
"testuser".to_owned(),
|
"testuser".to_owned(),
|
||||||
"test".to_owned(),
|
"test".to_owned(),
|
||||||
"asd".to_owned(),
|
"asd".to_owned(),
|
||||||
@@ -46,7 +46,7 @@ async fn test_create_event<CS: CalendarStore>(mut store: CS) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let event = store.get_event("testuser", "test", "asd").await.unwrap();
|
let event = store.get_object("testuser", "test", "asd").await.unwrap();
|
||||||
assert_eq!(event.get_ics(), EVENT);
|
assert_eq!(event.get_ics(), EVENT);
|
||||||
assert_eq!(event.get_uid(), "asd");
|
assert_eq!(event.get_uid(), "asd");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user