style: apply cargo fmt to entire codebase
Standardize code formatting across all 173 Rust source files using rustfmt. No functional changes - purely cosmetic. This establishes a consistent code style baseline for the project going forward.
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* CardDAV Handler Module
|
||||
*
|
||||
*
|
||||
* This module implements the CardDAV protocol (RFC 6352) endpoints for OxiCloud.
|
||||
* It provides contact/address book access and management through standard CardDAV
|
||||
* methods, allowing clients like Thunderbird, Apple Contacts, GNOME Contacts,
|
||||
* and DAVx⁵ to sync contacts.
|
||||
*
|
||||
*
|
||||
* Supported methods:
|
||||
* - OPTIONS: Advertise CardDAV capabilities
|
||||
* - PROPFIND: List address books and their properties
|
||||
@@ -16,36 +16,38 @@
|
||||
* - DELETE: Remove address books or contacts
|
||||
* - PROPPATCH: Modify address book properties
|
||||
*/
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
body::{self, Body},
|
||||
http::{HeaderName, Request, StatusCode, header},
|
||||
response::Response,
|
||||
http::{StatusCode, header, HeaderName, Request},
|
||||
body::{Body, self},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use bytes::Buf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::di::AppState;
|
||||
use crate::application::adapters::carddav_adapter::{CardDavAdapter, CardDavReportType, contact_to_vcard};
|
||||
use crate::application::adapters::webdav_adapter::{PropFindRequest, PropFindType};
|
||||
use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase};
|
||||
use crate::application::dtos::address_book_dto::{
|
||||
CreateAddressBookDto, UpdateAddressBookDto,
|
||||
use crate::application::adapters::carddav_adapter::{
|
||||
CardDavAdapter, CardDavReportType, contact_to_vcard,
|
||||
};
|
||||
use crate::application::adapters::webdav_adapter::{PropFindRequest, PropFindType};
|
||||
use crate::application::dtos::address_book_dto::{CreateAddressBookDto, UpdateAddressBookDto};
|
||||
use crate::application::dtos::contact_dto::CreateContactVCardDto;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase};
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
|
||||
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
||||
|
||||
/// Creates CardDAV routes with full path prefixes.
|
||||
///
|
||||
///
|
||||
/// Uses `merge()` instead of `nest()` to avoid Axum's trailing-slash routing gap.
|
||||
/// Registers `/carddav`, `/carddav/`, and `/carddav/{*path}` explicitly.
|
||||
pub fn carddav_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/carddav/{*path}", axum::routing::any(handle_carddav_methods))
|
||||
.route(
|
||||
"/carddav/{*path}",
|
||||
axum::routing::any(handle_carddav_methods),
|
||||
)
|
||||
.route("/carddav/", axum::routing::any(handle_carddav_methods_root))
|
||||
.route("/carddav", axum::routing::any(handle_carddav_methods_root))
|
||||
}
|
||||
@@ -73,7 +75,7 @@ async fn handle_carddav_methods_inner(
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let state = Arc::new(state);
|
||||
let method = req.method().clone();
|
||||
|
||||
|
||||
match method.as_str() {
|
||||
"OPTIONS" => handle_options().await,
|
||||
"PROPFIND" => handle_propfind(state.clone(), req, &path).await,
|
||||
@@ -83,7 +85,10 @@ async fn handle_carddav_methods_inner(
|
||||
"GET" => handle_get(state.clone(), req, &path).await,
|
||||
"DELETE" => handle_delete(state.clone(), req, &path).await,
|
||||
"PROPPATCH" => handle_proppatch(state.clone(), req, &path).await,
|
||||
_ => Err(AppError::method_not_allowed(format!("Method not allowed: {}", method))),
|
||||
_ => Err(AppError::method_not_allowed(format!(
|
||||
"Method not allowed: {}",
|
||||
method
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +100,10 @@ fn extract_carddav_path(uri_path: &str) -> String {
|
||||
} else if uri_path.ends_with("/carddav") {
|
||||
String::new()
|
||||
} else {
|
||||
uri_path.trim_start_matches('/').trim_end_matches('/').to_string()
|
||||
uri_path
|
||||
.trim_start_matches('/')
|
||||
.trim_end_matches('/')
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +142,10 @@ async fn handle_options() -> Result<Response<Body>, AppError> {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(HEADER_DAV, "1, 2, 3, addressbook")
|
||||
.header(header::ALLOW, "OPTIONS, GET, PUT, DELETE, PROPFIND, PROPPATCH, REPORT, MKCOL")
|
||||
.header(
|
||||
header::ALLOW,
|
||||
"OPTIONS, GET, PUT, DELETE, PROPFIND, PROPPATCH, REPORT, MKCOL",
|
||||
)
|
||||
.body(Body::empty())
|
||||
.unwrap())
|
||||
}
|
||||
@@ -146,32 +157,41 @@ async fn handle_propfind(
|
||||
req: Request<Body>,
|
||||
path: &str,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let depth = req.headers()
|
||||
let depth = req
|
||||
.headers()
|
||||
.get("Depth")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("1")
|
||||
.to_string();
|
||||
|
||||
|
||||
let user = extract_user(&req)?;
|
||||
let addressbook_service = get_addressbook_service(&state)?;
|
||||
let contact_svc = get_contact_service(&state)?;
|
||||
|
||||
|
||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
||||
.await
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||
|
||||
|
||||
let propfind_request = if body_bytes.is_empty() {
|
||||
PropFindRequest { prop_find_type: PropFindType::AllProp }
|
||||
PropFindRequest {
|
||||
prop_find_type: PropFindType::AllProp,
|
||||
}
|
||||
} else {
|
||||
crate::application::adapters::webdav_adapter::WebDavAdapter::parse_propfind(body_bytes.reader())
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to parse PROPFIND: {}", e)))?
|
||||
crate::application::adapters::webdav_adapter::WebDavAdapter::parse_propfind(
|
||||
body_bytes.reader(),
|
||||
)
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to parse PROPFIND: {}", e)))?
|
||||
};
|
||||
|
||||
|
||||
if path.is_empty() {
|
||||
// Root CardDAV path — list user's address books
|
||||
let address_books = addressbook_service.list_user_address_books(&user.id).await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list address books: {}", e)))?;
|
||||
|
||||
let address_books = addressbook_service
|
||||
.list_user_address_books(&user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to list address books: {}", e))
|
||||
})?;
|
||||
|
||||
let base_href = "/carddav/";
|
||||
let mut response_body = Vec::new();
|
||||
CardDavAdapter::generate_addressbooks_propfind_response(
|
||||
@@ -179,8 +199,9 @@ async fn handle_propfind(
|
||||
&address_books,
|
||||
&propfind_request,
|
||||
base_href,
|
||||
).map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::MULTI_STATUS)
|
||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||
@@ -189,22 +210,26 @@ async fn handle_propfind(
|
||||
} else {
|
||||
let parts: Vec<&str> = path.splitn(2, '/').collect();
|
||||
let address_book_id = parts[0];
|
||||
|
||||
|
||||
if parts.len() == 1 {
|
||||
// Address book collection
|
||||
let address_book = addressbook_service.get_address_book(address_book_id, &user.id).await
|
||||
let address_book = addressbook_service
|
||||
.get_address_book(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::not_found(format!("Address book not found: {}", e)))?;
|
||||
|
||||
|
||||
let contacts = if depth != "0" {
|
||||
contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
|
||||
let base_href = &format!("/carddav/{}/", address_book_id);
|
||||
let mut response_body = Vec::new();
|
||||
|
||||
|
||||
CardDavAdapter::generate_addressbook_collection_propfind(
|
||||
&mut response_body,
|
||||
&address_book,
|
||||
@@ -212,8 +237,9 @@ async fn handle_propfind(
|
||||
&propfind_request,
|
||||
base_href,
|
||||
&depth,
|
||||
).map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::MULTI_STATUS)
|
||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||
@@ -223,21 +249,27 @@ async fn handle_propfind(
|
||||
// Individual contact .vcf
|
||||
let contact_file = parts[1];
|
||||
let contact_uid = contact_file.trim_end_matches(".vcf");
|
||||
|
||||
|
||||
// Look up by UID across all contacts in this address book
|
||||
let contacts = contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
let contacts = contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
||||
|
||||
let contact = contacts.iter().find(|c| c.uid == contact_uid)
|
||||
.ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?;
|
||||
|
||||
|
||||
let contact = contacts
|
||||
.iter()
|
||||
.find(|c| c.uid == contact_uid)
|
||||
.ok_or_else(|| {
|
||||
AppError::not_found(format!("Contact not found: {}", contact_uid))
|
||||
})?;
|
||||
|
||||
// Build single-resource PROPFIND response
|
||||
let base_href = &format!("/carddav/{}/", address_book_id);
|
||||
let report = CardDavReportType::AddressbookMultiget {
|
||||
hrefs: vec![format!("{}{}.vcf", base_href, contact_uid)],
|
||||
props: vec![],
|
||||
};
|
||||
|
||||
|
||||
let mut response_body = Vec::new();
|
||||
CardDavAdapter::generate_contacts_response(
|
||||
&mut response_body,
|
||||
@@ -245,8 +277,9 @@ async fn handle_propfind(
|
||||
&[(contact.uid.clone(), contact_to_vcard(contact))],
|
||||
&report,
|
||||
base_href,
|
||||
).map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::MULTI_STATUS)
|
||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||
@@ -265,44 +298,48 @@ async fn handle_report(
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let user = extract_user(&req)?;
|
||||
let contact_svc = get_contact_service(&state)?;
|
||||
|
||||
|
||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
||||
.await
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||
|
||||
|
||||
let report = CardDavAdapter::parse_report(body_bytes.reader())
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to parse REPORT: {}", e)))?;
|
||||
|
||||
|
||||
let address_book_id = path.split('/').next().unwrap_or(path);
|
||||
|
||||
|
||||
if address_book_id.is_empty() {
|
||||
return Err(AppError::bad_request("Address book ID required in path"));
|
||||
}
|
||||
|
||||
|
||||
let contacts = match &report {
|
||||
CardDavReportType::AddressbookQuery { .. } => {
|
||||
contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?
|
||||
},
|
||||
CardDavReportType::AddressbookQuery { .. } => contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?,
|
||||
CardDavReportType::AddressbookMultiget { hrefs, .. } => {
|
||||
let all_contacts = contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
let all_contacts = contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
||||
|
||||
all_contacts.into_iter()
|
||||
|
||||
all_contacts
|
||||
.into_iter()
|
||||
.filter(|c| hrefs.iter().any(|href| href.contains(&c.uid)))
|
||||
.collect()
|
||||
},
|
||||
CardDavReportType::SyncCollection { .. } => {
|
||||
contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?
|
||||
},
|
||||
}
|
||||
CardDavReportType::SyncCollection { .. } => contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?,
|
||||
};
|
||||
|
||||
|
||||
// Generate vCards
|
||||
let vcards: Vec<(String, String)> = contacts.iter()
|
||||
let vcards: Vec<(String, String)> = contacts
|
||||
.iter()
|
||||
.map(|c| (c.uid.clone(), contact_to_vcard(c)))
|
||||
.collect();
|
||||
|
||||
|
||||
let base_href = &format!("/carddav/{}/", address_book_id);
|
||||
let mut response_body = Vec::new();
|
||||
CardDavAdapter::generate_contacts_response(
|
||||
@@ -311,8 +348,9 @@ async fn handle_report(
|
||||
&vcards,
|
||||
&report,
|
||||
base_href,
|
||||
).map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::MULTI_STATUS)
|
||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||
@@ -329,19 +367,23 @@ async fn handle_mkcol(
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let user = extract_user(&req)?;
|
||||
let addressbook_service = get_addressbook_service(&state)?;
|
||||
|
||||
|
||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
||||
.await
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||
|
||||
|
||||
let (name, description, color) = if body_bytes.is_empty() {
|
||||
let name = path.split('/').next_back().unwrap_or("New Address Book").to_string();
|
||||
let name = path
|
||||
.split('/')
|
||||
.next_back()
|
||||
.unwrap_or("New Address Book")
|
||||
.to_string();
|
||||
(name, None, None)
|
||||
} else {
|
||||
CardDavAdapter::parse_mkaddressbook(body_bytes.reader())
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to parse MKCOL: {}", e)))?
|
||||
};
|
||||
|
||||
|
||||
let create_dto = CreateAddressBookDto {
|
||||
name,
|
||||
owner_id: user.id.clone(),
|
||||
@@ -349,10 +391,12 @@ async fn handle_mkcol(
|
||||
color,
|
||||
is_public: Some(false),
|
||||
};
|
||||
|
||||
addressbook_service.create_address_book(create_dto).await
|
||||
|
||||
addressbook_service
|
||||
.create_address_book(create_dto)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to create address book: {}", e)))?;
|
||||
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::CREATED)
|
||||
.body(Body::empty())
|
||||
@@ -368,46 +412,54 @@ async fn handle_put(
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let user = extract_user(&req)?;
|
||||
let contact_svc = get_contact_service(&state)?;
|
||||
|
||||
|
||||
let parts: Vec<&str> = path.splitn(2, '/').collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(AppError::bad_request("Path must be {address_book_id}/{uid}.vcf"));
|
||||
return Err(AppError::bad_request(
|
||||
"Path must be {address_book_id}/{uid}.vcf",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
let address_book_id = parts[0];
|
||||
|
||||
|
||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
||||
.await
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||
|
||||
|
||||
let vcard_data = String::from_utf8(body_bytes.to_vec())
|
||||
.map_err(|e| AppError::bad_request(format!("Invalid UTF-8 in vCard data: {}", e)))?;
|
||||
|
||||
|
||||
// Extract UID from vCard
|
||||
let vcard_uid = extract_uid_from_vcard(&vcard_data);
|
||||
|
||||
|
||||
// Check if contact already exists
|
||||
let existing = if let Some(ref uid) = vcard_uid {
|
||||
let contacts = contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
let contacts = contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
contacts.into_iter().find(|c| c.uid == *uid)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
|
||||
if let Some(existing_contact) = existing {
|
||||
// Update: delete + recreate from vCard
|
||||
contact_svc.delete_contact(&existing_contact.id, &user.id).await
|
||||
contact_svc
|
||||
.delete_contact(&existing_contact.id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to update contact: {}", e)))?;
|
||||
|
||||
|
||||
let create_dto = CreateContactVCardDto {
|
||||
address_book_id: address_book_id.to_string(),
|
||||
vcard: vcard_data,
|
||||
user_id: user.id.clone(),
|
||||
};
|
||||
let contact = contact_svc.create_contact_from_vcard(create_dto).await
|
||||
let contact = contact_svc
|
||||
.create_contact_from_vcard(create_dto)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to recreate contact: {}", e)))?;
|
||||
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.header(header::ETAG, format!("\"{}\"", contact.etag))
|
||||
@@ -419,10 +471,12 @@ async fn handle_put(
|
||||
vcard: vcard_data,
|
||||
user_id: user.id.clone(),
|
||||
};
|
||||
|
||||
let contact = contact_svc.create_contact_from_vcard(create_dto).await
|
||||
|
||||
let contact = contact_svc
|
||||
.create_contact_from_vcard(create_dto)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to create contact: {}", e)))?;
|
||||
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::CREATED)
|
||||
.header(header::ETAG, format!("\"{}\"", contact.etag))
|
||||
@@ -451,20 +505,22 @@ async fn handle_get(
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let user = extract_user(&req)?;
|
||||
let contact_svc = get_contact_service(&state)?;
|
||||
|
||||
|
||||
let parts: Vec<&str> = path.splitn(2, '/').collect();
|
||||
let address_book_id = parts[0];
|
||||
|
||||
|
||||
if parts.len() < 2 {
|
||||
// GET on address book collection — return all contacts as vcf
|
||||
let contacts = contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
let contacts = contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
||||
|
||||
|
||||
let mut vcf_data = String::new();
|
||||
for contact in &contacts {
|
||||
vcf_data.push_str(&contact_to_vcard(contact));
|
||||
}
|
||||
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "text/vcard; charset=utf-8")
|
||||
@@ -474,15 +530,19 @@ async fn handle_get(
|
||||
// GET on individual contact
|
||||
let contact_file = parts[1];
|
||||
let contact_uid = contact_file.trim_end_matches(".vcf");
|
||||
|
||||
let contacts = contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
|
||||
let contacts = contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
||||
|
||||
let contact = contacts.iter().find(|c| c.uid == contact_uid)
|
||||
|
||||
let contact = contacts
|
||||
.iter()
|
||||
.find(|c| c.uid == contact_uid)
|
||||
.ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?;
|
||||
|
||||
|
||||
let vcard = contact_to_vcard(contact);
|
||||
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "text/vcard; charset=utf-8")
|
||||
@@ -502,33 +562,43 @@ async fn handle_delete(
|
||||
let user = extract_user(&req)?;
|
||||
let addressbook_service = get_addressbook_service(&state)?;
|
||||
let contact_svc = get_contact_service(&state)?;
|
||||
|
||||
|
||||
let parts: Vec<&str> = path.splitn(2, '/').collect();
|
||||
let address_book_id = parts[0];
|
||||
|
||||
|
||||
if address_book_id.is_empty() {
|
||||
return Err(AppError::bad_request("Address book ID required"));
|
||||
}
|
||||
|
||||
|
||||
if parts.len() < 2 {
|
||||
// Delete address book
|
||||
addressbook_service.delete_address_book(address_book_id, &user.id).await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to delete address book: {}", e)))?;
|
||||
addressbook_service
|
||||
.delete_address_book(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to delete address book: {}", e))
|
||||
})?;
|
||||
} else {
|
||||
// Delete contact
|
||||
let contact_file = parts[1];
|
||||
let contact_uid = contact_file.trim_end_matches(".vcf");
|
||||
|
||||
let contacts = contact_svc.list_contacts(address_book_id, &user.id).await
|
||||
|
||||
let contacts = contact_svc
|
||||
.list_contacts(address_book_id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
||||
|
||||
let contact = contacts.iter().find(|c| c.uid == contact_uid)
|
||||
|
||||
let contact = contacts
|
||||
.iter()
|
||||
.find(|c| c.uid == contact_uid)
|
||||
.ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?;
|
||||
|
||||
contact_svc.delete_contact(&contact.id, &user.id).await
|
||||
|
||||
contact_svc
|
||||
.delete_contact(&contact.id, &user.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to delete contact: {}", e)))?;
|
||||
}
|
||||
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.body(Body::empty())
|
||||
@@ -544,20 +614,23 @@ async fn handle_proppatch(
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let user = extract_user(&req)?;
|
||||
let addressbook_service = get_addressbook_service(&state)?;
|
||||
|
||||
|
||||
let body_bytes = body::to_bytes(req.into_body(), usize::MAX)
|
||||
.await
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||
|
||||
let (props_to_set, props_to_remove) = crate::application::adapters::webdav_adapter::WebDavAdapter::parse_proppatch(body_bytes.reader())
|
||||
|
||||
let (props_to_set, props_to_remove) =
|
||||
crate::application::adapters::webdav_adapter::WebDavAdapter::parse_proppatch(
|
||||
body_bytes.reader(),
|
||||
)
|
||||
.map_err(|e| AppError::bad_request(format!("Failed to parse PROPPATCH: {}", e)))?;
|
||||
|
||||
|
||||
let address_book_id = path.split('/').next().unwrap_or(path);
|
||||
|
||||
|
||||
if address_book_id.is_empty() {
|
||||
return Err(AppError::bad_request("Address book ID required"));
|
||||
}
|
||||
|
||||
|
||||
let mut update = UpdateAddressBookDto {
|
||||
name: None,
|
||||
description: None,
|
||||
@@ -565,7 +638,7 @@ async fn handle_proppatch(
|
||||
is_public: None,
|
||||
user_id: user.id.clone(),
|
||||
};
|
||||
|
||||
|
||||
for prop in &props_to_set {
|
||||
match prop.name.name.as_str() {
|
||||
"displayname" => update.name = Some(prop.value.clone().unwrap_or_default()),
|
||||
@@ -574,12 +647,16 @@ async fn handle_proppatch(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if update.name.is_some() || update.description.is_some() || update.color.is_some() {
|
||||
addressbook_service.update_address_book(address_book_id, update).await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to update address book: {}", e)))?;
|
||||
addressbook_service
|
||||
.update_address_book(address_book_id, update)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to update address book: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
let mut results = Vec::new();
|
||||
for prop in &props_to_set {
|
||||
results.push((&prop.name, true));
|
||||
@@ -587,15 +664,16 @@ async fn handle_proppatch(
|
||||
for prop in &props_to_remove {
|
||||
results.push((prop, true));
|
||||
}
|
||||
|
||||
|
||||
let href = format!("/carddav/{}", path);
|
||||
let mut response_body = Vec::new();
|
||||
crate::application::adapters::webdav_adapter::WebDavAdapter::generate_proppatch_response(
|
||||
&mut response_body,
|
||||
&href,
|
||||
&results,
|
||||
).map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::MULTI_STATUS)
|
||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||
|
||||
Reference in New Issue
Block a user