use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use io_http::rfc9110::request::HttpRequest;
use log::trace;
use url::Url;
use crate::rfc4918::{WebdavAuth, emit_header, resolve};
#[derive(Clone, Debug)]
pub struct WebdavRequest {
inner: HttpRequest,
}
impl WebdavRequest {
pub fn new(
base_url: &Url,
auth: &WebdavAuth,
user_agent: &str,
method: &str,
path: &str,
) -> Self {
let url = resolve(base_url, path);
let host = match (url.host_str(), url.port()) {
(Some(host), Some(port)) => format!("{host}:{port}"),
(Some(host), None) => host.to_string(),
(None, _) => String::new(),
};
let mut inner = HttpRequest::get(url).header("User-Agent", user_agent);
if !host.is_empty() {
inner = inner.header("Host", host);
}
if let Some(value) = emit_header(auth) {
inner = inner.header("Authorization", value);
}
inner.method = method.to_string();
Self { inner }
}
pub fn get(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "GET", path)
}
pub fn delete(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "DELETE", path)
}
pub fn put(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "PUT", path)
}
pub fn options(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "OPTIONS", path)
}
pub fn mkcol(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "MKCOL", path)
}
pub fn propfind(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "PROPFIND", path)
}
pub fn proppatch(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "PROPPATCH", path)
}
pub fn report(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "REPORT", path)
}
pub fn copy(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "COPY", path)
}
pub fn r#move(base_url: &Url, auth: &WebdavAuth, user_agent: &str, path: &str) -> Self {
Self::new(base_url, auth, user_agent, "MOVE", path)
}
pub fn depth(mut self, depth: u8) -> Self {
self.inner = self.inner.header("Depth", depth);
self
}
pub fn destination(mut self, destination: &str) -> Self {
self.inner = self.inner.header("Destination", destination);
self
}
pub fn overwrite(mut self, overwrite: bool) -> Self {
let value = if overwrite { "T" } else { "F" };
self.inner = self.inner.header("Overwrite", value);
self
}
pub fn if_match(mut self, etag: &str) -> Self {
self.inner = self.inner.header("If-Match", entity_tag(etag));
self
}
pub fn if_none_match(mut self, etag: &str) -> Self {
self.inner = self.inner.header("If-None-Match", entity_tag(etag));
self
}
pub fn content_type(mut self, value: &str) -> Self {
self.inner = self.inner.header("Content-Type", value);
self
}
pub fn content_type_xml(self) -> Self {
self.content_type("text/xml; charset=utf-8")
}
pub fn content_type_ical(self) -> Self {
self.content_type("text/calendar; charset=utf-8")
}
pub fn content_type_vcard(self) -> Self {
self.content_type("text/vcard; charset=utf-8")
}
pub fn body(mut self, body: Vec<u8>) -> HttpRequest {
if !body.is_empty() {
trace!("request body: {}", String::from_utf8_lossy(&body));
}
self.inner = self.inner.body(body);
self.inner
}
}
fn entity_tag(etag: &str) -> String {
if etag == "*" || etag.starts_with('"') || etag.starts_with("W/") {
etag.to_string()
} else {
format!("\"{etag}\"")
}
}
#[cfg(test)]
mod tests {
use io_http::rfc7617::basic::HttpAuthBasic;
use url::Url;
use crate::rfc4918::{WebdavAuth, request::*};
fn base() -> Url {
Url::parse("https://dav.example.org/dav/").unwrap()
}
#[test]
fn empty_path_returns_base() {
let req = WebdavRequest::propfind(&base(), &WebdavAuth::None, "io-webdav/test", "");
let request = req.body(Vec::new());
assert_eq!(request.url.as_str(), "https://dav.example.org/dav/");
}
#[test]
fn absolute_path_replaces() {
let req =
WebdavRequest::propfind(&base(), &WebdavAuth::None, "io-webdav/test", "/principals/");
let request = req.body(Vec::new());
assert_eq!(request.url.as_str(), "https://dav.example.org/principals/");
}
#[test]
fn relative_path_appends() {
let req = WebdavRequest::propfind(&base(), &WebdavAuth::None, "io-webdav/test", "personal");
let request = req.body(Vec::new());
assert_eq!(request.url.as_str(), "https://dav.example.org/dav/personal");
}
#[test]
fn auth_basic_emits_header() {
let auth = WebdavAuth::Basic(HttpAuthBasic::new("alice", "secret"));
let req = WebdavRequest::get(&base(), &auth, "io-webdav/test", "");
let request = req.body(Vec::new());
assert!(
request
.headers
.iter()
.any(|(name, value)| name == "Authorization" && value.starts_with("Basic "))
);
}
}