mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
dav: Add interface for copy and move
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -1622,6 +1622,17 @@ version = "0.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
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]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -2765,6 +2776,8 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"log",
|
"log",
|
||||||
|
"matchit",
|
||||||
|
"matchit-serde",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rustical_xml",
|
"rustical_xml",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ opentelemetry = [
|
|||||||
debug = 0
|
debug = 0
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
matchit = "0.8"
|
||||||
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
@@ -133,6 +134,7 @@ reqwest = { version = "0.12", features = [
|
|||||||
], default-features = false }
|
], default-features = false }
|
||||||
openidconnect = "4.0"
|
openidconnect = "4.0"
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
|
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "a6efbcee" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_store = { workspace = true }
|
rustical_store = { workspace = true }
|
||||||
|
|||||||
@@ -26,3 +26,5 @@ tokio.workspace = true
|
|||||||
http.workspace = true
|
http.workspace = true
|
||||||
headers.workspace = true
|
headers.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
|
matchit.workspace = true
|
||||||
|
matchit-serde.workspace = true
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("Precondition Failed")]
|
#[error("Precondition Failed")]
|
||||||
PreconditionFailed,
|
PreconditionFailed,
|
||||||
|
|
||||||
|
#[error("Forbidden")]
|
||||||
|
Forbidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
@@ -49,6 +52,7 @@ impl Error {
|
|||||||
Error::PropReadOnly => StatusCode::CONFLICT,
|
Error::PropReadOnly => StatusCode::CONFLICT,
|
||||||
Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
|
Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
|
||||||
Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::Forbidden => StatusCode::FORBIDDEN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,51 @@
|
|||||||
use axum::{
|
|
||||||
extract::{Path, State},
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use http::StatusCode;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
header::{Depth, Overwrite},
|
header::{Depth, Overwrite},
|
||||||
resource::ResourceService,
|
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>(
|
pub(crate) async fn axum_route_copy<R: ResourceService>(
|
||||||
Path(_path): Path<R::PathComponents>,
|
Path(path): Path<R::PathComponents>,
|
||||||
State(_resource_service): State<R>,
|
State(resource_service): State<R>,
|
||||||
depth: Option<Depth>,
|
depth: Option<Depth>,
|
||||||
principal: R::Principal,
|
principal: R::Principal,
|
||||||
overwrite: Overwrite,
|
overwrite: Overwrite,
|
||||||
|
matched_path: MatchedPath,
|
||||||
|
header_map: HeaderMap,
|
||||||
) -> Result<Response, R::Error> {
|
) -> Result<Response, R::Error> {
|
||||||
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but
|
let destination = header_map
|
||||||
// can return a 403 error
|
.get("Destination")
|
||||||
let _depth = depth.unwrap_or(Depth::Infinity);
|
.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(¶ms);
|
||||||
|
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())
|
Ok(StatusCode::FORBIDDEN.into_response())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,51 @@
|
|||||||
use axum::{
|
|
||||||
extract::{Path, State},
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use http::StatusCode;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
header::{Depth, Overwrite},
|
header::{Depth, Overwrite},
|
||||||
resource::ResourceService,
|
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>(
|
pub(crate) async fn axum_route_move<R: ResourceService>(
|
||||||
Path(_path): Path<R::PathComponents>,
|
Path(path): Path<R::PathComponents>,
|
||||||
State(_resource_service): State<R>,
|
State(resource_service): State<R>,
|
||||||
depth: Option<Depth>,
|
depth: Option<Depth>,
|
||||||
principal: R::Principal,
|
principal: R::Principal,
|
||||||
overwrite: Overwrite,
|
overwrite: Overwrite,
|
||||||
|
matched_path: MatchedPath,
|
||||||
|
header_map: HeaderMap,
|
||||||
) -> Result<Response, R::Error> {
|
) -> Result<Response, R::Error> {
|
||||||
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but
|
let destination = header_map
|
||||||
// can return a 403 error
|
.get("Destination")
|
||||||
let _depth = depth.unwrap_or(Depth::Infinity);
|
.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(¶ms);
|
||||||
|
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())
|
Ok(StatusCode::FORBIDDEN.into_response())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ use serde::Deserialize;
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
|
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>
|
type MemberType: Resource<Error = Self::Error, Principal = Self::Principal>
|
||||||
+ super::ResourceName;
|
+ super::ResourceName;
|
||||||
type Resource: Resource<Error = Self::Error, Principal = Self::Principal>;
|
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())
|
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>
|
fn axum_service(self) -> AxumService<Self>
|
||||||
where
|
where
|
||||||
Self: AxumMethods,
|
Self: AxumMethods,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user