Skip to main content

cranpose_services/webdav/
folder.rs

1use 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
15/// A WebDAV folder whose files retain their original HTTP playback references.
16pub struct WebDavFolder {
17    url: url::Url,
18    client: HttpClientRef,
19}
20
21impl WebDavFolder {
22    /// Opens a folder address through the supplied HTTP client. No request is sent yet.
23    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    /// Converts this folder to the shared content interface.
39    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}