Skip to main content

opendal_service_webdav/
backend.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::fmt::Debug;
19use std::str::FromStr;
20use std::sync::Arc;
21
22use http::StatusCode;
23use log::debug;
24
25use super::WEBDAV_SCHEME;
26use super::config::WebdavConfig;
27use super::core::parse_error;
28use super::core::*;
29use super::deleter::WebdavDeleter;
30use super::lister::WebdavLister;
31use super::reader::*;
32use super::writer::WebdavWriter;
33use opendal_core::raw::oio;
34use opendal_core::raw::*;
35use opendal_core::*;
36
37/// [WebDAV](https://datatracker.ietf.org/doc/html/rfc4918) backend support.
38#[doc = include_str!("docs.md")]
39#[derive(Default)]
40pub struct WebdavBuilder {
41    pub(super) config: WebdavConfig,
42}
43
44impl Debug for WebdavBuilder {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("WebdavBuilder")
47            .field("config", &self.config)
48            .finish_non_exhaustive()
49    }
50}
51
52impl WebdavBuilder {
53    /// Set endpoint for http backend.
54    ///
55    /// For example: `https://example.com`
56    pub fn endpoint(mut self, endpoint: &str) -> Self {
57        self.config.endpoint = if endpoint.is_empty() {
58            None
59        } else {
60            Some(endpoint.to_string())
61        };
62
63        self
64    }
65
66    /// set the username for Webdav
67    ///
68    /// default: no username
69    pub fn username(mut self, username: &str) -> Self {
70        if !username.is_empty() {
71            self.config.username = Some(username.to_owned());
72        }
73        self
74    }
75
76    /// set the password for Webdav
77    ///
78    /// default: no password
79    pub fn password(mut self, password: &str) -> Self {
80        if !password.is_empty() {
81            self.config.password = Some(password.to_owned());
82        }
83        self
84    }
85
86    /// set the bearer token for Webdav
87    ///
88    /// default: no access token
89    pub fn token(mut self, token: &str) -> Self {
90        if !token.is_empty() {
91            self.config.token = Some(token.to_string());
92        }
93        self
94    }
95
96    /// Set root path of http backend.
97    pub fn root(mut self, root: &str) -> Self {
98        self.config.root = if root.is_empty() {
99            None
100        } else {
101            Some(root.to_string())
102        };
103
104        self
105    }
106
107    /// Disable automatic parent directory creation before write operations.
108    ///
109    /// By default, OpenDAL creates parent directories using MKCOL before writing files.
110    /// This requires PROPFIND support to check directory existence.
111    ///
112    /// Some WebDAV-compatible servers (e.g., bazel-remote) don't support PROPFIND
113    /// or don't require explicit directory creation. Enable this option to skip
114    /// the MKCOL calls and write files directly.
115    ///
116    /// Default: false
117    pub fn disable_create_dir(mut self, disable: bool) -> Self {
118        self.config.disable_create_dir = disable;
119        self
120    }
121
122    /// Deprecated: WebDAV user metadata capability is enabled by default.
123    #[deprecated(
124        since = "0.57.0",
125        note = "WebDAV user metadata capability is enabled by default. Use CapabilityOverrideLayer to override write_with_user_metadata for endpoints without PROPPATCH support."
126    )]
127    pub fn enable_user_metadata(self, _enable: bool) -> Self {
128        self
129    }
130
131    /// Set the XML namespace prefix for user metadata properties.
132    ///
133    /// This prefix is used in PROPPATCH/PROPFIND XML requests.
134    /// Different servers may require different prefixes.
135    ///
136    /// Default: "opendal"
137    pub fn user_metadata_prefix(mut self, prefix: &str) -> Self {
138        if !prefix.is_empty() {
139            self.config.user_metadata_prefix = Some(prefix.to_string());
140        }
141        self
142    }
143
144    /// Set the XML namespace URI for user metadata properties.
145    ///
146    /// This URI uniquely identifies the namespace for custom properties.
147    /// Different servers may require different namespace URIs.
148    ///
149    /// Default: `https://opendal.apache.org/ns`
150    pub fn user_metadata_uri(mut self, uri: &str) -> Self {
151        if !uri.is_empty() {
152            self.config.user_metadata_uri = Some(uri.to_string());
153        }
154        self
155    }
156
157    /// Enable conditional read support.
158    ///
159    /// When enabled (the default), OpenDAL forwards the RFC 7232 headers
160    /// `If-Match`, `If-None-Match`, `If-Modified-Since` and
161    /// `If-Unmodified-Since` to the server when callers provide them.
162    ///
163    /// Some WebDAV-compatible servers (e.g., nginx-dav) don't return ETags
164    /// in PROPFIND or don't honor these headers on GET. Setting this to
165    /// `false` drops the four `read_with_if_*` capabilities, so calls like
166    /// `reader_with(path).if_match(...)` return `ErrorKind::Unsupported`
167    /// locally instead of being silently ignored by the server.
168    ///
169    /// Default: true
170    pub fn enable_conditional_read(mut self, enable: bool) -> Self {
171        self.config.enable_conditional_read = enable;
172        self
173    }
174}
175
176impl Builder for WebdavBuilder {
177    type Config = WebdavConfig;
178
179    fn build(self) -> Result<impl Service> {
180        debug!("backend build started: {:?}", self);
181
182        let endpoint = match &self.config.endpoint {
183            Some(v) => v,
184            None => {
185                return Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is empty")
186                    .with_context("service", WEBDAV_SCHEME));
187            }
188        };
189        // Some services might return the path with suffix `/remote.php/webdav/`, we need to trim them.
190        let server_path = http::Uri::from_str(endpoint)
191            .map_err(|err| {
192                Error::new(ErrorKind::ConfigInvalid, "endpoint is invalid")
193                    .with_context("service", WEBDAV_SCHEME)
194                    .set_source(err)
195            })?
196            .path()
197            .trim_end_matches('/')
198            .to_string();
199
200        let root = normalize_root(&self.config.root.clone().unwrap_or_default());
201        debug!("backend use root {root}");
202
203        let mut authorization = None;
204        if let Some(username) = &self.config.username {
205            authorization = Some(format_authorization_by_basic(
206                username,
207                self.config.password.as_deref().unwrap_or_default(),
208            )?);
209        }
210        if let Some(token) = &self.config.token {
211            authorization = Some(format_authorization_by_bearer(token)?)
212        }
213
214        let conditional_read = self.config.enable_conditional_read;
215
216        let core = Arc::new(WebdavCore {
217            info: ServiceInfo::new(WEBDAV_SCHEME, &root, ""),
218            capability: Capability {
219                stat: true,
220
221                read: true,
222                read_with_suffix: true,
223                read_with_if_match: conditional_read,
224                read_with_if_none_match: conditional_read,
225                read_with_if_modified_since: conditional_read,
226                read_with_if_unmodified_since: conditional_read,
227
228                write: true,
229                write_can_empty: true,
230                write_with_user_metadata: true,
231
232                create_dir: true,
233                delete: true,
234
235                copy: true,
236
237                rename: true,
238
239                list: true,
240
241                // We already support recursive list but some details still need to polish.
242                // list_with_recursive: true,
243                shared: true,
244
245                ..Default::default()
246            },
247            endpoint: endpoint.to_string(),
248            server_path,
249            authorization,
250            root,
251            user_metadata_prefix: self
252                .config
253                .user_metadata_prefix
254                .unwrap_or_else(|| DEFAULT_USER_METADATA_PREFIX.to_string()),
255            user_metadata_uri: self
256                .config
257                .user_metadata_uri
258                .unwrap_or_else(|| DEFAULT_USER_METADATA_URI.to_string()),
259            disable_create_dir: self.config.disable_create_dir,
260        });
261        Ok(WebdavBackend { core })
262    }
263}
264
265#[derive(Clone, Debug)]
266pub struct WebdavBackend {
267    pub(crate) core: Arc<WebdavCore>,
268}
269
270impl Service for WebdavBackend {
271    type Reader = oio::StreamReader<WebdavReader>;
272    type Writer = oio::OneShotWriter<WebdavWriter>;
273    type Lister = oio::PageLister<WebdavLister>;
274    type Deleter = oio::OneShotDeleter<WebdavDeleter>;
275    type Copier = oio::OneShotCopier;
276
277    fn info(&self) -> ServiceInfo {
278        self.core.info.clone()
279    }
280
281    fn capability(&self) -> Capability {
282        self.core.capability
283    }
284
285    async fn create_dir(
286        &self,
287        ctx: &OperationContext,
288        path: &str,
289        _: OpCreateDir,
290    ) -> Result<RpCreateDir> {
291        self.core.webdav_mkcol(ctx, path).await?;
292        Ok(RpCreateDir::default())
293    }
294
295    async fn stat(&self, ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
296        let metadata = self.core.webdav_stat(ctx, path).await?;
297        Ok(RpStat::new(metadata))
298    }
299    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
300        let output: oio::StreamReader<WebdavReader> = {
301            Ok(oio::StreamReader::new(WebdavReader::new(
302                self.clone(),
303                ctx.clone(),
304                path,
305                args,
306            )))
307        }?;
308
309        Ok(output)
310    }
311
312    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
313        let output: oio::OneShotWriter<WebdavWriter> = {
314            Ok(oio::OneShotWriter::new(WebdavWriter::new(
315                self.core.clone(),
316                ctx.clone(),
317                args,
318                path.to_string(),
319            )))
320        }?;
321
322        Ok(output)
323    }
324
325    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
326        let output: oio::OneShotDeleter<WebdavDeleter> = {
327            Ok(oio::OneShotDeleter::new(WebdavDeleter::new(
328                self.core.clone(),
329                ctx.clone(),
330            )))
331        }?;
332
333        Ok(output)
334    }
335
336    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
337        let output: oio::PageLister<WebdavLister> = {
338            Ok(oio::PageLister::new(WebdavLister::new(
339                self.core.clone(),
340                ctx.clone(),
341                path,
342                args,
343            )))
344        }?;
345
346        Ok(output)
347    }
348
349    fn copy(
350        &self,
351        ctx: &OperationContext,
352        from: &str,
353        to: &str,
354        _args: OpCopy,
355        _opts: OpCopier,
356    ) -> Result<Self::Copier> {
357        let core = self.core.clone();
358        let ctx = ctx.clone();
359        let from = from.to_string();
360        let to = to.to_string();
361
362        Ok(oio::OneShotCopier::new_with(move || {
363            let core = core.clone();
364            let ctx = ctx.clone();
365            let from = from.clone();
366            let to = to.clone();
367
368            async move {
369                let resp = core.webdav_copy(&ctx, &from, &to).await?;
370                let status = resp.status();
371
372                match status {
373                    StatusCode::CREATED | StatusCode::NO_CONTENT => Ok(Metadata::default()),
374                    _ => Err(parse_error(resp)),
375                }
376            }
377        }))
378    }
379
380    async fn rename(
381        &self,
382        ctx: &OperationContext,
383        from: &str,
384        to: &str,
385        _args: OpRename,
386    ) -> Result<RpRename> {
387        let resp = self.core.webdav_move(ctx, from, to).await?;
388
389        let status = resp.status();
390        match status {
391            StatusCode::CREATED | StatusCode::NO_CONTENT | StatusCode::OK => {
392                Ok(RpRename::default())
393            }
394            _ => Err(parse_error(resp)),
395        }
396    }
397
398    async fn presign(
399        &self,
400        _ctx: &OperationContext,
401        _path: &str,
402        _args: OpPresign,
403    ) -> Result<RpPresign> {
404        Err(Error::new(
405            ErrorKind::Unsupported,
406            "operation is not supported",
407        ))
408    }
409}