Checkpoint: Migration to axum

This commit is contained in:
Lennart
2025-06-08 14:10:12 +02:00
parent 790c657b08
commit 95889e3df1
60 changed files with 1476 additions and 2205 deletions

View File

@@ -1,10 +1,12 @@
use crate::Error;
use actix_web::web::Path;
use actix_web::{HttpResponse, web::Data};
use rustical_store::{Addressbook, AddressbookStore, auth::User};
use crate::{Error, addressbook::resource::AddressbookResourceService};
use axum::{
extract::{Path, State},
response::{IntoResponse, Response},
};
use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::User};
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument;
use tracing_actix_web::RootSpan;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub struct Resourcetype {
@@ -39,15 +41,13 @@ struct MkcolRequest {
set: PropElement<MkcolAddressbookProp>,
}
#[instrument(parent = root_span.id(), skip(store, root_span))]
pub async fn route_mkcol<AS: AddressbookStore>(
path: Path<(String, String)>,
body: String,
#[instrument(skip(addr_store))]
pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>,
user: User,
store: Data<AS>,
root_span: RootSpan,
) -> Result<HttpResponse, Error> {
let (principal, addressbook_id) = path.into_inner();
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
}
@@ -65,7 +65,7 @@ pub async fn route_mkcol<AS: AddressbookStore>(
push_topic: uuid::Uuid::new_v4().to_string(),
};
match store
match addr_store
.get_addressbook(&principal, &addressbook_id, true)
.await
{
@@ -74,7 +74,11 @@ pub async fn route_mkcol<AS: AddressbookStore>(
}
Ok(_) => {
// oh no, there's a conflict
return Ok(HttpResponse::Conflict().body("An addressbook already exists at this URI"));
return Ok((
StatusCode::CONFLICT,
"An addressbook already exists at this URI",
)
.into_response());
}
Err(err) => {
// some other error
@@ -82,12 +86,10 @@ pub async fn route_mkcol<AS: AddressbookStore>(
}
}
match store.insert_addressbook(addressbook).await {
match addr_store.insert_addressbook(addressbook).await {
// TODO: The spec says we should return a mkcol-response.
// However, it works without one but breaks on iPadOS when using an empty one :)
Ok(()) => Ok(HttpResponse::Created()
.insert_header(("Cache-Control", "no-cache"))
.body("")),
Ok(()) => Ok(StatusCode::CREATED.into_response()),
Err(err) => {
dbg!(err.to_string());
Err(err.into())

View File

@@ -1,3 +1,3 @@
pub mod mkcol;
pub mod post;
// pub mod post;
pub mod report;

View File

@@ -4,8 +4,6 @@ use crate::{
AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource,
},
};
use actix_web::dev::{Path, ResourceDef};
use http::StatusCode;
use rustical_dav::{
resource::{PrincipalUri, Resource},
@@ -32,27 +30,28 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
addressbook_id: &str,
store: &AS,
) -> Result<(Vec<AddressObject>, Vec<String>), Error> {
let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.vcf"));
let mut result = vec![];
let mut not_found = vec![];
for href in &addressbook_multiget.href {
let mut path = Path::new(href.as_str());
if !resource_def.capture_match_info(&mut path) {
if let Some(filename) = href.strip_prefix(path) {
if let Some(object_id) = filename.strip_suffix(".vcf") {
match store
.get_object(principal, addressbook_id, object_id, false)
.await
{
Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
Err(err) => return Err(err.into()),
};
} else {
not_found.push(href.to_owned());
continue;
}
} else {
not_found.push(href.to_owned());
continue;
};
let object_id = path.get("object_id").unwrap();
match store
.get_object(principal, addressbook_id, object_id, false)
.await
{
Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
// TODO: Maybe add error handling on a per-object basis
Err(err) => return Err(err.into()),
};
}
}
Ok((result, not_found))

View File

@@ -1,11 +1,15 @@
use crate::{CardDavPrincipalUri, Error, address_object::resource::AddressObjectPropWrapperName};
use actix_web::{
HttpRequest, Responder,
web::{Data, Path},
use crate::{
CardDavPrincipalUri, Error, address_object::resource::AddressObjectPropWrapperName,
addressbook::resource::AddressbookResourceService,
};
use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget};
use axum::{
Extension,
extract::{OriginalUri, Path, State},
response::IntoResponse,
};
use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest};
use rustical_store::{AddressbookStore, auth::User};
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User};
use rustical_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection;
use tracing::instrument;
@@ -30,16 +34,15 @@ impl ReportRequest {
}
}
#[instrument(skip(req, addr_store))]
pub async fn route_report_addressbook<AS: AddressbookStore>(
path: Path<(String, String)>,
body: String,
#[instrument(skip(addr_store))]
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>,
user: User,
req: HttpRequest,
puri: Data<CardDavPrincipalUri>,
addr_store: Data<AS>,
) -> Result<impl Responder, Error> {
let (principal, addressbook_id) = path.into_inner();
OriginalUri(uri): OriginalUri,
Extension(puri): Extension<CardDavPrincipalUri>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
body: String,
) -> Result<impl IntoResponse, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
}
@@ -51,8 +54,8 @@ pub async fn route_report_addressbook<AS: AddressbookStore>(
handle_addressbook_multiget(
addr_multiget,
request.props(),
req.path(),
puri.as_ref(),
uri.path(),
&puri,
&user,
&principal,
&addressbook_id,
@@ -63,8 +66,8 @@ pub async fn route_report_addressbook<AS: AddressbookStore>(
ReportRequest::SyncCollection(sync_collection) => {
handle_sync_collection(
sync_collection,
req.path(),
puri.as_ref(),
uri.path(),
&puri,
&user,
&principal,
&addressbook_id,

View File

@@ -1,25 +1,27 @@
use super::methods::mkcol::route_mkcol;
use super::methods::post::route_post;
use super::methods::report::route_report_addressbook;
use super::prop::{SupportedAddressData, SupportedReportSet};
use crate::address_object::resource::{AddressObjectResource, AddressObjectResourceService};
use crate::address_object::resource::AddressObjectResource;
use crate::{CardDavPrincipalUri, Error};
use actix_web::http::Method;
use actix_web::web;
use async_trait::async_trait;
use axum::extract::Request;
use axum::handler::Handler;
use axum::response::Response;
use derive_more::derive::{From, Into};
use futures_util::future::BoxFuture;
use rustical_dav::extensions::{
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
};
use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{PrincipalUri, Resource, ResourceService};
use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService};
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
use rustical_store::auth::User;
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore};
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
use std::str::FromStr;
use std::convert::Infallible;
use std::sync::Arc;
use tower::Service;
pub struct AddressbookResourceService<AS: AddressbookStore, S: SubscriptionStore> {
pub(crate) addr_store: Arc<AS>,
@@ -35,6 +37,15 @@ impl<A: AddressbookStore, S: SubscriptionStore> AddressbookResourceService<A, S>
}
}
impl<A: AddressbookStore, S: SubscriptionStore> Clone for AddressbookResourceService<A, S> {
fn clone(&self) -> Self {
Self {
addr_store: self.addr_store.clone(),
sub_store: self.sub_store.clone(),
}
}
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "AddressbookPropName")]
pub enum AddressbookProp {
@@ -255,18 +266,20 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
.await?;
Ok(())
}
}
#[inline]
fn actix_scope(self) -> actix_web::Scope {
let mkcol_method = web::method(Method::from_str("MKCOL").unwrap());
let report_method = web::method(Method::from_str("REPORT").unwrap());
web::scope("/{addressbook_id}")
.service(AddressObjectResourceService::<AS>::new(self.addr_store.clone()).actix_scope())
.service(
self.actix_resource()
.route(mkcol_method.to(route_mkcol::<AS>))
.route(report_method.to(route_report_addressbook::<AS>))
.post(route_post::<AS, S>),
)
impl<AS: AddressbookStore, S: SubscriptionStore> AxumMethods for AddressbookResourceService<AS, S> {
fn report() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(route_report_addressbook::<AS, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn mkcol() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(route_mkcol::<AS, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
}