1#![crate_name = "remotefs_webdav"]
2#![crate_type = "lib"]
3
4#![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
48pub struct WebDAVFs {
50 client: Client,
51 url: String,
52 wrkdir: String,
53 connected: bool,
54}
55
56impl WebDAVFs {
57 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 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 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.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 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 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 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 let p = Path::new("a.txt");
347 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let wrkdir = PathBuf::from(format!("/test-{}/", uuid::Uuid::new_v4()));
734 assert!(
736 client.create_dir(&wrkdir, UnixPex::from(0o755)).is_ok(),
737 "create tempdir"
738 );
739 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 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}