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::{
|
||||
event::resource::{EventProp, EventResource},
|
||||
calendar_object::resource::{CalendarObjectProp, CalendarObjectResource},
|
||||
Error,
|
||||
};
|
||||
use actix_web::{
|
||||
@@ -11,7 +11,7 @@ use rustical_dav::{
|
||||
resource::HandlePropfind,
|
||||
xml::{multistatus::PropstatWrapper, MultistatusElement},
|
||||
};
|
||||
use rustical_store::{model::Event, CalendarStore};
|
||||
use rustical_store::{model::object::CalendarObject, CalendarStore};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -31,7 +31,7 @@ pub async fn get_events_calendar_multiget<C: CalendarStore + ?Sized>(
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
store: &RwLock<C>,
|
||||
) -> Result<Vec<Event>, Error> {
|
||||
) -> Result<Vec<CalendarObject>, Error> {
|
||||
// TODO: add proper error results for single events
|
||||
let resource_def =
|
||||
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;
|
||||
}
|
||||
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)
|
||||
@@ -67,7 +67,7 @@ pub async fn handle_calendar_multiget<C: CalendarStore + ?Sized>(
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
cal_store: &RwLock<C>,
|
||||
) -> Result<MultistatusElement<PropstatWrapper<EventProp>, String>, Error> {
|
||||
) -> Result<MultistatusElement<PropstatWrapper<CalendarObjectProp>, String>, Error> {
|
||||
let events =
|
||||
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 {
|
||||
let path = format!("{}/{}", req.path(), event.get_uid());
|
||||
responses.push(
|
||||
EventResource::from(event)
|
||||
CalendarObjectResource::from(event)
|
||||
.propfind(prefix, &path, props.clone())
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -4,12 +4,12 @@ use rustical_dav::{
|
||||
resource::HandlePropfind,
|
||||
xml::{multistatus::PropstatWrapper, MultistatusElement},
|
||||
};
|
||||
use rustical_store::{model::Event, CalendarStore};
|
||||
use rustical_store::{model::object::CalendarObject, CalendarStore};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
event::resource::{EventProp, EventResource},
|
||||
calendar_object::resource::{CalendarObjectProp, CalendarObjectResource},
|
||||
Error,
|
||||
};
|
||||
|
||||
@@ -95,9 +95,9 @@ pub async fn get_events_calendar_query<C: CalendarStore + ?Sized>(
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
store: &RwLock<C>,
|
||||
) -> Result<Vec<Event>, Error> {
|
||||
) -> Result<Vec<CalendarObject>, Error> {
|
||||
// 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>(
|
||||
@@ -107,7 +107,7 @@ pub async fn handle_calendar_query<C: CalendarStore + ?Sized>(
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
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 props = match cal_query.prop {
|
||||
@@ -126,7 +126,7 @@ pub async fn handle_calendar_query<C: CalendarStore + ?Sized>(
|
||||
for event in events {
|
||||
let path = format!("{}/{}", req.path(), event.get_uid());
|
||||
responses.push(
|
||||
EventResource::from(event)
|
||||
CalendarObjectResource::from(event)
|
||||
.propfind(prefix, &path, props.clone())
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ use serde::Deserialize;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
event::resource::{EventProp, EventResource},
|
||||
calendar_object::resource::{CalendarObjectProp, CalendarObjectResource},
|
||||
Error,
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ pub async fn handle_sync_collection<C: CalendarStore + ?Sized>(
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
cal_store: &RwLock<C>,
|
||||
) -> Result<MultistatusElement<PropstatWrapper<EventProp>, String>, Error> {
|
||||
) -> Result<MultistatusElement<PropstatWrapper<CalendarObjectProp>, String>, Error> {
|
||||
let props = match sync_collection.prop {
|
||||
PropfindType::Allprop => {
|
||||
vec!["allprop".to_owned()]
|
||||
@@ -73,7 +73,7 @@ pub async fn handle_sync_collection<C: CalendarStore + ?Sized>(
|
||||
for event in new_events {
|
||||
let path = format!("{}/{}", req.path(), event.get_uid());
|
||||
responses.push(
|
||||
EventResource::from(event)
|
||||
CalendarObjectResource::from(event)
|
||||
.propfind(prefix, &path, props.clone())
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::event::resource::EventResource;
|
||||
use crate::calendar_object::resource::CalendarObjectResource;
|
||||
use crate::Error;
|
||||
use actix_web::{web::Data, HttpRequest};
|
||||
use async_trait::async_trait;
|
||||
@@ -218,7 +218,7 @@ impl Resource for CalendarResource {
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<C: CalendarStore + ?Sized> ResourceService for CalendarResourceService<C> {
|
||||
type MemberType = EventResource;
|
||||
type MemberType = CalendarObjectResource;
|
||||
type PathComponents = (String, String); // principal, calendar_id
|
||||
type Resource = CalendarResource;
|
||||
type Error = Error;
|
||||
@@ -243,7 +243,7 @@ impl<C: CalendarStore + ?Sized> ResourceService for CalendarResourceService<C> {
|
||||
.cal_store
|
||||
.read()
|
||||
.await
|
||||
.get_events(&self.principal, &self.calendar_id)
|
||||
.get_objects(&self.principal, &self.calendar_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.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
|
||||
.read()
|
||||
.await
|
||||
.get_event(&principal, &cid, &uid)
|
||||
.get_object(&principal, &cid, &uid)
|
||||
.await?;
|
||||
|
||||
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) {
|
||||
// Only write if not existing
|
||||
match store.get_event(&principal, &cid, &uid).await {
|
||||
match store.get_object(&principal, &cid, &uid).await {
|
||||
Ok(_) => {
|
||||
// Conflict
|
||||
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(""))
|
||||
}
|
||||
@@ -3,14 +3,14 @@ use actix_web::{web::Data, HttpRequest};
|
||||
use async_trait::async_trait;
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::resource::{InvalidProperty, Resource, ResourceService};
|
||||
use rustical_store::model::Event;
|
||||
use rustical_store::model::object::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use strum::{EnumString, VariantNames};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub struct EventResourceService<C: CalendarStore + ?Sized> {
|
||||
pub struct CalendarObjectResourceService<C: CalendarStore + ?Sized> {
|
||||
pub cal_store: Arc<RwLock<C>>,
|
||||
pub path: String,
|
||||
pub principal: String,
|
||||
@@ -20,7 +20,7 @@ pub struct EventResourceService<C: CalendarStore + ?Sized> {
|
||||
|
||||
#[derive(EnumString, Debug, VariantNames, Clone)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum EventPropName {
|
||||
pub enum CalendarObjectPropName {
|
||||
Getetag,
|
||||
CalendarData,
|
||||
Getcontenttype,
|
||||
@@ -28,7 +28,7 @@ pub enum EventPropName {
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum EventProp {
|
||||
pub enum CalendarObjectProp {
|
||||
Getetag(String),
|
||||
#[serde(rename = "C:calendar-data")]
|
||||
CalendarData(String),
|
||||
@@ -37,36 +37,38 @@ pub enum EventProp {
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl InvalidProperty for EventProp {
|
||||
impl InvalidProperty for CalendarObjectProp {
|
||||
fn invalid_property(&self) -> bool {
|
||||
matches!(self, Self::Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, From, Into)]
|
||||
pub struct EventResource(Event);
|
||||
pub struct CalendarObjectResource(CalendarObject);
|
||||
|
||||
impl Resource for EventResource {
|
||||
type PropName = EventPropName;
|
||||
type Prop = EventProp;
|
||||
impl Resource for CalendarObjectResource {
|
||||
type PropName = CalendarObjectPropName;
|
||||
type Prop = CalendarObjectProp;
|
||||
type Error = Error;
|
||||
|
||||
fn get_prop(&self, _prefix: &str, prop: Self::PropName) -> Result<Self::Prop, Self::Error> {
|
||||
Ok(match prop {
|
||||
EventPropName::Getetag => EventProp::Getetag(self.0.get_etag()),
|
||||
EventPropName::CalendarData => EventProp::CalendarData(self.0.get_ics().to_owned()),
|
||||
EventPropName::Getcontenttype => {
|
||||
EventProp::Getcontenttype("text/calendar;charset=utf-8".to_owned())
|
||||
CalendarObjectPropName::Getetag => CalendarObjectProp::Getetag(self.0.get_etag()),
|
||||
CalendarObjectPropName::CalendarData => {
|
||||
CalendarObjectProp::CalendarData(self.0.get_ics().to_owned())
|
||||
}
|
||||
CalendarObjectPropName::Getcontenttype => {
|
||||
CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8".to_owned())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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 Resource = EventResource;
|
||||
type MemberType = EventResource;
|
||||
type Resource = CalendarObjectResource;
|
||||
type MemberType = CalendarObjectResource;
|
||||
type Error = Error;
|
||||
|
||||
async fn new(
|
||||
@@ -102,7 +104,7 @@ impl<C: CalendarStore + ?Sized> ResourceService for EventResourceService<C> {
|
||||
.cal_store
|
||||
.read()
|
||||
.await
|
||||
.get_event(&self.principal, &self.cid, &self.uid)
|
||||
.get_object(&self.principal, &self.cid, &self.uid)
|
||||
.await?;
|
||||
Ok(event.into())
|
||||
}
|
||||
@@ -115,7 +117,7 @@ impl<C: CalendarStore + ?Sized> ResourceService for EventResourceService<C> {
|
||||
self.cal_store
|
||||
.write()
|
||||
.await
|
||||
.delete_event(&self.principal, &self.cid, &self.uid, use_trashbin)
|
||||
.delete_object(&self.principal, &self.cid, &self.uid, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use actix_web::http::Method;
|
||||
use actix_web::web::{self, Data};
|
||||
use actix_web::{guard, HttpResponse, Responder};
|
||||
use calendar::resource::CalendarResourceService;
|
||||
use event::resource::EventResourceService;
|
||||
use calendar_object::resource::CalendarObjectResourceService;
|
||||
use principal::PrincipalResourceService;
|
||||
use root::RootResourceService;
|
||||
use rustical_auth::CheckAuthentication;
|
||||
@@ -15,8 +15,8 @@ use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub mod calendar;
|
||||
pub mod calendar_object;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod principal;
|
||||
pub mod root;
|
||||
|
||||
@@ -102,21 +102,23 @@ pub fn configure_dav<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
||||
web::resource("/{event}")
|
||||
.route(
|
||||
propfind_method()
|
||||
.to(route_propfind::<A, EventResourceService<C>>),
|
||||
.to(route_propfind::<A, CalendarObjectResourceService<C>>),
|
||||
)
|
||||
.route(
|
||||
proppatch_method()
|
||||
.to(route_proppatch::<A, EventResourceService<C>>),
|
||||
.to(route_proppatch::<A, CalendarObjectResourceService<C>>),
|
||||
)
|
||||
.route(
|
||||
web::method(Method::DELETE)
|
||||
.to(route_delete::<A, EventResourceService<C>>),
|
||||
.to(route_delete::<A, CalendarObjectResourceService<C>>),
|
||||
)
|
||||
.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(
|
||||
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::{
|
||||
timestamp::{parse_duration, CalDateTime},
|
||||
Error,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::Duration;
|
||||
use ical::parser::{ical::component::IcalCalendar, Component};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{io::BufReader, str::FromStr};
|
||||
use ical::{generator::IcalEvent, parser::Component};
|
||||
|
||||
use crate::timestamp::{parse_duration, CalDateTime};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Event {
|
||||
uid: String,
|
||||
ics: String,
|
||||
cal: IcalCalendar,
|
||||
}
|
||||
|
||||
// Custom implementation for Event (de)serialization
|
||||
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 struct EventObject {
|
||||
pub(crate) event: IcalEvent,
|
||||
}
|
||||
|
||||
impl EventObject {
|
||||
pub fn get_first_occurence(&self) -> Result<CalDateTime> {
|
||||
// This is safe since we enforce the event's existance in the constructor
|
||||
let event = self.cal.events.first().unwrap();
|
||||
let dtstart = event
|
||||
let dtstart = self
|
||||
.event
|
||||
.get_property("DTSTART")
|
||||
.ok_or(anyhow!("DTSTART property missing!"))?
|
||||
.value
|
||||
@@ -101,14 +26,12 @@ impl Event {
|
||||
|
||||
pub fn get_last_occurence(&self) -> Result<CalDateTime> {
|
||||
// This is safe since we enforce the event's existence in the constructor
|
||||
let event = self.cal.events.first().unwrap();
|
||||
|
||||
if event.get_property("RRULE").is_some() {
|
||||
if self.event.get_property("RRULE").is_some() {
|
||||
// TODO: understand recurrence rules
|
||||
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
|
||||
.value
|
||||
.to_owned()
|
||||
@@ -116,7 +39,7 @@ impl Event {
|
||||
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
|
||||
.value
|
||||
.to_owned()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod calendar;
|
||||
pub mod event;
|
||||
pub mod object;
|
||||
|
||||
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::Event;
|
||||
use crate::{CalendarStore, Error};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
@@ -19,16 +19,16 @@ impl SqliteCalendarStore {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EventRow {
|
||||
struct CalendarObjectRow {
|
||||
uid: String,
|
||||
ics: String,
|
||||
}
|
||||
|
||||
impl TryFrom<EventRow> for Event {
|
||||
impl TryFrom<CalendarObjectRow> for CalendarObject {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: EventRow) -> Result<Self, Error> {
|
||||
Event::from_ics(value.uid, value.ics)
|
||||
fn try_from(value: CalendarObjectRow) -> Result<Self, Error> {
|
||||
CalendarObject::from_ics(value.uid, value.ics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +186,9 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
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!(
|
||||
EventRow,
|
||||
CalendarObjectRow,
|
||||
"SELECT uid, ics FROM events WHERE principal = ? AND cid = ? AND deleted_at IS NULL",
|
||||
principal,
|
||||
cid
|
||||
@@ -200,9 +200,14 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
.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!(
|
||||
EventRow,
|
||||
CalendarObjectRow,
|
||||
"SELECT uid, ics FROM events WHERE (principal, cid, uid) = (?, ?, ?)",
|
||||
principal,
|
||||
cid,
|
||||
@@ -214,7 +219,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
async fn put_event(
|
||||
async fn put_object(
|
||||
&mut self,
|
||||
principal: String,
|
||||
cid: String,
|
||||
@@ -224,7 +229,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
let mut tx = self.db.begin().await?;
|
||||
|
||||
// input validation
|
||||
Event::from_ics(uid.to_owned(), ics.to_owned())?;
|
||||
CalendarObject::from_ics(uid.to_owned(), ics.to_owned())?;
|
||||
sqlx::query!(
|
||||
"REPLACE INTO events (principal, cid, uid, ics) VALUES (?, ?, ?, ?)",
|
||||
principal,
|
||||
@@ -247,7 +252,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_event(
|
||||
async fn delete_object(
|
||||
&mut self,
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
@@ -285,7 +290,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
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?;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -314,7 +319,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
synctoken: i64,
|
||||
) -> Result<(Vec<Event>, Vec<String>, i64), Error> {
|
||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
||||
struct Row {
|
||||
uid: String,
|
||||
synctoken: i64,
|
||||
@@ -340,7 +345,7 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
.unwrap_or(0);
|
||||
|
||||
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),
|
||||
Err(Error::NotFound) => deleted_events.push(uid),
|
||||
Err(err) => return Err(err),
|
||||
|
||||
@@ -2,7 +2,8 @@ use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::model::{Calendar, Event};
|
||||
use crate::model::object::CalendarObject;
|
||||
use crate::model::Calendar;
|
||||
|
||||
#[async_trait]
|
||||
pub trait CalendarStore: Send + Sync + 'static {
|
||||
@@ -29,23 +30,28 @@ pub trait CalendarStore: Send + Sync + 'static {
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
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_event(&self, principal: &str, cid: &str, uid: &str) -> Result<Event, Error>;
|
||||
async fn put_event(
|
||||
async fn get_objects(&self, principal: &str, cid: &str) -> Result<Vec<CalendarObject>, Error>;
|
||||
async fn get_object(
|
||||
&self,
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
uid: &str,
|
||||
) -> Result<CalendarObject, Error>;
|
||||
async fn put_object(
|
||||
&mut self,
|
||||
principal: String,
|
||||
cid: String,
|
||||
uid: String,
|
||||
ics: String,
|
||||
) -> Result<(), Error>;
|
||||
async fn delete_event(
|
||||
async fn delete_object(
|
||||
&mut self,
|
||||
principal: &str,
|
||||
cid: &str,
|
||||
uid: &str,
|
||||
use_trashbin: bool,
|
||||
) -> 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();
|
||||
|
||||
store
|
||||
.put_event(
|
||||
.put_object(
|
||||
"testuser".to_owned(),
|
||||
"test".to_owned(),
|
||||
"asd".to_owned(),
|
||||
@@ -46,7 +46,7 @@ async fn test_create_event<CS: CalendarStore>(mut store: CS) {
|
||||
.await
|
||||
.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_uid(), "asd");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user