dav: Add interface for copy and move

This commit is contained in:
Lennart
2025-06-14 14:44:10 +02:00
parent 0669d4e683
commit 86ab6ef75e
8 changed files with 1717 additions and 390 deletions

13
Cargo.lock generated
View File

@@ -1622,6 +1622,17 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "matchit-serde"
version = "0.1.0"
source = "git+https://github.com/lennart-k/matchit-serde?rev=a6efbcee#a6efbcee66deb95e2a1dad5bf770ab457ce4f3aa"
dependencies = [
"derive_more",
"matchit",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "md-5"
version = "0.10.6"
@@ -2765,6 +2776,8 @@ dependencies = [
"http",
"itertools 0.14.0",
"log",
"matchit",
"matchit-serde",
"quick-xml",
"rustical_xml",
"serde",

View File

@@ -34,6 +34,7 @@ opentelemetry = [
debug = 0
[workspace.dependencies]
matchit = "0.8"
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
async-trait = "0.1"
axum = "0.8"
@@ -133,6 +134,7 @@ reqwest = { version = "0.12", features = [
], default-features = false }
openidconnect = "4.0"
clap = { version = "4.5", features = ["derive", "env"] }
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "a6efbcee" }
[dependencies]
rustical_store = { workspace = true }

View File

@@ -26,3 +26,5 @@ tokio.workspace = true
http.workspace = true
headers.workspace = true
strum.workspace = true
matchit.workspace = true
matchit-serde.workspace = true

View File

@@ -28,6 +28,9 @@ pub enum Error {
#[error("Precondition Failed")]
PreconditionFailed,
#[error("Forbidden")]
Forbidden,
}
impl Error {
@@ -49,6 +52,7 @@ impl Error {
Error::PropReadOnly => StatusCode::CONFLICT,
Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Forbidden => StatusCode::FORBIDDEN,
}
}
}

View File

@@ -1,25 +1,51 @@
use axum::{
extract::{Path, State},
response::{IntoResponse, Response},
};
use http::StatusCode;
use tracing::instrument;
use crate::{
header::{Depth, Overwrite},
resource::ResourceService,
};
use axum::{
extract::{MatchedPath, Path, State},
response::{IntoResponse, Response},
};
use http::{HeaderMap, StatusCode};
use matchit_serde::ParamsDeserializer;
use serde::Deserialize;
use tracing::instrument;
#[instrument(skip(_path, _resource_service,))]
#[instrument(skip(path, resource_service,))]
pub(crate) async fn axum_route_copy<R: ResourceService>(
Path(_path): Path<R::PathComponents>,
State(_resource_service): State<R>,
Path(path): Path<R::PathComponents>,
State(resource_service): State<R>,
depth: Option<Depth>,
principal: R::Principal,
overwrite: Overwrite,
matched_path: MatchedPath,
header_map: HeaderMap,
) -> Result<Response, R::Error> {
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but
// can return a 403 error
let _depth = depth.unwrap_or(Depth::Infinity);
Ok(StatusCode::FORBIDDEN.into_response())
let destination = header_map
.get("Destination")
.ok_or(crate::Error::Forbidden)?
.to_str()
.map_err(|_| crate::Error::Forbidden)?;
let mut router = matchit::Router::new();
router.insert(matched_path.as_str(), ()).unwrap();
if let Ok(matchit::Match { params, .. }) = router.at(destination) {
let params: Vec<(&str, &str)> = params.iter().collect();
let params = matchit_serde::Params(&params);
let dest_path = R::PathComponents::deserialize(&ParamsDeserializer::new(params))
.map_err(|_| crate::Error::Forbidden)?;
if resource_service
.copy_resource(&path, &dest_path, &principal, overwrite.is_true())
.await?
{
// Overwritten
Ok(StatusCode::NO_CONTENT.into_response())
} else {
// Not overwritten
Ok(StatusCode::CREATED.into_response())
}
} else {
Ok(StatusCode::FORBIDDEN.into_response())
}
}

View File

@@ -1,25 +1,51 @@
use axum::{
extract::{Path, State},
response::{IntoResponse, Response},
};
use http::StatusCode;
use tracing::instrument;
use crate::{
header::{Depth, Overwrite},
resource::ResourceService,
};
use axum::{
extract::{MatchedPath, Path, State},
response::{IntoResponse, Response},
};
use http::{HeaderMap, StatusCode};
use matchit_serde::ParamsDeserializer;
use serde::Deserialize;
use tracing::instrument;
#[instrument(skip(_path, _resource_service,))]
#[instrument(skip(path, resource_service,))]
pub(crate) async fn axum_route_move<R: ResourceService>(
Path(_path): Path<R::PathComponents>,
State(_resource_service): State<R>,
Path(path): Path<R::PathComponents>,
State(resource_service): State<R>,
depth: Option<Depth>,
principal: R::Principal,
overwrite: Overwrite,
matched_path: MatchedPath,
header_map: HeaderMap,
) -> Result<Response, R::Error> {
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but
// can return a 403 error
let _depth = depth.unwrap_or(Depth::Infinity);
Ok(StatusCode::FORBIDDEN.into_response())
let destination = header_map
.get("Destination")
.ok_or(crate::Error::Forbidden)?
.to_str()
.map_err(|_| crate::Error::Forbidden)?;
let mut router = matchit::Router::new();
router.insert(matched_path.as_str(), ()).unwrap();
if let Ok(matchit::Match { params, .. }) = router.at(destination) {
let params: Vec<(&str, &str)> = params.iter().collect();
let params = matchit_serde::Params(&params);
let dest_path = R::PathComponents::deserialize(&ParamsDeserializer::new(params))
.map_err(|_| crate::Error::Forbidden)?;
if resource_service
.copy_resource(&path, &dest_path, &principal, overwrite.is_true())
.await?
{
// Overwritten
Ok(StatusCode::NO_CONTENT.into_response())
} else {
// Not overwritten
Ok(StatusCode::CREATED.into_response())
}
} else {
Ok(StatusCode::FORBIDDEN.into_response())
}
}

View File

@@ -9,7 +9,13 @@ use serde::Deserialize;
#[async_trait]
pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
type PathComponents: for<'de> Deserialize<'de> + Sized + Send + Sync + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String)
type PathComponents: std::fmt::Debug
+ for<'de> Deserialize<'de>
+ Sized
+ Send
+ Sync
+ Clone
+ 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String)
type MemberType: Resource<Error = Self::Error, Principal = Self::Principal>
+ super::ResourceName;
type Resource: Resource<Error = Self::Error, Principal = Self::Principal>;
@@ -47,6 +53,28 @@ pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
Err(crate::Error::Unauthorized.into())
}
// Returns whether an existing resource was overwritten
async fn copy_resource(
&self,
_path: &Self::PathComponents,
_destination: &Self::PathComponents,
_user: &Self::Principal,
_overwrite: bool,
) -> Result<bool, Self::Error> {
Err(crate::Error::Forbidden.into())
}
// Returns whether an existing resource was overwritten
async fn move_resource(
&self,
_path: &Self::PathComponents,
_destination: &Self::PathComponents,
_user: &Self::Principal,
_overwrite: bool,
) -> Result<bool, Self::Error> {
Err(crate::Error::Forbidden.into())
}
fn axum_service(self) -> AxumService<Self>
where
Self: AxumMethods,

File diff suppressed because it is too large Load Diff