remotefs_webdav/
lib.rs

1#![crate_name = "remotefs_webdav"]
2#![crate_type = "lib"]
3
4//! # remotefs-webdav
5//!
6//! remotefs is a library that provides a client implementation of [Remotefs-rs](https://github.com/veeso/remotefs-rs)
7//! for the WebDAV protocol as specified in [RFC4918](https://www.rfc-editor.org/rfc/rfc4918).
8//!
9//! ## Get started
10//!
11//! First of all you need to add **remotefs** and **remotefs-webdav** to your project dependencies:
12//!
13//! ```toml
14//! [dependencies]
15//! remotefs = "^0.3"
16//! remotefs-webdav = "^0.2"
17//! ```
18//!
19//! these features are supported:
20//!
21//! - `no-log`: disable logging. By default, this library will log via the `log` crate.
22
23#![doc(html_playground_url = "https://play.rust-lang.org")]
24#![doc(
25    html_favicon_url = "https://raw.githubusercontent.com/remotefs-rs/remotefs-rs/main/assets/logo-128.png"
26)]
27#![doc(
28    html_logo_url = "https://raw.githubusercontent.com/remotefs-rs/remotefs-rs/main/assets/logo.png"
29)]
30
31#[macro_use]
32extern crate log;
33
34#[cfg(test)]
35mod mock;
36mod parser;
37mod webdav_xml;
38
39use std::io::Read;
40use std::path::{Path, PathBuf};
41
42use remotefs::fs::{Metadata, ReadStream, UnixPex, Welcome, WriteStream};
43use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs, RemoteResult};
44use rustydav::client::Client;
45
46use self::parser::ResponseParser;
47
48/// WebDAV remote fs client
49pub struct WebDAVFs {
50    client: Client,
51    url: String,
52    wrkdir: String,
53    connected: bool,
54}
55
56impl WebDAVFs {
57    /// Create a new WebDAVFs instance
58    pub fn new(username: &str, password: &str, url: &str) -> WebDAVFs {
59        WebDAVFs {
60            client: Client::init(username, password),
61            url: url.to_string(),
62            wrkdir: String::from("/"),
63            connected: false,
64        }
65    }
66
67    /// Resolve query url
68    fn url(&self, path: &Path, force_dir: bool) -> String {
69        let mut p = self.url.clone();
70        p.push_str(&self.path(path).to_string_lossy());
71        if !p.ends_with('/') && (path.is_dir() || force_dir) {
72            p.push('/');
73        }
74        p
75    }
76
77    /// Resolve path
78    fn path(&self, path: &Path) -> PathBuf {
79        if path.is_absolute() {
80            path.to_path_buf()
81        } else {
82            Path::new(&self.wrkdir).join(path)
83        }
84    }
85}
86
87impl RemoteFs for WebDAVFs {
88    fn connect(&mut self) -> RemoteResult<Welcome> {
89        //self.list_dir(Path::new("/"))?;
90        self.connected = true;
91
92        Ok(Welcome::default())
93    }
94
95    fn disconnect(&mut self) -> RemoteResult<()> {
96        self.connected = false;
97        Ok(())
98    }
99
100    fn is_connected(&mut self) -> bool {
101        self.connected
102    }
103
104    fn pwd(&mut self) -> RemoteResult<PathBuf> {
105        Ok(PathBuf::from(&self.wrkdir))
106    }
107
108    fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
109        let new_dir = self.path(dir);
110        self.list_dir(&new_dir)?;
111
112        self.wrkdir = new_dir.to_string_lossy().to_string();
113        if !self.wrkdir.ends_with('/') {
114            self.wrkdir.push('/');
115        }
116        debug!("Changed directory to: {}", self.wrkdir);
117        Ok(new_dir)
118    }
119
120    fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
121        let url = self.url(path, true);
122        debug!("Listing directory: {}", url);
123        let response = self
124            .client
125            .list(&url, "1")
126            .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))?;
127
128        debug!("Parsing response");
129        match ResponseParser::from(response).files()? {
130            files if !files.is_empty() => {
131                // remove file at 0
132                let mut children = Vec::with_capacity(files.len());
133                for file in files.iter().skip(1) {
134                    children.push(file.clone());
135                }
136                Ok(children)
137            }
138            _ => Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory)),
139        }
140    }
141
142    fn stat(&mut self, path: &Path) -> RemoteResult<File> {
143        let url = self.url(path, false);
144        debug!("Listing directory: {}", url);
145        let response = self
146            .client
147            .list(&url, "1")
148            .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))?;
149
150        debug!("Parsing response");
151        match ResponseParser::from(response).files()? {
152            files if !files.is_empty() => Ok(files[0].clone()),
153            _ => Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory)),
154        }
155    }
156
157    fn setstat(&mut self, _path: &Path, _metadata: Metadata) -> RemoteResult<()> {
158        Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
159    }
160
161    fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
162        debug!("Checking if file exists: {}", path.display());
163        Ok(self.stat(path).is_ok())
164    }
165
166    fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
167        let url = self.url(path, false);
168        debug!("Removing file: {}", url);
169        let response = self
170            .client
171            .delete(&url)
172            .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))?;
173
174        ResponseParser::from(response).status()
175    }
176
177    fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
178        let url = self.url(path, true);
179        debug!("Removing directory: {}", url);
180        let response = self
181            .client
182            .delete(&url)
183            .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))?;
184
185        ResponseParser::from(response).status()
186    }
187
188    fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
189        self.remove_dir(path)
190    }
191
192    fn create_dir(&mut self, path: &Path, _mode: UnixPex) -> RemoteResult<()> {
193        if self.stat(path).is_ok() {
194            return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
195        }
196        let url = self.url(path, true);
197        // check if dir exists
198        debug!("Creating directory: {}", url);
199        let response = self
200            .client
201            .mkcol(&url)
202            .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))?;
203
204        ResponseParser::from(response).status()
205    }
206
207    fn symlink(&mut self, _path: &Path, _target: &Path) -> RemoteResult<()> {
208        Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
209    }
210
211    fn copy(&mut self, _src: &Path, _dest: &Path) -> RemoteResult<()> {
212        Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
213    }
214
215    fn mov(&mut self, src: &Path, dest: &Path) -> RemoteResult<()> {
216        let src_url = self.url(src, false);
217        let dest_url = self.url(dest, false);
218        debug!("Moving file: {} to {}", src_url, dest_url);
219
220        let response = self
221            .client
222            .mv(&src_url, &dest_url)
223            .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))?;
224
225        ResponseParser::from(response).status()
226    }
227
228    fn exec(&mut self, _cmd: &str) -> RemoteResult<(u32, String)> {
229        Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
230    }
231
232    fn append(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
233        Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
234    }
235
236    fn create(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
237        Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
238    }
239
240    fn open(&mut self, _path: &Path) -> RemoteResult<ReadStream> {
241        Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
242    }
243
244    fn create_file(
245        &mut self,
246        path: &Path,
247        _metadata: &Metadata,
248        mut reader: Box<dyn std::io::Read + Send>,
249    ) -> RemoteResult<u64> {
250        let url = self.url(path, false);
251        debug!("Creating file: {}", url);
252        let mut content = Vec::new();
253        reader
254            .read_to_end(&mut content)
255            .map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))?;
256        let size = content.len() as u64;
257        let response = self
258            .client
259            .put(content, &url)
260            .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))?;
261
262        ResponseParser::from(response).status()?;
263
264        Ok(size)
265    }
266
267    fn open_file(
268        &mut self,
269        src: &Path,
270        mut dest: Box<dyn std::io::Write + Send>,
271    ) -> RemoteResult<u64> {
272        let url = self.url(src, false);
273        debug!("Opening file: {}", url);
274        let mut response = self
275            .client
276            .get(&url)
277            .map_err(|e| RemoteError::new_ex(RemoteErrorType::ProtocolError, e))?;
278
279        // write to dest
280        let mut buf = vec![0; 1024];
281        let mut total_size = 0;
282        loop {
283            let n = response
284                .read(&mut buf)
285                .map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))?;
286            total_size += n as u64;
287            if n == 0 {
288                return Ok(total_size);
289            }
290            dest.write_all(&buf[..n])
291                .map_err(|e| RemoteError::new_ex(RemoteErrorType::IoError, e))?;
292        }
293    }
294}
295
296#[cfg(test)]
297mod test {
298
299    #[cfg(feature = "with-containers")]
300    use std::io::Cursor;
301
302    use pretty_assertions::assert_eq;
303    #[cfg(feature = "with-containers")]
304    use serial_test::serial;
305
306    use super::*;
307
308    #[test]
309    fn test_should_init_client() {
310        crate::mock::logger();
311        let client = WebDAVFs::new("user", "password", "http://localhost:3080");
312        assert_eq!(client.url, "http://localhost:3080");
313        assert_eq!(client.wrkdir, "/");
314    }
315
316    #[test]
317    fn test_should_get_url() {
318        let mut client = WebDAVFs::new("user", "password", "http://localhost:3080");
319        let path = Path::new("a.txt");
320        assert_eq!(client.url(path, false), "http://localhost:3080/a.txt");
321
322        let path = Path::new("/a.txt");
323        assert_eq!(client.url(path, false), "http://localhost:3080/a.txt");
324
325        let path = Path::new("/");
326        assert_eq!(client.url(path, false), "http://localhost:3080/");
327
328        client.wrkdir = "/test/".to_string();
329        let path = Path::new("a.txt");
330        assert_eq!(client.url(path, false), "http://localhost:3080/test/a.txt");
331
332        let path = Path::new("/a.txt");
333        assert_eq!(client.url(path, false), "http://localhost:3080/a.txt");
334
335        let path = Path::new("/gabibbo");
336        assert_eq!(client.url(path, true), "http://localhost:3080/gabibbo/");
337    }
338
339    #[test]
340    #[serial]
341    #[cfg(feature = "with-containers")]
342    fn should_not_append_to_file() {
343        crate::mock::logger();
344        let mut client = setup_client();
345        // Create file
346        let p = Path::new("a.txt");
347        // Append to file
348        let file_data = "Hello, world!\n";
349        let reader = Cursor::new(file_data.as_bytes());
350        assert!(client
351            .append_file(p, &Metadata::default(), Box::new(reader))
352            .is_err());
353        finalize_client(client);
354    }
355
356    #[test]
357    #[serial]
358    #[cfg(feature = "with-containers")]
359    fn should_not_change_directory() {
360        crate::mock::logger();
361        let mut client = setup_client();
362        assert!(client
363            .change_dir(Path::new("/tmp/sdfghjuireghiuergh/useghiyuwegh"))
364            .is_err());
365        finalize_client(client);
366    }
367
368    #[test]
369    #[serial]
370    #[cfg(feature = "with-containers")]
371    fn should_not_copy_file() {
372        crate::mock::logger();
373        let mut client = setup_client();
374        // Create file
375        let p = Path::new("a.txt");
376        let file_data = "test data\n";
377        let reader = Cursor::new(file_data.as_bytes());
378        let mut metadata = Metadata::default();
379        metadata.size = file_data.len() as u64;
380        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
381        assert!(client.copy(p, Path::new("aaa/bbbb/ccc/b.txt")).is_err());
382        finalize_client(client);
383    }
384
385    #[test]
386    #[serial]
387    #[cfg(feature = "with-containers")]
388    fn should_create_directory() {
389        crate::mock::logger();
390        let mut client = setup_client();
391        // create directory
392        assert!(client
393            .create_dir(Path::new("mydir"), UnixPex::from(0o755))
394            .is_ok());
395        finalize_client(client);
396    }
397
398    #[test]
399    #[serial]
400    #[cfg(feature = "with-containers")]
401    fn should_not_create_directory_cause_already_exists() {
402        crate::mock::logger();
403        let mut client = setup_client();
404        // create directory
405        assert!(client
406            .create_dir(Path::new("mydir/"), UnixPex::from(0o755))
407            .is_ok());
408        assert_eq!(
409            client
410                .create_dir(Path::new("mydir/"), UnixPex::from(0o755))
411                .unwrap_err()
412                .kind,
413            RemoteErrorType::DirectoryAlreadyExists
414        );
415        finalize_client(client);
416    }
417
418    #[test]
419    #[serial]
420    #[cfg(feature = "with-containers")]
421    fn should_not_create_directory() {
422        crate::mock::logger();
423        let mut client = setup_client();
424        // create directory
425        assert!(client
426            .create_dir(
427                Path::new("/tmp/werfgjwerughjwurih/iwerjghiwgui"),
428                UnixPex::from(0o755)
429            )
430            .is_err());
431        finalize_client(client);
432    }
433
434    #[test]
435    #[serial]
436    #[cfg(feature = "with-containers")]
437    fn should_create_file() {
438        crate::mock::logger();
439        let mut client = setup_client();
440        // Create file
441        let p = Path::new("a.txt");
442        let file_data = "test data\n";
443        let reader = Cursor::new(file_data.as_bytes());
444        let mut metadata = Metadata::default();
445        metadata.size = file_data.len() as u64;
446        assert_eq!(
447            client
448                .create_file(p, &metadata, Box::new(reader))
449                .ok()
450                .unwrap(),
451            10
452        );
453        // Verify size
454        assert_eq!(client.stat(p).ok().unwrap().metadata().size, 10);
455        finalize_client(client);
456    }
457
458    #[test]
459    #[serial]
460    #[cfg(feature = "with-containers")]
461    fn should_not_exec_command() {
462        crate::mock::logger();
463        let mut client = setup_client();
464        assert!(client.exec("echo 5").is_err());
465        finalize_client(client);
466    }
467
468    #[test]
469    #[serial]
470    #[cfg(feature = "with-containers")]
471    fn should_tell_whether_file_exists() {
472        crate::mock::logger();
473        let mut client = setup_client();
474        // Create file
475        let p = Path::new("a.txt");
476        let file_data = "test data\n";
477        let reader = Cursor::new(file_data.as_bytes());
478        let mut metadata = Metadata::default();
479        metadata.size = file_data.len() as u64;
480        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
481        // Verify size
482        assert_eq!(client.exists(p).ok().unwrap(), true);
483        assert_eq!(client.exists(Path::new("b.txt")).ok().unwrap(), false);
484        assert_eq!(
485            client.exists(Path::new("/tmp/ppppp/bhhrhu")).ok().unwrap(),
486            false
487        );
488        finalize_client(client);
489    }
490
491    #[test]
492    #[serial]
493    #[cfg(feature = "with-containers")]
494    fn should_list_dir() {
495        crate::mock::logger();
496        let mut client = setup_client();
497        // Create file
498        let wrkdir = client.pwd().ok().unwrap();
499        let p = Path::new("a.txt");
500        let file_data = "test data\n";
501        let reader = Cursor::new(file_data.as_bytes());
502        let mut metadata = Metadata::default();
503        metadata.size = file_data.len() as u64;
504        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
505        // Verify size
506        let file = client
507            .list_dir(wrkdir.as_path())
508            .ok()
509            .unwrap()
510            .get(0)
511            .unwrap()
512            .clone();
513        assert_eq!(file.name().as_str(), "a.txt");
514        let mut expected_path = wrkdir;
515        expected_path.push(p);
516        assert_eq!(file.path.as_path(), expected_path.as_path());
517        assert_eq!(file.extension().as_deref().unwrap(), "txt");
518        assert_eq!(file.metadata.size, 10);
519        assert_eq!(file.metadata.mode, None);
520        finalize_client(client);
521    }
522
523    #[test]
524    #[serial]
525    #[cfg(feature = "with-containers")]
526    fn should_move_file() {
527        crate::mock::logger();
528        let mut client = setup_client();
529        // Create file
530        let p = Path::new("a.txt");
531        let file_data = "test data\n";
532        let reader = Cursor::new(file_data.as_bytes());
533        let mut metadata = Metadata::default();
534        metadata.size = file_data.len() as u64;
535        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
536        let dest = Path::new("b.txt");
537        assert!(client.mov(p, dest).is_ok());
538        finalize_client(client);
539    }
540
541    #[test]
542    #[serial]
543    #[cfg(feature = "with-containers")]
544    fn should_open_file() {
545        crate::mock::logger();
546        let mut client = setup_client();
547        // Create file
548        let p = Path::new("a.txt");
549        let file_data = "test data\n";
550        let reader = Cursor::new(file_data.as_bytes());
551        let mut metadata = Metadata::default();
552        metadata.size = file_data.len() as u64;
553        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
554        // Verify size
555        let buffer: Box<dyn std::io::Write + Send> = Box::new(Vec::with_capacity(512));
556        assert_eq!(client.open_file(p, buffer).ok().unwrap(), 10);
557        finalize_client(client);
558    }
559
560    #[test]
561    #[serial]
562    #[cfg(feature = "with-containers")]
563    fn should_print_working_directory() {
564        crate::mock::logger();
565        let mut client = setup_client();
566        assert!(client.pwd().is_ok());
567        finalize_client(client);
568    }
569
570    #[test]
571    #[serial]
572    #[cfg(feature = "with-containers")]
573    fn should_remove_dir_all() {
574        crate::mock::logger();
575        let mut client = setup_client();
576        // Create dir
577        let mut dir_path = client.pwd().ok().unwrap();
578        dir_path.push(Path::new("test/"));
579        assert!(client
580            .create_dir(dir_path.as_path(), UnixPex::from(0o775))
581            .is_ok());
582        // Create file
583        let mut file_path = dir_path.clone();
584        file_path.push(Path::new("a.txt"));
585        let file_data = "test data\n";
586        let reader = Cursor::new(file_data.as_bytes());
587        let mut metadata = Metadata::default();
588        metadata.size = file_data.len() as u64;
589        assert!(client
590            .create_file(file_path.as_path(), &metadata, Box::new(reader))
591            .is_ok());
592        // Remove dir
593        assert!(client.remove_dir_all(dir_path.as_path()).is_ok());
594        finalize_client(client);
595    }
596
597    #[test]
598    #[serial]
599    #[cfg(feature = "with-containers")]
600    fn should_remove_dir() {
601        crate::mock::logger();
602        let mut client = setup_client();
603        // Create dir
604        let mut dir_path = client.pwd().ok().unwrap();
605        dir_path.push(Path::new("test/"));
606        assert!(client
607            .create_dir(dir_path.as_path(), UnixPex::from(0o775))
608            .is_ok());
609        assert!(client.remove_dir(dir_path.as_path()).is_ok());
610        finalize_client(client);
611    }
612
613    #[test]
614    #[serial]
615    #[cfg(feature = "with-containers")]
616    fn should_not_remove_dir() {
617        crate::mock::logger();
618        let mut client = setup_client();
619        // Remove dir
620        assert!(client.remove_dir(Path::new("test/")).is_err());
621        finalize_client(client);
622    }
623
624    #[test]
625    #[serial]
626    #[cfg(feature = "with-containers")]
627    fn should_remove_file() {
628        crate::mock::logger();
629        let mut client = setup_client();
630        // Create file
631        let p = Path::new("a.txt");
632        let file_data = "test data\n";
633        let reader = Cursor::new(file_data.as_bytes());
634        let mut metadata = Metadata::default();
635        metadata.size = file_data.len() as u64;
636        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
637        assert!(client.remove_file(p).is_ok());
638        finalize_client(client);
639    }
640
641    #[test]
642    #[serial]
643    #[cfg(feature = "with-containers")]
644    fn should_not_setstat_file() {
645        use std::time::SystemTime;
646
647        crate::mock::logger();
648        let mut client = setup_client();
649        // Create file
650        let p = Path::new("a.sh");
651        let file_data = "echo 5\n";
652        let reader = Cursor::new(file_data.as_bytes());
653        let mut metadata = Metadata::default();
654        metadata.size = file_data.len() as u64;
655        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
656        assert!(client
657            .setstat(
658                p,
659                Metadata {
660                    accessed: Some(SystemTime::UNIX_EPOCH),
661                    created: Some(SystemTime::UNIX_EPOCH),
662                    gid: Some(1000),
663                    file_type: remotefs::fs::FileType::File,
664                    mode: Some(UnixPex::from(0o755)),
665                    modified: Some(SystemTime::UNIX_EPOCH),
666                    size: 7,
667                    symlink: None,
668                    uid: Some(1000),
669                }
670            )
671            .is_err());
672        finalize_client(client);
673    }
674
675    #[test]
676    #[serial]
677    #[cfg(feature = "with-containers")]
678    fn should_stat_file() {
679        crate::mock::logger();
680        let mut client = setup_client();
681        // Create file
682        let p = Path::new("a.sh");
683        let file_data = "echo 5\n";
684        let reader = Cursor::new(file_data.as_bytes());
685        let mut metadata = Metadata::default();
686        metadata.size = file_data.len() as u64;
687        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
688        let entry = client.stat(p).ok().unwrap();
689        assert_eq!(entry.name(), "a.sh");
690        let mut expected_path = client.pwd().ok().unwrap();
691        expected_path.push("a.sh");
692        assert_eq!(entry.path(), expected_path.as_path());
693        let meta = entry.metadata();
694        assert_eq!(meta.size, 7);
695        finalize_client(client);
696    }
697
698    #[test]
699    #[serial]
700    #[cfg(feature = "with-containers")]
701    fn should_not_stat_file() {
702        crate::mock::logger();
703        let mut client = setup_client();
704        // Create file
705        let p = Path::new("a.sh");
706        assert!(client.stat(p).is_err());
707        finalize_client(client);
708    }
709
710    #[test]
711    #[serial]
712    #[cfg(feature = "with-containers")]
713    fn should_make_symlink() {
714        crate::mock::logger();
715        let mut client = setup_client();
716        // Create file
717        let p = Path::new("a.sh");
718        let file_data = "echo 5\n";
719        let reader = Cursor::new(file_data.as_bytes());
720        let mut metadata = Metadata::default();
721        metadata.size = file_data.len() as u64;
722        assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
723        let symlink = Path::new("b.sh");
724        assert!(client.symlink(symlink, p).is_err());
725        finalize_client(client);
726    }
727
728    #[cfg(feature = "with-containers")]
729    fn setup_client() -> WebDAVFs {
730        let mut client = WebDAVFs::new("alice", "secret1234", "http://localhost:3080");
731        assert!(client.connect().is_ok(), "connect");
732        // generate random string
733        let wrkdir = PathBuf::from(format!("/test-{}/", uuid::Uuid::new_v4()));
734        // mkdir
735        assert!(
736            client.create_dir(&wrkdir, UnixPex::from(0o755)).is_ok(),
737            "create tempdir"
738        );
739        // change dir
740        assert!(client.change_dir(&wrkdir).is_ok(), "change dir");
741        assert!(client.is_connected(), "connected");
742
743        client
744    }
745
746    #[cfg(feature = "with-containers")]
747    fn finalize_client(mut client: WebDAVFs) {
748        let wrkdir = client.pwd().unwrap();
749        // remove tempdir
750        assert!(client.remove_dir_all(&wrkdir).is_ok(), "remove tempdir");
751        assert!(client.disconnect().is_ok(), "disconnect");
752        assert!(!client.is_connected(), "disconnected");
753    }
754}