diff --git a/crates/carddav/src/addressbook/methods/import.rs b/crates/carddav/src/addressbook/methods/import.rs new file mode 100644 index 0000000..231723f --- /dev/null +++ b/crates/carddav/src/addressbook/methods/import.rs @@ -0,0 +1,63 @@ +use std::io::BufReader; + +use crate::Error; +use crate::addressbook::AddressbookResourceService; +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Response}, +}; +use http::StatusCode; +use ical::{ + parser::{Component, ComponentMut, vcard}, + property::Property, +}; +use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::Principal}; +use tracing::instrument; + +#[instrument(skip(resource_service))] +pub async fn route_import( + Path((principal, addressbook_id)): Path<(String, String)>, + user: Principal, + State(resource_service): State>, + body: String, +) -> Result { + if !user.is_principal(&principal) { + return Err(Error::Unauthorized); + } + + let parser = vcard::VcardParser::new(BufReader::new(body.as_bytes())); + + let mut objects = vec![]; + for res in parser { + let mut card = res.unwrap(); + let uid = card.get_uid(); + if uid.is_none() { + let mut card_mut = card.mutable(); + card_mut.set_property(Property { + name: "UID".to_owned(), + value: Some(uuid::Uuid::new_v4().to_string()), + params: None, + }); + card = card_mut.verify().unwrap(); + } + + objects.push(card.try_into().unwrap()); + } + + let addressbook = Addressbook { + principal, + id: addressbook_id, + displayname: None, + description: None, + deleted_at: None, + synctoken: 0, + push_topic: uuid::Uuid::new_v4().to_string(), + }; + + let addr_store = resource_service.addr_store; + addr_store + .import_addressbook(addressbook, objects, false) + .await?; + + Ok(StatusCode::OK.into_response()) +} diff --git a/crates/carddav/src/addressbook/methods/mod.rs b/crates/carddav/src/addressbook/methods/mod.rs index 49a98d6..ed59cfc 100644 --- a/crates/carddav/src/addressbook/methods/mod.rs +++ b/crates/carddav/src/addressbook/methods/mod.rs @@ -1,5 +1,5 @@ pub mod get; +pub mod import; pub mod mkcol; pub mod post; -pub mod put; pub mod report; diff --git a/crates/carddav/src/addressbook/methods/put.rs b/crates/carddav/src/addressbook/methods/put.rs deleted file mode 100644 index 098c86a..0000000 --- a/crates/carddav/src/addressbook/methods/put.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::Error; -use crate::addressbook::AddressbookResourceService; -use axum::response::IntoResponse; -use axum::{ - extract::{Path, State}, - response::Response, -}; -use http::StatusCode; -use ical::VcardParser; -use rustical_ical::AddressObject; -use rustical_store::Addressbook; -use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal}; -use tracing::instrument; - -#[instrument(skip(addr_store))] -pub async fn route_put( - Path((principal, addressbook_id)): Path<(String, String)>, - State(AddressbookResourceService { addr_store, .. }): State>, - user: Principal, - body: String, -) -> Result { - if !user.is_principal(&principal) { - return Err(Error::Unauthorized); - } - - let mut objects = vec![]; - for object in VcardParser::new(body.as_bytes()) { - let object = object.map_err(rustical_ical::Error::from)?; - objects.push(AddressObject::try_from(object)?); - } - - let addressbook = Addressbook { - id: addressbook_id.clone(), - principal: principal.clone(), - displayname: None, - description: None, - deleted_at: None, - synctoken: Default::default(), - push_topic: uuid::Uuid::new_v4().to_string(), - }; - - addr_store - .import_addressbook(principal.clone(), addressbook, objects) - .await?; - - Ok(StatusCode::CREATED.into_response()) -} diff --git a/crates/carddav/src/addressbook/service.rs b/crates/carddav/src/addressbook/service.rs index 9d6588d..8f3b0d4 100644 --- a/crates/carddav/src/addressbook/service.rs +++ b/crates/carddav/src/addressbook/service.rs @@ -3,8 +3,8 @@ use super::methods::report::route_report_addressbook; use crate::address_object::AddressObjectResourceService; use crate::address_object::resource::AddressObjectResource; use crate::addressbook::methods::get::route_get; +use crate::addressbook::methods::import::route_import; use crate::addressbook::methods::post::route_post; -use crate::addressbook::methods::put::route_put; use crate::addressbook::resource::AddressbookResource; use crate::{CardDavPrincipalUri, Error}; use async_trait::async_trait; @@ -139,9 +139,9 @@ impl AxumMethods for AddressbookReso }) } - fn put() -> Option BoxFuture<'static, Result>> { + fn import() -> Option BoxFuture<'static, Result>> { Some(|state, req| { - let mut service = Handler::with_state(route_put::, state); + let mut service = Handler::with_state(route_import::, state); Box::pin(Service::call(&mut service, req)) }) } diff --git a/crates/store/src/addressbook_store.rs b/crates/store/src/addressbook_store.rs index 1fff3ee..325071b 100644 --- a/crates/store/src/addressbook_store.rs +++ b/crates/store/src/addressbook_store.rs @@ -76,8 +76,8 @@ pub trait AddressbookStore: Send + Sync + 'static { async fn import_addressbook( &self, - principal: String, addressbook: Addressbook, objects: Vec, + merge_existing: bool, ) -> Result<(), Error>; } diff --git a/crates/store_sqlite/src/addressbook_store.rs b/crates/store_sqlite/src/addressbook_store.rs index eec49e6..a74140f 100644 --- a/crates/store_sqlite/src/addressbook_store.rs +++ b/crates/store_sqlite/src/addressbook_store.rs @@ -17,10 +17,20 @@ struct AddressObjectRow { } impl TryFrom for AddressObject { - type Error = crate::Error; + type Error = rustical_store::Error; fn try_from(value: AddressObjectRow) -> Result { - Ok(Self::from_vcf(value.id, value.vcf)?) + let object = Self::from_vcf(value.vcf)?; + if object.get_id() != value.id { + return Err(rustical_store::Error::IcalError( + rustical_ical::Error::InvalidData(format!( + "object_id={} and UID={} don't match", + object.get_id(), + value.id + )), + )); + } + Ok(object) } } @@ -259,7 +269,7 @@ impl SqliteAddressbookStore { .fetch_all(executor) .await.map_err(crate::Error::from)? .into_iter() - .map(|row| row.try_into().map_err(rustical_store::Error::from)) + .map(|row| row.try_into()) .collect() } @@ -270,7 +280,7 @@ impl SqliteAddressbookStore { object_id: &str, show_deleted: bool, ) -> Result { - Ok(sqlx::query_as!( + sqlx::query_as!( AddressObjectRow, "SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)", principal, @@ -281,7 +291,7 @@ impl SqliteAddressbookStore { .fetch_one(executor) .await .map_err(crate::Error::from)? - .try_into()?) + .try_into() } async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>( @@ -627,20 +637,32 @@ impl AddressbookStore for SqliteAddressbookStore { #[instrument(skip(objects))] async fn import_addressbook( &self, - principal: String, addressbook: Addressbook, objects: Vec, + merge_existing: bool, ) -> Result<(), Error> { let mut tx = self.db.begin().await.map_err(crate::Error::from)?; - let addressbook_id = addressbook.id.clone(); - Self::_insert_addressbook(&mut *tx, addressbook).await?; + let existing = + match Self::_get_addressbook(&mut *tx, &addressbook.principal, &addressbook.id, true) + .await + { + Ok(addressbook) => Some(addressbook), + Err(Error::NotFound) => None, + Err(err) => return Err(err), + }; + if existing.is_some() && !merge_existing { + return Err(Error::AlreadyExists); + } + if existing.is_none() { + Self::_insert_addressbook(&mut *tx, addressbook.clone()).await?; + } for object in objects { Self::_put_object( &mut *tx, - principal.clone(), - addressbook_id.clone(), + addressbook.principal.clone(), + addressbook.id.clone(), object, false, )