Initial commit

This commit is contained in:
Lennart
2023-09-04 13:20:13 +02:00
commit ccb09f40b4
26 changed files with 10064 additions and 0 deletions

2256
crates/api/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
crates/api/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "rustical_api"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4.0"
anyhow = { version = "1.0.75", features = ["backtrace"] }
rustical_store = { path = "../store/" }
serde = "1.0.188"
serde_json = "1.0.105"
tokio = { version = "1.32.0", features = ["sync"] }

30
crates/api/src/lib.rs Normal file
View File

@@ -0,0 +1,30 @@
use actix_web::{
http::Method,
web::{self, Data, Path},
Responder,
};
use rustical_store::calendar::CalendarStore;
use tokio::sync::RwLock;
pub fn configure_api<C: CalendarStore>(cfg: &mut web::ServiceConfig, store: Data<RwLock<C>>) {
cfg.app_data(store)
.route("ping", web::method(Method::GET).to(get_ping::<C>))
.route(
"/{cid}/events",
web::method(Method::GET).to(get_events::<C>),
);
}
pub async fn get_events<C: CalendarStore>(
store: Data<RwLock<C>>,
path: Path<String>,
) -> impl Responder {
let cid = path.into_inner();
let events = store.read().await.get_events(&cid).await.unwrap();
serde_json::to_string_pretty(&events)
}
pub async fn get_ping<C: CalendarStore>(store: Data<RwLock<C>>) -> impl Responder {
let cals = store.read().await.get_calendars().await.unwrap();
serde_json::to_string_pretty(&cals)
}

2320
crates/dav/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
crates/dav/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "rustical_dav"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4.0"
actix-web-httpauth = "0.8.0"
anyhow = { version = "1.0.75", features = ["backtrace"] }
base64 = "0.21.3"
derive_more = "0.99.17"
futures-util = "0.3.28"
quick-xml = { version = "0.30.0", features = [
"serde",
"serde-types",
"serialize",
] }
roxmltree = "0.18.0"
rustical_store = { path = "../store/" }
serde = { version = "1.0.188", features = ["serde_derive", "derive"] }
serde_json = "1.0.105"
tokio = { version = "1.32.0", features = ["sync", "full"] }

View File

@@ -0,0 +1,53 @@
use actix_web::{http::StatusCode, FromRequest, HttpRequest, ResponseError};
use derive_more::Display;
use futures_util::future::{err, ok, Ready};
#[derive(Debug, Display)]
pub struct InvalidDepthHeader {}
impl ResponseError for InvalidDepthHeader {
fn status_code(&self) -> actix_web::http::StatusCode {
StatusCode::BAD_REQUEST
}
}
#[derive(Debug, PartialEq)]
pub enum Depth {
Zero,
One,
Infinity,
}
impl TryFrom<&[u8]> for Depth {
type Error = InvalidDepthHeader;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
match value {
b"0" => Ok(Depth::Zero),
b"1" => Ok(Depth::One),
b"Infinity" | b"infinity" => Ok(Depth::Infinity),
_ => Err(InvalidDepthHeader {}),
}
}
}
impl FromRequest for Depth {
type Error = InvalidDepthHeader;
type Future = Ready<Result<Self, Self::Error>>;
fn extract(req: &HttpRequest) -> Self::Future {
if let Some(depth_header) = req.headers().get("Depth") {
match depth_header.as_bytes().try_into() {
Ok(depth) => ok(depth),
Err(e) => err(e),
}
} else {
// default depth
ok(Depth::Zero)
}
}
fn from_request(req: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future {
Self::extract(req)
}
}

28
crates/dav/src/error.rs Normal file
View File

@@ -0,0 +1,28 @@
use actix_web::{http::StatusCode, HttpResponse};
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
pub enum Error {
#[display(fmt = "Internal server error")]
InternalError,
#[display(fmt = "Not found")]
NotFound,
#[display(fmt = "Bad request")]
BadRequest,
Unauthorized,
}
impl actix_web::error::ResponseError for Error {
fn status_code(&self) -> StatusCode {
match *self {
Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotFound => StatusCode::NOT_FOUND,
Self::BadRequest => StatusCode::BAD_REQUEST,
Self::Unauthorized => StatusCode::UNAUTHORIZED,
}
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).body(self.to_string())
}
}

78
crates/dav/src/lib.rs Normal file
View File

@@ -0,0 +1,78 @@
use actix_web::http::Method;
use actix_web::web::{self, Data};
use actix_web::{guard, HttpResponse, Responder};
use actix_web_httpauth::middleware::HttpAuthentication;
use error::Error;
use routes::{calendar, event, principal, root};
use rustical_store::calendar::CalendarStore;
use std::str::FromStr;
use tokio::sync::RwLock;
pub mod depth_extractor;
pub mod error;
pub mod namespace;
mod propfind;
pub mod routes;
pub fn configure_well_known(cfg: &mut web::ServiceConfig, caldav_root: String) {
cfg.service(web::redirect("/caldav", caldav_root).permanent());
}
pub fn configure_dav<C: CalendarStore>(cfg: &mut web::ServiceConfig, store: Data<RwLock<C>>) {
let propfind_method = || Method::from_str("PROPFIND").unwrap();
let report_method = || Method::from_str("REPORT").unwrap();
let auth = HttpAuthentication::basic(|req, creds| async move {
if creds.user_id().is_empty() {
// not authenticated
Err((actix_web::error::ErrorUnauthorized("Unauthorized"), req))
} else {
Ok(req)
}
});
cfg.app_data(store)
.service(
web::resource("{path:.*}")
// Without the guard this service would handle all requests
.guard(guard::Method(Method::OPTIONS))
.to(options_handler),
)
.service(
web::resource("")
.route(web::method(propfind_method()).to(root::route_propfind_root))
.wrap(auth.clone()),
)
.service(
web::resource("/{principal}")
.route(web::method(propfind_method()).to(principal::route_propfind_principal::<C>))
.wrap(auth.clone()),
)
.service(
web::resource("/{principal}/{calendar}")
.route(web::method(report_method()).to(calendar::route_report_calendar::<C>))
.route(web::method(propfind_method()).to(calendar::route_propfind_calendar::<C>))
.wrap(auth.clone()),
)
.service(
web::resource("/{principal}/{calendar}/{event}")
.route(web::method(Method::DELETE).to(event::delete_event::<C>))
.route(web::method(Method::GET).to(event::get_event::<C>))
.route(web::method(Method::PUT).to(event::put_event::<C>))
.wrap(auth.clone()),
);
}
async fn options_handler() -> impl Responder {
HttpResponse::Ok()
.insert_header((
"Allow",
"OPTIONS, GET, HEAD, POST, PUT, REPORT, PROPFIND, PROPPATCH",
))
.insert_header((
"DAV",
"1, 2, 3, calendar-access, extended-mkcol",
// "1, 2, 3, calendar-access, addressbook, extended-mkcol",
))
.body("options")
}

View File

@@ -0,0 +1,38 @@
use quick_xml::events::attributes::Attribute;
// An enum keeping track of the XML namespaces used for WebDAV and its extensions
//
// Can also generate appropriate attributes for quick_xml
pub enum Namespace {
Dav,
CalDAV,
ICal,
CServer,
}
impl Namespace {
pub fn as_str(&self) -> &'static str {
match self {
Self::Dav => "DAV:",
Self::CalDAV => "urn:ietf:params:xml:ns:caldav",
Self::ICal => "http://apple.com/ns/ical/",
Self::CServer => "http://calendarserver.org/ns/",
}
}
// Returns an opinionated namespace attribute name
pub fn xml_attr(&self) -> &'static str {
match self {
Self::Dav => "xmlns",
Self::CalDAV => "xmlns:C",
Self::ICal => "xmlns:IC",
Self::CServer => "xmlns:CS",
}
}
}
impl From<Namespace> for Attribute<'static> {
fn from(value: Namespace) -> Self {
(value.xml_attr(), value.as_str()).into()
}
}

122
crates/dav/src/propfind.rs Normal file
View File

@@ -0,0 +1,122 @@
use actix_web::http::StatusCode;
use anyhow::{anyhow, Result};
use quick_xml::{
events::{attributes::Attribute, BytesText},
Writer,
};
pub fn parse_propfind(body: &str) -> Result<Vec<&str>> {
if body.is_empty() {
// if body is empty, allprops must be returned (RFC 4918)
return Ok(vec!["allprops"]);
}
let doc = roxmltree::Document::parse(body)?;
let propfind_node = doc.root_element();
if propfind_node.tag_name().name() != "propfind" {
return Err(anyhow!("invalid tag"));
}
let prop_node = if let Some(el) = propfind_node.first_element_child() {
el
} else {
return Ok(Vec::new());
};
match prop_node.tag_name().name() {
"prop" => Ok(prop_node
.children()
.map(|node| node.tag_name().name())
.collect()),
_ => Err(anyhow!("invalid prop tag")),
}
}
pub fn write_resourcetype(
writer: &mut Writer<&mut Vec<u8>>,
types: Vec<&str>,
) -> Result<(), quick_xml::Error> {
writer
.create_element("resourcetype")
.write_inner_content(|writer| {
for resourcetype in types {
writer.create_element(resourcetype).write_empty()?;
}
Ok(())
})?;
Ok(())
}
pub fn write_invalid_props_response(
writer: &mut Writer<&mut Vec<u8>>,
href: &str,
invalid_props: Vec<&str>,
) -> Result<(), quick_xml::Error> {
if invalid_props.is_empty() {
return Ok(());
};
write_propstat_response(writer, href, StatusCode::NOT_FOUND, |writer| {
for prop in invalid_props {
writer.create_element(prop).write_empty()?;
}
Ok(())
})?;
Ok(())
}
// Writes a propstat response into a multistatus
// closure hooks into the <prop> element
pub fn write_propstat_response<F>(
writer: &mut Writer<&mut Vec<u8>>,
href: &str,
status: StatusCode,
prop_closure: F,
) -> Result<(), quick_xml::Error>
where
F: FnOnce(&mut Writer<&mut Vec<u8>>) -> Result<(), quick_xml::Error>,
{
writer
.create_element("response")
.write_inner_content(|writer| {
writer
.create_element("href")
.write_text_content(BytesText::new(href))?;
writer
.create_element("propstat")
.write_inner_content(|writer| {
writer
.create_element("prop")
.write_inner_content(prop_closure)?;
writer
.create_element("status")
.write_text_content(BytesText::new(&format!("HTTP/1.1 {}", status)))?;
Ok(())
})?;
Ok(())
})?;
Ok(())
}
pub fn generate_multistatus<'a, F, A>(namespaces: A, closure: F) -> Result<String>
where
F: FnOnce(&mut Writer<&mut Vec<u8>>) -> Result<(), quick_xml::Error>,
A: IntoIterator,
A::Item: Into<Attribute<'a>>,
{
let mut output_buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2);
writer
.create_element("multistatus")
.with_attributes(namespaces)
.write_inner_content(closure)?;
Ok(format!(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n{}",
std::str::from_utf8(&output_buffer)?
))
}

View File

@@ -0,0 +1,207 @@
use crate::namespace::Namespace;
use crate::propfind::{
generate_multistatus, parse_propfind, write_invalid_props_response, write_propstat_response,
write_resourcetype,
};
use crate::Error;
use actix_web::http::header::ContentType;
use actix_web::http::StatusCode;
use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse};
use actix_web_httpauth::extractors::basic::BasicAuth;
use anyhow::Result;
use quick_xml::events::BytesText;
use quick_xml::Writer;
use roxmltree::{Node, NodeType};
use rustical_store::calendar::{Calendar, CalendarStore, Event};
use std::collections::HashSet;
use std::ops::Deref;
use std::sync::Arc;
use tokio::sync::RwLock;
async fn handle_report_calendar_query(
query_node: Node<'_, '_>,
request: HttpRequest,
events: Vec<Event>,
) -> Result<HttpResponse, Error> {
let prop_node = query_node
.children()
.find(|n| n.node_type() == NodeType::Element && n.tag_name().name() == "prop")
.ok_or(Error::BadRequest)?;
let props: Arc<HashSet<&str>> = Arc::new(
prop_node
.children()
.map(|node| node.tag_name().name())
.collect(),
);
let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
for event in events {
write_propstat_response(
writer,
&format!("{}/{}", request.path(), event.get_uid()),
StatusCode::OK,
|writer| {
for prop in props.deref() {
match *prop {
"getetag" => {
writer
.create_element("getetag")
.write_text_content(BytesText::new(&event.get_etag()))?;
}
"calendar-data" => {
writer
.create_element("C:calendar-data")
.write_text_content(BytesText::new(event.to_ics()))?;
}
_ => {}
}
}
Ok(())
},
)?;
}
Ok(())
})
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}
pub async fn route_report_calendar<C: CalendarStore>(
store: Data<RwLock<C>>,
body: String,
path: Path<(String, String)>,
request: HttpRequest,
) -> Result<HttpResponse, Error> {
let (_principal, cid) = path.into_inner();
let doc = roxmltree::Document::parse(&body).map_err(|_e| Error::InternalError)?;
let query_node = doc.root_element();
let events = store.read().await.get_events(&cid).await.unwrap();
// TODO: implement filtering
match query_node.tag_name().name() {
"calendar-query" => {}
"calendar-multiget" => {}
_ => return Err(Error::BadRequest),
};
handle_report_calendar_query(query_node, request, events).await
}
pub fn generate_propfind_calendar_response(
props: Vec<&str>,
principal: &str,
path: &str,
calendar: &Calendar,
) -> Result<String> {
let mut props = props;
if props.contains(&"allprops") {
props.extend(
[
"resourcetype",
"current-user-principal",
"displayname",
"supported-calendar-component-set",
"getcontenttype",
"calendar-description",
]
.iter(),
);
}
let mut invalid_props = Vec::<&str>::new();
let mut output_buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2);
write_propstat_response(&mut writer, path, StatusCode::OK, |writer| {
for prop in props {
match prop {
"resourcetype" => write_resourcetype(writer, vec!["C:calendar", "collection"])?,
"current-user-principal" => {
writer
.create_element("current-user-principal")
.write_inner_content(|writer| {
writer
.create_element("href")
.write_text_content(BytesText::new(&format!(
// TODO: Replace hard-coded string
"/dav/{principal}/",
)))?;
Ok(())
})?;
}
"displayname" => {
let el = writer.create_element("displayname");
if let Some(name) = calendar.clone().name {
el.write_text_content(BytesText::new(&name))?;
} else {
el.write_empty()?;
}
}
"supported-calendar-component-set" => {
writer
.create_element("C:supported-calendar-component-set")
.write_inner_content(|writer| {
writer
.create_element("C:comp")
.with_attribute(("name", "VEVENT"))
.write_empty()?;
Ok(())
})?;
}
"getcontenttype" => {
writer
.create_element("getcontenttype")
.write_text_content(BytesText::new("text/calendar"))?;
}
"allprops" => {}
_ => invalid_props.push(prop),
};
}
Ok(())
})?;
write_invalid_props_response(&mut writer, path, invalid_props)?;
Ok(std::str::from_utf8(&output_buffer)?.to_string())
}
pub async fn route_propfind_calendar<C: CalendarStore>(
path: Path<(String, String)>,
body: String,
request: HttpRequest,
auth: BasicAuth,
store: Data<RwLock<C>>,
) -> Result<HttpResponse, Error> {
let (_principal, cid) = path.into_inner();
let calendar = store
.read()
.await
.get_calendar(&cid)
.await
.map_err(|_e| Error::InternalError)?;
let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?;
let responses_string = generate_propfind_calendar_response(
props.clone(),
auth.user_id(),
request.path(),
&calendar,
)
.map_err(|_e| Error::InternalError)?;
let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
writer.write_event(quick_xml::events::Event::Text(BytesText::from_escaped(
responses_string,
)))?;
Ok(())
})
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}

View File

@@ -0,0 +1,64 @@
use crate::Error;
use actix_web::web::{Data, Path};
use actix_web::HttpResponse;
use rustical_store::calendar::CalendarStore;
use tokio::sync::RwLock;
pub async fn delete_event<C: CalendarStore>(
store: Data<RwLock<C>>,
path: Path<(String, String, String)>,
) -> Result<HttpResponse, Error> {
let (_principal, mut cid, uid) = path.into_inner();
if cid.ends_with(".ics") {
cid.truncate(cid.len() - 4);
}
store
.write()
.await
.delete_event(&uid)
.await
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::Ok().body(""))
}
pub async fn get_event<C: CalendarStore>(
store: Data<RwLock<C>>,
path: Path<(String, String, String)>,
) -> Result<HttpResponse, Error> {
let (_principal, mut cid, uid) = path.into_inner();
if cid.ends_with(".ics") {
cid.truncate(cid.len() - 4);
}
let event = store
.read()
.await
.get_event(&uid)
.await
.map_err(|_e| Error::NotFound)?;
Ok(HttpResponse::Ok()
.insert_header(("ETag", event.get_etag()))
.body(event.to_ics().to_string()))
}
pub async fn put_event<C: CalendarStore>(
store: Data<RwLock<C>>,
path: Path<(String, String, String)>,
body: String,
) -> Result<HttpResponse, Error> {
let (_principal, mut cid, uid) = path.into_inner();
// Incredibly bodged method of normalising the uid but works for a prototype
if cid.ends_with(".ics") {
cid.truncate(cid.len() - 4);
}
dbg!(&body);
store
.write()
.await
.upsert_event(uid, body)
.await
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::Ok().body(""))
}

View File

@@ -0,0 +1,4 @@
pub mod calendar;
pub mod event;
pub mod principal;
pub mod root;

View File

@@ -0,0 +1,140 @@
use crate::{
calendar::generate_propfind_calendar_response,
depth_extractor::Depth,
namespace::Namespace,
propfind::{
generate_multistatus, parse_propfind, write_invalid_props_response,
write_propstat_response, write_resourcetype,
},
Error,
};
use actix_web::{
http::{header::ContentType, StatusCode},
web::Data,
HttpRequest, HttpResponse,
};
use actix_web_httpauth::extractors::basic::BasicAuth;
use anyhow::Result;
use quick_xml::{
events::{BytesText, Event},
Writer,
};
use rustical_store::calendar::CalendarStore;
use tokio::sync::RwLock;
// Executes the PROPFIND request and returns a XML string to be written into a <mulstistatus> object.
pub async fn generate_propfind_principal_response(
props: Vec<&str>,
principal: &str,
path: &str,
) -> Result<String, quick_xml::Error> {
let mut props = props;
if props.contains(&"allprops") {
props.extend(
[
"resourcetype",
"current-user-principal",
"principal-URL",
"calendar-home-set",
"calendar-user-address-set",
]
.iter(),
);
}
let mut invalid_props = Vec::<&str>::new();
let mut output_buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2);
write_propstat_response(&mut writer, path, StatusCode::OK, |writer| {
let writer = writer;
for prop in props {
match prop {
"resourcetype" => write_resourcetype(writer, vec!["principal", "collection"])?,
"current-user-principal" | "principal-URL" => {
writer.create_element(prop).write_inner_content(|writer| {
writer
.create_element("href")
// TODO: Replace hard-coded string
.write_text_content(BytesText::new(&format!("/dav/{principal}/",)))?;
Ok(())
})?;
}
"calendar-home-set" | "calendar-user-address-set" => {
writer
.create_element(&format!("C:{prop}"))
.write_inner_content(|writer| {
writer
.create_element("href")
.write_text_content(BytesText::new(&format!(
// TODO: Replace hard-coded string
"/dav/{principal}/"
)))?;
Ok(())
})?;
}
"allprops" => {}
_ => invalid_props.push(prop),
};
}
Ok(())
})?;
dbg!(&invalid_props);
write_invalid_props_response(&mut writer, path, invalid_props)?;
Ok(std::str::from_utf8(&output_buffer)?.to_string())
}
pub async fn route_propfind_principal<C: CalendarStore>(
body: String,
request: HttpRequest,
auth: BasicAuth,
store: Data<RwLock<C>>,
depth: Depth,
) -> Result<HttpResponse, Error> {
let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?;
let mut responses = Vec::new();
// also get calendars:
if depth != Depth::Zero {
let cals = store
.read()
.await
.get_calendars()
.await
.map_err(|_e| Error::InternalError)?;
for cal in cals {
responses.push(
generate_propfind_calendar_response(
props.clone(),
auth.user_id(),
&format!("{}/{}", request.path(), cal.id),
&cal,
)
.map_err(|_e| Error::InternalError)?,
);
}
}
responses.push(
generate_propfind_principal_response(props.clone(), auth.user_id(), request.path())
.await
.map_err(|_e| Error::InternalError)?,
);
let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
for response in responses {
writer.write_event(Event::Text(BytesText::from_escaped(response)))?;
}
Ok(())
})
.map_err(|_e| Error::InternalError)?;
println!("{}", &output);
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}

View File

@@ -0,0 +1,85 @@
use actix_web::{
http::{header::ContentType, StatusCode},
HttpRequest, HttpResponse,
};
use actix_web_httpauth::extractors::basic::BasicAuth;
use quick_xml::{
events::{BytesText, Event},
Writer,
};
use crate::{
namespace::Namespace,
propfind::{
generate_multistatus, parse_propfind, write_invalid_props_response,
write_propstat_response, write_resourcetype,
},
Error,
};
// Executes the PROPFIND request and returns a XML string to be written into a <mulstistatus> object.
pub async fn generate_propfind_root_response(
props: Vec<&str>,
principal: &str,
path: &str,
) -> Result<String, quick_xml::Error> {
let mut props = props;
if props.contains(&"allprops") {
props.extend(["resourcetype", "current-user-principal"].iter());
}
let mut invalid_props = Vec::<&str>::new();
let mut output_buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2);
write_propstat_response(&mut writer, path, StatusCode::OK, |writer| {
for prop in props {
match prop {
"resourcetype" => write_resourcetype(writer, vec!["collection"])?,
"current-user-principal" => {
writer
.create_element("current-user-principal")
.write_inner_content(|writer| {
writer
.create_element("href")
.write_text_content(BytesText::new(
// TODO: Replace hard-coded string
&format!("/dav/{principal}",),
))?;
Ok(())
})?;
}
"allprops" => {}
_ => invalid_props.push(prop),
};
}
Ok(())
})?;
dbg!(&invalid_props);
write_invalid_props_response(&mut writer, path, invalid_props)?;
Ok(std::str::from_utf8(&output_buffer)?.to_string())
}
pub async fn route_propfind_root(
body: String,
request: HttpRequest,
auth: BasicAuth,
) -> Result<HttpResponse, Error> {
let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?;
let responses_string =
generate_propfind_root_response(props.clone(), auth.user_id(), request.path())
.await
.map_err(|_e| Error::InternalError)?;
let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
writer.write_event(Event::Text(BytesText::from_escaped(responses_string)))?;
Ok(())
})
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}

1748
crates/store/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
crates/store/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "rustical_store"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { version = "1.0.75", features = ["backtrace"] }
async-trait = "0.1.73"
serde = { version = "1.0.188", features = ["derive", "rc"] }
serde_json = "1.0.105"
sha2 = "0.10.7"
sqlx = { version = "0.7.1", features = ["sqlx-sqlite", "sqlx-postgres", "uuid", "time", "chrono", "postgres", "sqlite", "runtime-tokio"] }
tokio = { version = "1.32.0", features = ["sync", "full"] }

View File

@@ -0,0 +1,108 @@
use std::collections::HashMap;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tokio::{fs::File, io::AsyncWriteExt};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Event {
uid: String,
ics: String,
}
impl 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.to_ics());
format!("{:x}", hasher.finalize())
}
pub fn to_ics(&self) -> &str {
&self.ics
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Calendar {
pub id: String,
pub name: Option<String>,
pub ics: String,
}
impl Calendar {}
#[async_trait]
pub trait CalendarStore: Send + Sync + 'static {
async fn get_calendar(&self, id: &str) -> Result<Calendar>;
async fn get_calendars(&self) -> Result<Vec<Calendar>>;
async fn get_events(&self, cid: &str) -> Result<Vec<Event>>;
async fn get_event(&self, uid: &str) -> Result<Event>;
async fn upsert_event(&mut self, uid: String, ics: String) -> Result<()>;
async fn delete_event(&mut self, uid: &str) -> Result<()>;
}
#[derive(Debug, Deserialize, Serialize)]
pub struct JsonCalendarStore {
calendars: HashMap<String, Calendar>,
events: HashMap<String, Event>,
path: String,
}
impl JsonCalendarStore {
pub fn new(path: String) -> Self {
JsonCalendarStore {
calendars: HashMap::new(),
events: HashMap::new(),
path,
}
}
pub async fn save(&self) -> Result<()> {
let mut file = File::create(&self.path).await?;
let json = serde_json::to_string_pretty(&self)?;
file.write_all(json.as_bytes()).await?;
Ok(())
}
}
#[async_trait]
impl CalendarStore for JsonCalendarStore {
async fn get_calendar(&self, id: &str) -> Result<Calendar> {
Ok(self.calendars.get(id).ok_or(anyhow!("not found"))?.clone())
}
async fn get_calendars(&self) -> Result<Vec<Calendar>> {
Ok(vec![Calendar {
id: "test".to_string(),
name: Some("test".to_string()),
ics: "asd".to_string(),
}])
}
async fn get_events(&self, _cid: &str) -> Result<Vec<Event>> {
Ok(self.events.values().cloned().collect())
}
async fn get_event(&self, uid: &str) -> Result<Event> {
Ok(self.events.get(uid).ok_or(anyhow!("not found"))?.clone())
}
async fn upsert_event(&mut self, uid: String, ics: String) -> Result<()> {
self.events.insert(uid.clone(), Event { uid, ics });
self.save().await.unwrap();
Ok(())
}
async fn delete_event(&mut self, uid: &str) -> Result<()> {
self.events.remove(uid);
self.save().await?;
Ok(())
}
}

2
crates/store/src/lib.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod calendar;
pub mod users;

39
crates/store/src/users.rs Normal file
View File

@@ -0,0 +1,39 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct User {
pub id: String,
pub name: Option<String>,
}
impl User {}
#[async_trait]
pub trait UserStore: Send + Sync + 'static {
async fn get_user(&self, id: &str) -> Result<User>;
async fn get_users(&self) -> Result<Vec<User>>;
}
pub struct TestUserStore {}
#[async_trait]
impl UserStore for TestUserStore {
async fn get_user(&self, id: &str) -> Result<User> {
if id != "test" {
return Err(anyhow!("asd"));
}
Ok(User {
id: "test".to_string(),
name: Some("test".to_string()),
})
}
async fn get_users(&self) -> Result<Vec<User>> {
Ok(vec![User {
id: "test".to_string(),
name: Some("test".to_string()),
}])
}
}