Migrate from Event type to CalendarObject

This is preparation to support other calendar components like VTODO and
VJOURNAL
This commit is contained in:
Lennart
2024-09-30 19:35:54 +02:00
parent 41d68f9ae0
commit b3a7806139
14 changed files with 229 additions and 161 deletions

View File

@@ -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?,
); );

View File

@@ -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?,
); );

View File

@@ -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?,
); );

View File

@@ -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()))

View File

@@ -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(""))
} }

View File

@@ -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(())
} }

View File

@@ -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>),
), ),
), ),
), ),

View File

@@ -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()

View File

@@ -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;

View 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
}
}

View File

@@ -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),

View File

@@ -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>;
} }

View File

@@ -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");
} }