1use alloc::{
13 format,
14 string::{String, ToString},
15 vec::Vec,
16};
17
18use io_http::rfc9110::request::HttpRequest;
19use log::trace;
20use url::Url;
21
22use crate::rfc4918::{WebdavAuth, emit_header, resolve};
23
24#[derive(Clone, Debug)]
26pub struct WebdavRequest {
27 inner: HttpRequest,
28}
29
30impl WebdavRequest {
31 pub fn new(
36 base_url: &Url,
37 auth: &WebdavAuth,
38 user_agent: &str,
39 method: &str,
40 path: &str,
41 ) -> Self {
42 let url = resolve(base_url, path);
43
44 let host = match (url.host_str(), url.port()) {
45 (Some(host), Some(port)) => format!("{host}:{port}"),
46 (Some(host), None) => host.to_string(),
47 (None, _) => String::new(),
48 };
49
50 let mut inner = HttpRequest::get(url).header("User-Agent", user_agent);
51
52 if !host.is_empty() {
53 inner = inner.header("Host", host);
54 }
55
56 if let Some(value) = emit_header(auth) {
57 inner = inner.header("Authorization", value);
58 }
59
60 inner.method = method.to_string();
61
62 Self { inner }
63 }
64
65 pub fn get(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
67 Self::new(base_url, auth, user_agent, "GET", path)
68 }
69
70 pub fn delete(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
72 Self::new(base_url, auth, user_agent, "DELETE", path)
73 }
74
75 pub fn put(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
77 Self::new(base_url, auth, user_agent, "PUT", path)
78 }
79
80 pub fn options(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
82 Self::new(base_url, auth, user_agent, "OPTIONS", path)
83 }
84
85 pub fn mkcol(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
87 Self::new(base_url, auth, user_agent, "MKCOL", path)
88 }
89
90 pub fn propfind(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
92 Self::new(base_url, auth, user_agent, "PROPFIND", path)
93 }
94
95 pub fn proppatch(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
97 Self::new(base_url, auth, user_agent, "PROPPATCH", path)
98 }
99
100 pub fn report(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
102 Self::new(base_url, auth, user_agent, "REPORT", path)
103 }
104
105 pub fn copy(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
107 Self::new(base_url, auth, user_agent, "COPY", path)
108 }
109
110 pub fn r#move(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
112 Self::new(base_url, auth, user_agent, "MOVE", path)
113 }
114
115 pub fn depth(mut self, depth: u8) -> Self {
117 self.inner = self.inner.header("Depth", depth);
118 self
119 }
120
121 pub fn destination(mut self, destination: &str) -> Self {
123 self.inner = self.inner.header("Destination", destination);
124 self
125 }
126
127 pub fn overwrite(mut self, overwrite: bool) -> Self {
129 let value = if overwrite { "T" } else { "F" };
130 self.inner = self.inner.header("Overwrite", value);
131 self
132 }
133
134 pub fn if_match(mut self, etag: &str) -> Self {
136 self.inner = self.inner.header("If-Match", entity_tag(etag));
137 self
138 }
139
140 pub fn if_none_match(mut self, etag: &str) -> Self {
143 self.inner = self.inner.header("If-None-Match", entity_tag(etag));
144 self
145 }
146
147 pub fn content_type(mut self, value: &str) -> Self {
149 self.inner = self.inner.header("Content-Type", value);
150 self
151 }
152
153 pub fn content_type_xml(self) -> Self {
155 self.content_type("text/xml; charset=utf-8")
156 }
157
158 pub fn content_type_ical(self) -> Self {
160 self.content_type("text/calendar; charset=utf-8")
161 }
162
163 pub fn content_type_vcard(self) -> Self {
165 self.content_type("text/vcard; charset=utf-8")
166 }
167
168 pub fn body(mut self, body: Vec<u8>) -> HttpRequest {
175 if !body.is_empty() {
176 trace!("request body: {}", String::from_utf8_lossy(&body));
177 }
178 self.inner = self.inner.body(body);
179 self.inner
180 }
181}
182
183fn entity_tag(etag: &str) -> String {
187 if etag == "*" || etag.starts_with('"') || etag.starts_with("W/") {
188 etag.to_string()
189 } else {
190 format!("\"{etag}\"")
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use io_http::rfc7617::basic::HttpAuthBasic;
197 use url::Url;
198
199 use crate::rfc4918::{WebdavAuth, request::*};
200
201 fn base() -> Url {
202 Url::parse("https://dav.example.org/dav/").unwrap()
203 }
204
205 #[test]
206 fn empty_path_returns_base() {
207 let req = WebdavRequest::propfind(&base(), &WebdavAuth::None, "io-webdav/test", "");
208 let request = req.body(Vec::new());
209 assert_eq!(request.url.as_str(), "https://dav.example.org/dav/");
210 }
211
212 #[test]
213 fn absolute_path_replaces() {
214 let req =
215 WebdavRequest::propfind(&base(), &WebdavAuth::None, "io-webdav/test", "/principals/");
216 let request = req.body(Vec::new());
217 assert_eq!(request.url.as_str(), "https://dav.example.org/principals/");
218 }
219
220 #[test]
221 fn relative_path_appends() {
222 let req = WebdavRequest::propfind(&base(), &WebdavAuth::None, "io-webdav/test", "personal");
223 let request = req.body(Vec::new());
224 assert_eq!(request.url.as_str(), "https://dav.example.org/dav/personal");
225 }
226
227 #[test]
228 fn auth_basic_emits_header() {
229 let auth = WebdavAuth::Basic(HttpAuthBasic::new("alice", "secret"));
230 let req = WebdavRequest::get(&base(), &auth, "io-webdav/test", "");
231 let request = req.body(Vec::new());
232 assert!(
233 request
234 .headers
235 .iter()
236 .any(|(name, value)| name == "Authorization" && value.starts_with("Basic "))
237 );
238 }
239}