aster_forge_webdav/
put.rs1use http::header::{CACHE_CONTROL, CONTENT_LOCATION, IF_MATCH, IF_NONE_MATCH};
4use http::{HeaderMap, HeaderValue, StatusCode};
5
6use crate::{
7 DavPath, DavProtocolError, DavResponse, evaluate_http_etag_preconditions, href_for_dav_path,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum DavPutResourceState<'a> {
13 Missing,
14 File { etag: Option<&'a str> },
15 Collection,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct DavPutPlan {
21 pub resource_existed: bool,
22 pub create: bool,
23 pub create_new: bool,
24 pub content_length_hint: Option<u64>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
29pub enum DavPutPlanError {
30 #[error(transparent)]
31 Protocol(#[from] DavProtocolError),
32 #[error("PUT cannot replace a collection")]
33 CollectionTarget,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
38#[error("invalid PUT Content-Location response header")]
39pub struct DavPutResponseError;
40
41pub fn plan_put_request(
43 headers: &HeaderMap,
44 state: DavPutResourceState<'_>,
45) -> Result<DavPutPlan, DavPutPlanError> {
46 let (resource_existed, etag) = match state {
47 DavPutResourceState::Missing => (false, None),
48 DavPutResourceState::File { etag } => (true, etag),
49 DavPutResourceState::Collection => return Err(DavPutPlanError::CollectionTarget),
50 };
51 evaluate_http_etag_preconditions(headers, resource_existed, etag, false)?;
52 Ok(DavPutPlan {
53 resource_existed,
54 create: !header_equals(headers, IF_MATCH, "*"),
55 create_new: header_equals(headers, IF_NONE_MATCH, "*"),
56 content_length_hint: content_length_hint(headers),
57 })
58}
59
60#[must_use]
62pub fn put_plan_error_response(error: &DavPutPlanError) -> DavResponse {
63 match error {
64 DavPutPlanError::Protocol(error) => crate::protocol_error_response(error),
65 DavPutPlanError::CollectionTarget => {
66 let mut response = DavResponse::empty(StatusCode::METHOD_NOT_ALLOWED);
67 response
68 .headers
69 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
70 response
71 }
72 }
73}
74
75pub fn put_success_response(
77 plan: &DavPutPlan,
78 prefix: &str,
79 path: &DavPath,
80) -> Result<DavResponse, DavPutResponseError> {
81 if plan.resource_existed {
82 return Ok(DavResponse::empty(StatusCode::NO_CONTENT));
83 }
84 let mut response = DavResponse::empty(StatusCode::CREATED);
85 let location =
86 HeaderValue::from_str(&href_for_dav_path(prefix, path)).map_err(|_| DavPutResponseError)?;
87 response.headers.insert(CONTENT_LOCATION, location);
88 Ok(response)
89}
90
91fn content_length_hint(headers: &HeaderMap) -> Option<u64> {
92 headers
93 .get("X-Expected-Entity-Length")
94 .and_then(|value| value.to_str().ok())
95 .and_then(|value| value.trim().parse::<u64>().ok())
96 .or_else(|| {
97 headers
98 .get(http::header::CONTENT_LENGTH)
99 .and_then(|value| value.to_str().ok())
100 .and_then(|value| value.trim().parse::<u64>().ok())
101 })
102}
103
104fn header_equals(headers: &HeaderMap, name: http::header::HeaderName, expected: &str) -> bool {
105 headers
106 .get(name)
107 .and_then(|value| value.to_str().ok())
108 .is_some_and(|value| value.trim() == expected)
109}