corroded_dav_cli/
lib.rs

1//! # Corroded DAV library and command line client.
2//!
3//! (c) 2025 Andreas Feldner
4//!
5pub mod catalogue;
6pub mod filter;
7pub mod davclient;
8
9use std::env;
10use std::fs::File;
11use std::io::{BufReader, BufWriter, Error as IoError, ErrorKind, Write};
12use std::path::Path;
13use netrc::Netrc;
14pub use catalogue::CatalogueInfo;
15use filter::FilterCriteria;
16pub use davclient::{ClientExt, DavCtrlError};
17use url::Url;
18use http::StatusCode;
19
20/// The DavController object that provides functions
21/// to perform WebDAV operations.
22///
23/// It does not keep a dialgoue or session context with the
24/// caller, so is quite stateless apart from configuration.
25///
26/// It is currently build around rustydav, but is in
27/// conflict with its design decision to tie one
28/// username/password pair to a client instance.
29pub struct DavController {
30    client_ext: ClientExt
31}
32
33impl DavController {
34    /// creates a new DavController initialized with the given Netrc configuration object.
35    pub fn new (rc: Netrc) -> Self {
36        Self{client_ext: ClientExt::new(rc)}
37    }
38    
39    /// puts one file given by its path to the given WebDAV URL.
40    ///
41    /// # Errors
42    ///
43    /// DavCtrlError::InvalidSource if the given path cannot be found or read.
44    ///
45    fn _put_one (&self, file_path: &Path, target_url: &Url) -> Result<StatusCode, DavCtrlError> {
46        if file_path.is_file() {
47            let file = File::open(file_path)?;
48            self.client_ext.write(file, target_url)
49        } else {
50            Err(DavCtrlError::InvalidSource(format!("Not an existing file: {}", file_path.display())))
51        }
52    }
53    
54    /// Puts all of the files identified by the provided paths to the given WebDAV URL.
55    /// The result of each put operation is returned as a vector of equal length and sorting,
56    /// containing either a successful status code or a DavCtrlError.
57    pub fn put (&self, file_paths: &Vec<&Path>, target_base: &Url) -> Vec<Result<StatusCode, DavCtrlError>> {
58        let mut retvec = Vec::new();
59        for file_path in file_paths {
60            if !target_base.path().ends_with('/') {
61                // non-directory URL is acceptable only for uploading one file
62                if file_paths.len() == 1 {
63                    // in this case, do _not_ replace the last path segment with the file's name
64                    retvec.push(self._put_one(file_path, target_base));
65                } else {
66                    retvec.push(Err(DavCtrlError::InvalidDestination(
67                        format!("Given target URL {target_base} is not a directory and cannot receive multiple files")
68                    )));
69                }
70            } else if let Some(filename) = file_path.file_name() {
71                match target_base.join(&filename.to_string_lossy()) {
72                    Ok(target_url) => {
73                        retvec.push(self._put_one( file_path, &target_url));
74                    },
75                    Err(error) => {
76                        retvec.push(Err(DavCtrlError::from(error)));
77                    }
78                }
79            } else {
80                retvec.push(
81                    Err(DavCtrlError::InvalidSource(
82                        format!("Source path '{}' does not end with a file name", file_path.display())))
83                );
84            }
85        }
86        retvec
87    }
88    
89    
90    /// Gets one file given by its WebDAV URL to the directory by the given path.
91    ///
92    /// # Errors
93    ///
94    /// DavCtrlError::InvalidDestination if the given path cannot be found or is not a directory.
95    ///
96    fn _get_one(&self, source: &Url, target_dir: &Path) -> Result<StatusCode, DavCtrlError> {
97        if target_dir.is_dir() {
98            let filename = source.path_segments().
99                    and_then(|paths| paths.last()).
100                    ok_or_else(|| DavCtrlError::InvalidSource(format!("Source URL '{}' contains no filename", source)))?;
101            let file = std::fs::File::create(target_dir.join(filename))?;
102            let mut buffer = BufWriter::new(file);
103            let status_code = self.client_ext.read(source, &mut buffer)?;
104            buffer.flush()?;
105            Ok(status_code)
106        } else {
107            Err(DavCtrlError::InvalidDestination(format!("Destination '{}' is not a directory", target_dir.display())))
108        }
109    }
110    
111    /// Gets all of the files identified by the provided WebDAV URLs to the given directory.
112    /// The result of each get operation is returned as a vector of equal length and sorting,
113    /// containing either a successful status code or a DavCtrlError.
114    pub fn get (&self, sources: &Vec<&Url>, target_dir: &Path) -> Vec<Result<StatusCode, DavCtrlError>> {
115        let mut retvec = Vec::new();
116        for source in sources {
117            retvec.push(self._get_one(source, target_dir));
118        }
119        retvec
120    }
121    
122    /// Reads the requested part of a file identified by a WebDAV URL to a given file.
123    pub fn get_slice(&self, source: &Url, offset: u64, size: u32, target_file: &Path) -> Result<StatusCode,DavCtrlError> {
124        if target_file.is_file() {
125            let file = std::fs::File::create(target_file)?;
126            let mut buffer = BufWriter::new(file);
127            let status_code = self.client_ext.read_slice(source, offset, size, &mut buffer)?;
128            buffer.flush()?;
129            Ok(status_code)
130        } else {
131            Err(DavCtrlError::InvalidDestination(format!("Destination '{}' is not a file", target_file.display())))
132        }
133        
134    }
135    
136    /// List the objects contained in the given WebDAV URL.
137    pub fn ls (&self, url_to_list: &Url, filter: &FilterCriteria) -> Result<Vec<CatalogueInfo>, DavCtrlError> {
138        self.client_ext.list(url_to_list, filter)
139    }
140    
141    /// Delete the object identified by the given WebDAV URL.
142    pub fn delete (&self, url_to_delete: &Url) -> Result<StatusCode, DavCtrlError> {
143        self.client_ext.delete(url_to_delete)
144    }
145    
146    /// Move a file or collection
147    pub fn mv (&self, from: &Url, to: &Url) -> Result<StatusCode, DavCtrlError> {
148        self.client_ext.mv(from, to)
149    }
150    
151    /// Create a collection
152    pub fn mkdir (&self, collection_url: &Url) -> Result<StatusCode, DavCtrlError> {
153        self.client_ext.mkdir (collection_url)
154    }
155    
156    /// Provide username and password to use for future WebDAV accesses. To
157    /// provide different credentials for defined DAV servers, use a netrc object
158    /// instead.
159    pub fn set_default_credentials(&mut self, username: String, password: String) {
160        self.client_ext.netrc.default = Some(netrc::Machine { 
161            login: username, 
162            password: Some(password), 
163            account: None, 
164            port: None 
165        });
166    }
167}
168
169/// Reads the contents of a .netrc file from its conventional location ~/.netrc
170///
171/// # Errors
172/// 
173/// IoError if the home directory cannot be found, or does not contain a readable .netrc file.
174pub fn read_netrc() -> Result<Netrc, IoError> {
175    #[allow(deprecated)]
176    // honestly, I don't care where you have to place .netrc if you run this on cygwin under Windows
177    let home = env::home_dir().ok_or(IoError::from(ErrorKind::NotFound))?;
178    let path = home.join(".netrc");
179    if path.is_file() {
180        let netrc = File::open(path)?;
181        Netrc::parse(BufReader::new(netrc)).map_err(|_e| IoError::from(ErrorKind::InvalidInput))
182    } else {
183        Err(IoError::from(ErrorKind::NotFound))
184    }
185}
186
187
188#[cfg(test)]
189mod tests;