]> OzVa Git service - ozva-cloud/commitdiff
feat: implement more webdav methods (#13)
authorsigoden <sigoden@gmail.com>
Sat, 4 Jun 2022 11:08:18 +0000 (19:08 +0800)
committerGitHub <noreply@github.com>
Sat, 4 Jun 2022 11:08:18 +0000 (19:08 +0800)
Now you can mount the server as webdav driver on windows.

README.md
src/args.rs
src/server.rs

index 740ec536fb7dfdd9373157bea79c8ab0f21d1b58..a6d7ac98f13046512343be801b3370780c3c7f23 100644 (file)
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 [![CI](https://github.com/sigoden/duf/actions/workflows/ci.yaml/badge.svg)](https://github.com/sigoden/duf/actions/workflows/ci.yaml)
 [![Crates](https://img.shields.io/crates/v/duf.svg)](https://crates.io/crates/duf)
 
-Duf is a simple file server. Support static serve, search, upload, delete, webdav...
+Duf is a simple file server. Support static serve, search, upload, webdav...
 
 ![demo](https://user-images.githubusercontent.com/4012553/171526189-09afc2de-793f-4216-b3d5-31ea408d3610.png)
 
@@ -11,9 +11,8 @@ Duf is a simple file server. Support static serve, search, upload, delete, webda
 
 - Serve static files
 - Download folder as zip file
-- Search files
 - Upload files and folders (Drag & Drop)
-- Delete files
+- Search files
 - Basic authentication
 - Partial responses (Parallel/Resume download)
 - Support https
@@ -111,8 +110,6 @@ duf --tls-cert my.crt --tls-key my.key
 Download a file
 ```
 curl http://127.0.0.1:5000/some-file
-
-curl -o some-file2 http://127.0.0.1:5000/some-file
 ```
 
 Download a folder as zip file
index 2bcd05fe8a38773bdcdaf99dbbe083fda965ef69..3341276130229844211ae17d287f0a88d051d736 100644 (file)
@@ -113,7 +113,8 @@ pub struct Args {
     pub address: String,
     pub port: u16,
     pub path: PathBuf,
-    pub path_prefix: Option<String>,
+    pub path_prefix: String,
+    pub uri_prefix: String,
     pub auth: Option<String>,
     pub no_auth_access: bool,
     pub allow_upload: bool,
@@ -136,7 +137,13 @@ impl Args {
         let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
         let path_prefix = matches
             .value_of("path-prefix")
-            .map(|v| v.trim_matches('/').to_owned());
+            .map(|v| v.trim_matches('/').to_owned())
+            .unwrap_or_default();
+        let uri_prefix = if path_prefix.is_empty() {
+            "/".to_owned()
+        } else {
+            format!("/{}/", &path_prefix)
+        };
         let cors = matches.is_present("cors");
         let auth = matches.value_of("auth").map(|v| v.to_owned());
         let no_auth_access = matches.is_present("no-auth-access");
@@ -159,6 +166,7 @@ impl Args {
             port,
             path,
             path_prefix,
+            uri_prefix,
             auth,
             no_auth_access,
             cors,
index 95e53b7a54a7da74de021b2e19a4e917dd99a47c..5fe8360dc2aaaeabff82d5dfdd612201e4cc60ff 100644 (file)
@@ -89,7 +89,7 @@ pub async fn serve_https(args: Args) -> BoxResult<()> {
             }))
         }
     }));
-    print_listening(args.address.as_str(), args.port, true);
+    print_listening(args.address.as_str(), args.port, &args.uri_prefix, true);
     let graceful = server.with_graceful_shutdown(shutdown_signal());
     graceful.await?;
     Ok(())
@@ -108,7 +108,7 @@ pub async fn serve_http(args: Args) -> BoxResult<()> {
             }))
         }
     }));
-    print_listening(args.address.as_str(), args.port, false);
+    print_listening(args.address.as_str(), args.port, &args.uri_prefix, false);
     let graceful = server.with_graceful_shutdown(shutdown_signal());
     graceful.await?;
     Ok(())
@@ -172,11 +172,10 @@ impl InnerService {
 
         let query = req.uri().query().unwrap_or_default();
 
-        let meta = fs::metadata(path).await.ok();
-
-        let is_miss = meta.is_none();
-        let is_dir = meta.map(|v| v.is_dir()).unwrap_or_default();
-        let is_file = !is_miss && !is_dir;
+        let (is_miss, is_dir, is_file, size) = match fs::metadata(path).await.ok() {
+            Some(meta) => (false, meta.is_dir(), meta.is_file(), meta.len()),
+            None => (true, false, false, 0),
+        };
 
         let allow_upload = self.args.allow_upload;
         let allow_delete = self.args.allow_delete;
@@ -188,9 +187,10 @@ impl InnerService {
             return Ok(res);
         }
 
+        let headers = req.headers();
+
         match req.method() {
             &Method::GET => {
-                let headers = req.headers();
                 if is_dir {
                     if render_index || render_spa {
                         self.handle_render_index(path, headers, &mut res).await?;
@@ -215,7 +215,7 @@ impl InnerService {
                 self.handle_method_options(&mut res);
             }
             &Method::PUT => {
-                if !allow_upload || (!allow_delete && is_file) {
+                if !allow_upload || (!allow_delete && is_file && size > 0) {
                     status!(res, StatusCode::FORBIDDEN);
                 } else {
                     self.handle_upload(path, req, &mut res).await?;
@@ -230,22 +230,52 @@ impl InnerService {
                     status!(res, StatusCode::NOT_FOUND);
                 }
             }
+            &Method::HEAD => {
+                if is_miss {
+                    status!(res, StatusCode::NOT_FOUND);
+                } else {
+                    status!(res, StatusCode::OK);
+                }
+            }
             method => match method.as_str() {
                 "PROPFIND" => {
                     if is_dir {
-                        self.handle_propfind_dir(path, &mut res).await?;
+                        self.handle_propfind_dir(path, headers, &mut res).await?;
                     } else if is_file {
                         self.handle_propfind_file(path, &mut res).await?;
                     } else {
                         status!(res, StatusCode::NOT_FOUND);
                     }
                 }
+                "PROPPATCH" => {
+                    if is_file {
+                        self.handle_propfind_file(path, &mut res).await?;
+                    } else {
+                        status!(res, StatusCode::NOT_FOUND);
+                    }
+                }
                 "MKCOL" if allow_upload && is_miss => self.handle_mkcol(path, &mut res).await?,
                 "COPY" if allow_upload && !is_miss => {
-                    self.handle_copy(path, req.headers(), &mut res).await?
+                    self.handle_copy(path, headers, &mut res).await?
                 }
                 "MOVE" if allow_upload && allow_delete && !is_miss => {
-                    self.handle_move(path, req.headers(), &mut res).await?
+                    self.handle_move(path, headers, &mut res).await?
+                }
+                "LOCK" => {
+                    // Fake lock
+                    if is_file {
+                        self.handle_lock(req_path, &mut res).await?;
+                    } else {
+                        status!(res, StatusCode::NOT_FOUND);
+                    }
+                }
+                "UNLOCK" => {
+                    // Fake unlock
+                    if is_miss {
+                        status!(res, StatusCode::NOT_FOUND);
+                    } else {
+                        status!(res, StatusCode::OK);
+                    }
                 }
                 _ => {
                     status!(res, StatusCode::METHOD_NOT_ALLOWED);
@@ -292,7 +322,7 @@ impl InnerService {
     async fn handle_ls_dir(&self, path: &Path, exist: bool, res: &mut Response) -> BoxResult<()> {
         let mut paths = vec![];
         if exist {
-            paths = match self.list_dir(path, path, false).await {
+            paths = match self.list_dir(path, path).await {
                 Ok(paths) => paths,
                 Err(_) => {
                     status!(res, StatusCode::FORBIDDEN);
@@ -454,36 +484,46 @@ impl InnerService {
     }
 
     fn handle_method_options(&self, res: &mut Response) {
-        let allow_upload = self.args.allow_upload;
-        let allow_delete = self.args.allow_delete;
-        let mut methods = vec!["GET", "PROPFIND", "OPTIONS"];
-        if allow_upload {
-            methods.extend(["PUT", "COPY", "MKCOL"]);
-        }
-        if allow_delete {
-            methods.push("DELETE");
-        }
-        if allow_upload && allow_delete {
-            methods.push("COPY");
-        }
-        let value = methods.join(",").parse().unwrap();
-        res.headers_mut().insert("Allow", value);
+        res.headers_mut().insert(
+            "Allow",
+            "GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"
+                .parse()
+                .unwrap(),
+        );
         res.headers_mut().insert("DAV", "1".parse().unwrap());
 
         status!(res, StatusCode::NO_CONTENT);
     }
 
-    async fn handle_propfind_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
-        let paths = match self.list_dir(path, &self.args.path, true).await {
-            Ok(paths) => paths,
-            Err(_) => {
-                status!(res, StatusCode::FORBIDDEN);
-                return Ok(());
-            }
+    async fn handle_propfind_dir(
+        &self,
+        path: &Path,
+        headers: &HeaderMap<HeaderValue>,
+        res: &mut Response,
+    ) -> BoxResult<()> {
+        let depth: u32 = match headers.get("depth") {
+            Some(v) => match v.to_str().ok().and_then(|v| v.parse().ok()) {
+                Some(v) => v,
+                None => {
+                    status!(res, StatusCode::BAD_REQUEST);
+                    return Ok(());
+                }
+            },
+            None => 1,
         };
+        let mut paths = vec![self.to_pathitem(path, &self.args.path).await?.unwrap()];
+        if depth > 0 {
+            match self.list_dir(path, &self.args.path).await {
+                Ok(child) => paths.extend(child),
+                Err(_) => {
+                    status!(res, StatusCode::FORBIDDEN);
+                    return Ok(());
+                }
+            }
+        }
         let output = paths
             .iter()
-            .map(|v| v.xml(self.args.path_prefix.as_ref()))
+            .map(|v| v.to_dav_xml(self.args.uri_prefix.as_str()))
             .fold(String::new(), |mut acc, v| {
                 acc.push_str(&v);
                 acc
@@ -494,7 +534,7 @@ impl InnerService {
 
     async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
         if let Some(pathitem) = self.to_pathitem(path, &self.args.path).await? {
-            res_propfind(res, &pathitem.xml(self.args.path_prefix.as_ref()));
+            res_propfind(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
         } else {
             status!(res, StatusCode::NOT_FOUND);
         }
@@ -557,6 +597,27 @@ impl InnerService {
         Ok(())
     }
 
+    async fn handle_lock(&self, req_path: &str, res: &mut Response) -> BoxResult<()> {
+        let now = Utc::now().timestamp();
+        res.headers_mut().insert(
+            "content-type",
+            "application/xml; charset=utf-8".parse().unwrap(),
+        );
+        res.headers_mut()
+            .insert("lock-token", format!("<{}>", now).parse().unwrap());
+        *res.body_mut() = Body::from(format!(
+            r#"<?xml version="1.0" encoding="utf-8"?>
+<D:prop xmlns:D="DAV:"><D:lockdiscovery><D:activelock>
+       <D:locktype><D:write/></D:locktype>
+       <D:lockscope><D:exclusive/></D:lockscope>
+       <D:locktoken><D:href>{}</D:href></D:locktoken>
+       <D:lockroot><D:href>{}</D:href></D:lockroot>
+</D:activelock></D:lockdiscovery></D:prop>"#,
+            now, req_path
+        ));
+        Ok(())
+    }
+
     fn send_index(
         &self,
         path: &Path,
@@ -652,25 +713,15 @@ impl InnerService {
 
     fn strip_path_prefix<'a, P: AsRef<Path>>(&self, path: &'a P) -> Option<&'a Path> {
         let path = path.as_ref();
-        match self.args.path_prefix.as_deref() {
-            Some(prefix) => {
-                let prefix = prefix.trim_start_matches('/');
-                path.strip_prefix(prefix).ok()
-            }
-            None => Some(path),
+        if self.args.path_prefix.is_empty() {
+            Some(path)
+        } else {
+            path.strip_prefix(&self.args.path_prefix).ok()
         }
     }
 
-    async fn list_dir(
-        &self,
-        entry_path: &Path,
-        base_path: &Path,
-        include_entry: bool,
-    ) -> BoxResult<Vec<PathItem>> {
+    async fn list_dir(&self, entry_path: &Path, base_path: &Path) -> BoxResult<Vec<PathItem>> {
         let mut paths: Vec<PathItem> = vec![];
-        if include_entry {
-            paths.push(self.to_pathitem(entry_path, base_path).await?.unwrap())
-        }
         let mut rd = fs::read_dir(entry_path).await?;
         while let Ok(Some(entry)) = rd.next_entry().await {
             let entry_path = entry.path();
@@ -740,11 +791,7 @@ struct PathItem {
 }
 
 impl PathItem {
-    pub fn xml(&self, prefix: Option<&String>) -> String {
-        let prefix = match prefix {
-            Some(value) => format!("/{}/", value),
-            None => "/".to_owned(),
-        };
+    pub fn to_dav_xml(&self, prefix: &str) -> String {
         let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822();
         match self.path_type {
             PathType::Dir | PathType::SymlinkDir => format!(
@@ -755,9 +802,6 @@ impl PathItem {
 <D:displayname>{}</D:displayname>
 <D:getlastmodified>{}</D:getlastmodified>
 <D:resourcetype><D:collection/></D:resourcetype>
-<D:lockdiscovery/>
-<D:supportedlock>
-</D:supportedlock>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
@@ -773,9 +817,6 @@ impl PathItem {
 <D:getcontentlength>{}</D:getcontentlength>
 <D:getlastmodified>{}</D:getlastmodified>
 <D:resourcetype></D:resourcetype>
-<D:lockdiscovery/>
-<D:supportedlock>
-</D:supportedlock>
 </D:prop>
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
@@ -834,6 +875,10 @@ fn add_cors(res: &mut Response) {
 
 fn res_propfind(res: &mut Response, content: &str) {
     *res.status_mut() = StatusCode::MULTI_STATUS;
+    res.headers_mut().insert(
+        "content-type",
+        "application/xml; charset=utf-8".parse().unwrap(),
+    );
     *res.body_mut() = Body::from(format!(
         r#"<?xml version="1.0" encoding="utf-8" ?>
 <D:multistatus xmlns:D="DAV:">
@@ -910,15 +955,19 @@ fn to_content_range(range: &Range, complete_length: u64) -> Option<ContentRange>
     })
 }
 
-fn print_listening(address: &str, port: u16, tls: bool) {
+fn print_listening(address: &str, port: u16, prefix: &str, tls: bool) {
+    let prefix = prefix.trim_end_matches('/');
     let addrs = retrive_listening_addrs(address);
     let protocol = if tls { "https" } else { "http" };
     if addrs.len() == 1 {
-        eprintln!("Listening on {}://{}:{}", protocol, addrs[0], port);
+        eprintln!(
+            "Listening on {}://{}:{}{}",
+            protocol, addrs[0], port, prefix
+        );
     } else {
         eprintln!("Listening on:");
         for addr in addrs {
-            eprintln!("  {}://{}:{}", protocol, addr, port);
+            eprintln!("  {}://{}:{}{}", protocol, addr, port, prefix);
         }
         eprintln!();
     }