cranpose_services/webdav/
folder.rs1use std::{rc::Rc, sync::Arc};
2
3use super::{
4 item::{WebDavItem, metadata},
5 listing::{self, EntryKind},
6};
7use crate::{
8 ContentEntry, ContentError, ContentFolder, ContentFolderRef, ContentFuture, ContentMetadata,
9 HttpClientRef, HttpControl, HttpMethod, HttpRequest,
10};
11
12const MAX_LISTING_BYTES: usize = 16 * 1024 * 1024;
13const PROPERTIES: &str = r#"<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getcontentlength/><d:getcontenttype/></d:prop></d:propfind>"#;
14
15pub struct WebDavFolder {
17 url: url::Url,
18 client: HttpClientRef,
19}
20
21impl WebDavFolder {
22 pub fn new(address: &str, client: HttpClientRef) -> Result<Self, ContentError> {
24 let mut url = url::Url::parse(address).map_err(|e| ContentError::Io(e.to_string()))?;
25 if !matches!(url.scheme(), "http" | "https") || url.host().is_none() {
26 return Err(ContentError::Unsupported("non-HTTP WebDAV folders"));
27 }
28 if !url.username().is_empty() || url.password().is_some() {
29 return Err(ContentError::Unsupported("credentials inside folder URLs"));
30 }
31 if !url.path().ends_with('/') {
32 url.set_path(&format!("{}/", url.path()));
33 }
34 url.set_fragment(None);
35 Ok(Self { url, client })
36 }
37
38 pub fn handle(self) -> ContentFolderRef {
40 Rc::new(self)
41 }
42}
43
44impl ContentFolder for WebDavFolder {
45 fn metadata(&self) -> ContentMetadata {
46 metadata(&self.url)
47 }
48
49 fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>> {
50 Box::pin(async move {
51 let request = HttpRequest::get(self.url.as_str())
52 .method(HttpMethod::Propfind)
53 .header("Depth", "1")
54 .header("Content-Type", "application/xml; charset=utf-8")
55 .body(PROPERTIES);
56 let control = HttpControl::new();
57 let response = self
58 .client
59 .send(&request, control.clone())
60 .await
61 .map_err(|e| ContentError::Io(e.to_string()))?;
62 if response.status != 207 {
63 return Err(ContentError::Io(format!(
64 "WebDAV folder returned HTTP {}",
65 response.status
66 )));
67 }
68 let mut bytes = Vec::new();
69 while let Some(chunk) = response
70 .read_chunk()
71 .await
72 .map_err(|e| ContentError::Io(e.to_string()))?
73 {
74 if chunk.len() > MAX_LISTING_BYTES - bytes.len() {
75 control.cancel();
76 return Err(ContentError::Io(
77 "WebDAV directory listing exceeds 16 MiB".into(),
78 ));
79 }
80 bytes.extend_from_slice(&chunk);
81 }
82 let parent = self.url.clone();
83 #[cfg(not(target_arch = "wasm32"))]
84 let entries = cranpose_core::withBlocking(move || listing::parse(&bytes, &parent))
85 .await
86 .map_err(|e| ContentError::Io(e.to_string()))??;
87 #[cfg(target_arch = "wasm32")]
88 let entries = listing::parse(&bytes, &parent)?;
89 Ok(entries
90 .into_iter()
91 .map(|entry| match entry.kind {
92 EntryKind::Folder => ContentEntry::Folder(
93 Self {
94 url: entry.url,
95 client: Arc::clone(&self.client),
96 }
97 .handle(),
98 ),
99 EntryKind::File { len, mime_type } => ContentEntry::File(Rc::new(
100 WebDavItem::new(entry.url, len, mime_type, Arc::clone(&self.client)),
101 )),
102 })
103 .collect())
104 })
105 }
106}