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    type Composer = ();
277
278    fn info(&self) -> ServiceInfo {
279        self.core.info.clone()
280    }
281
282    fn capability(&self) -> Capability {
283        self.core.capability
284    }
285
286    async fn create_dir(
287        &self,
288        ctx: &OperationContext,
289        path: &str,
290        _: OpCreateDir,
291    ) -> Result<RpCreateDir> {
292        self.core.webdav_mkcol(ctx, path).await?;
293        Ok(RpCreateDir::default())
294    }
295
296    async fn stat(&self, ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
297        let metadata = self.core.webdav_stat(ctx, path).await?;
298        Ok(RpStat::new(metadata))
299    }
300    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
301        let output: oio::StreamReader<WebdavReader> = {
302            Ok(oio::StreamReader::new(WebdavReader::new(
303                self.clone(),
304                ctx.clone(),
305                path,
306                args,
307            )))
308        }?;
309
310        Ok(output)
311    }
312
313    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
314        let output: oio::OneShotWriter<WebdavWriter> = {
315            Ok(oio::OneShotWriter::new(WebdavWriter::new(
316                self.core.clone(),
317                ctx.clone(),
318                args,
319                path.to_string(),
320            )))
321        }?;
322
323        Ok(output)
324    }
325
326    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
327        let output: oio::OneShotDeleter<WebdavDeleter> = {
328            Ok(oio::OneShotDeleter::new(WebdavDeleter::new(
329                self.core.clone(),
330                ctx.clone(),
331            )))
332        }?;
333
334        Ok(output)
335    }
336
337    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
338        let output: oio::PageLister<WebdavLister> = {
339            Ok(oio::PageLister::new(WebdavLister::new(
340                self.core.clone(),
341                ctx.clone(),
342                path,
343                args,
344            )))
345        }?;
346
347        Ok(output)
348    }
349
350    fn copy(
351        &self,
352        ctx: &OperationContext,
353        from: &str,
354        to: &str,
355        args: OpCopy,
356    ) -> Result<Self::Copier> {
357        let backend = self.clone();
358        let core = self.core.clone();
359        let ctx = ctx.clone();
360        let from = from.to_string();
361        let to = to.to_string();
362        let source_content_length_hint = args.source_content_length_hint();
363
364        Ok(oio::OneShotCopier::new_with(move || {
365            let backend = backend.clone();
366            let core = core.clone();
367            let ctx = ctx.clone();
368            let from = from.clone();
369            let to = to.clone();
370
371            async move {
372                let source_size = match source_content_length_hint {
373                    Some(size) => size,
374                    None => backend
375                        .stat(&ctx, &from, OpStat::default())
376                        .await?
377                        .into_metadata()
378                        .content_length(),
379                };
380
381                let resp = core.webdav_copy(&ctx, &from, &to).await?;
382                let status = resp.status();
383
384                match status {
385                    StatusCode::CREATED | StatusCode::NO_CONTENT => {
386                        Ok(MetadataBuilder::file(source_size).build())
387                    }
388                    _ => Err(parse_error(
389                        ErrorContext::new(ServiceOperation("Copy")),
390                        resp,
391                    )),
392                }
393            }
394        }))
395    }
396
397    async fn rename(
398        &self,
399        ctx: &OperationContext,
400        from: &str,
401        to: &str,
402        _args: OpRename,
403    ) -> Result<RpRename> {
404        let resp = self.core.webdav_move(ctx, from, to).await?;
405
406        let status = resp.status();
407        match status {
408            StatusCode::CREATED | StatusCode::NO_CONTENT | StatusCode::OK => {
409                Ok(RpRename::default())
410            }
411            _ => Err(parse_error(
412                ErrorContext::new(ServiceOperation("Move")),
413                resp,
414            )),
415        }
416    }
417
418    async fn presign(
419        &self,
420        _ctx: &OperationContext,
421        _path: &str,
422        _args: OpPresign,
423    ) -> Result<RpPresign> {
424        Err(Error::new(
425            ErrorKind::Unsupported,
426            "operation is not supported",
427        ))
428    }
429}