Browse Source

feat: implement more webdav methods (#13)

Now you can mount the server as webdav driver on windows.
pull/17/head
sigoden 3 years ago committed by GitHub
parent
commit
05155aa532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      README.md
  2. 12
      src/args.rs
  3. 179
      src/server.rs

7
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) [![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) [![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) ![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 - Serve static files
- Download folder as zip file - Download folder as zip file
- Search files
- Upload files and folders (Drag & Drop) - Upload files and folders (Drag & Drop)
- Delete files - Search files
- Basic authentication - Basic authentication
- Partial responses (Parallel/Resume download) - Partial responses (Parallel/Resume download)
- Support https - Support https
@ -111,8 +110,6 @@ duf --tls-cert my.crt --tls-key my.key
Download a file Download a file
``` ```
curl http://127.0.0.1:5000/some-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 Download a folder as zip file

12
src/args.rs

@ -113,7 +113,8 @@ pub struct Args {
pub address: String, pub address: String,
pub port: u16, pub port: u16,
pub path: PathBuf, pub path: PathBuf,
pub path_prefix: Option<String>, pub path_prefix: String,
pub uri_prefix: String,
pub auth: Option<String>, pub auth: Option<String>,
pub no_auth_access: bool, pub no_auth_access: bool,
pub allow_upload: 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 = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
let path_prefix = matches let path_prefix = matches
.value_of("path-prefix") .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 cors = matches.is_present("cors");
let auth = matches.value_of("auth").map(|v| v.to_owned()); let auth = matches.value_of("auth").map(|v| v.to_owned());
let no_auth_access = matches.is_present("no-auth-access"); let no_auth_access = matches.is_present("no-auth-access");
@ -159,6 +166,7 @@ impl Args {
port, port,
path, path,
path_prefix, path_prefix,
uri_prefix,
auth, auth,
no_auth_access, no_auth_access,
cors, cors,

179
src/server.rs

@ -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()); let graceful = server.with_graceful_shutdown(shutdown_signal());
graceful.await?; graceful.await?;
Ok(()) 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()); let graceful = server.with_graceful_shutdown(shutdown_signal());
graceful.await?; graceful.await?;
Ok(()) Ok(())
@ -172,11 +172,10 @@ impl InnerService {
let query = req.uri().query().unwrap_or_default(); let query = req.uri().query().unwrap_or_default();
let meta = fs::metadata(path).await.ok(); 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()),
let is_miss = meta.is_none(); None => (true, false, false, 0),
let is_dir = meta.map(|v| v.is_dir()).unwrap_or_default(); };
let is_file = !is_miss && !is_dir;
let allow_upload = self.args.allow_upload; let allow_upload = self.args.allow_upload;
let allow_delete = self.args.allow_delete; let allow_delete = self.args.allow_delete;
@ -188,9 +187,10 @@ impl InnerService {
return Ok(res); return Ok(res);
} }
let headers = req.headers();
match req.method() { match req.method() {
&Method::GET => { &Method::GET => {
let headers = req.headers();
if is_dir { if is_dir {
if render_index || render_spa { if render_index || render_spa {
self.handle_render_index(path, headers, &mut res).await?; self.handle_render_index(path, headers, &mut res).await?;
@ -215,7 +215,7 @@ impl InnerService {
self.handle_method_options(&mut res); self.handle_method_options(&mut res);
} }
&Method::PUT => { &Method::PUT => {
if !allow_upload || (!allow_delete && is_file) { if !allow_upload || (!allow_delete && is_file && size > 0) {
status!(res, StatusCode::FORBIDDEN); status!(res, StatusCode::FORBIDDEN);
} else { } else {
self.handle_upload(path, req, &mut res).await?; self.handle_upload(path, req, &mut res).await?;
@ -230,22 +230,52 @@ impl InnerService {
status!(res, StatusCode::NOT_FOUND); 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() { method => match method.as_str() {
"PROPFIND" => { "PROPFIND" => {
if is_dir { if is_dir {
self.handle_propfind_dir(path, &mut res).await?; self.handle_propfind_dir(path, headers, &mut res).await?;
} else if is_file { } else if is_file {
self.handle_propfind_file(path, &mut res).await?; self.handle_propfind_file(path, &mut res).await?;
} else { } else {
status!(res, StatusCode::NOT_FOUND); 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?, "MKCOL" if allow_upload && is_miss => self.handle_mkcol(path, &mut res).await?,
"COPY" if allow_upload && !is_miss => { "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 => { "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); 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<()> { async fn handle_ls_dir(&self, path: &Path, exist: bool, res: &mut Response) -> BoxResult<()> {
let mut paths = vec![]; let mut paths = vec![];
if exist { if exist {
paths = match self.list_dir(path, path, false).await { paths = match self.list_dir(path, path).await {
Ok(paths) => paths, Ok(paths) => paths,
Err(_) => { Err(_) => {
status!(res, StatusCode::FORBIDDEN); status!(res, StatusCode::FORBIDDEN);
@ -454,36 +484,46 @@ impl InnerService {
} }
fn handle_method_options(&self, res: &mut Response) { fn handle_method_options(&self, res: &mut Response) {
let allow_upload = self.args.allow_upload; res.headers_mut().insert(
let allow_delete = self.args.allow_delete; "Allow",
let mut methods = vec!["GET", "PROPFIND", "OPTIONS"]; "GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"
if allow_upload { .parse()
methods.extend(["PUT", "COPY", "MKCOL"]); .unwrap(),
} );
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("DAV", "1".parse().unwrap()); res.headers_mut().insert("DAV", "1".parse().unwrap());
status!(res, StatusCode::NO_CONTENT); status!(res, StatusCode::NO_CONTENT);
} }
async fn handle_propfind_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> { async fn handle_propfind_dir(
let paths = match self.list_dir(path, &self.args.path, true).await { &self,
Ok(paths) => paths, path: &Path,
Err(_) => { headers: &HeaderMap<HeaderValue>,
status!(res, StatusCode::FORBIDDEN); res: &mut Response,
return Ok(()); ) -> 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 let output = paths
.iter() .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| { .fold(String::new(), |mut acc, v| {
acc.push_str(&v); acc.push_str(&v);
acc acc
@ -494,7 +534,7 @@ impl InnerService {
async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> BoxResult<()> { async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
if let Some(pathitem) = self.to_pathitem(path, &self.args.path).await? { 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 { } else {
status!(res, StatusCode::NOT_FOUND); status!(res, StatusCode::NOT_FOUND);
} }
@ -557,6 +597,27 @@ impl InnerService {
Ok(()) 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( fn send_index(
&self, &self,
path: &Path, path: &Path,
@ -652,25 +713,15 @@ impl InnerService {
fn strip_path_prefix<'a, P: AsRef<Path>>(&self, path: &'a P) -> Option<&'a Path> { fn strip_path_prefix<'a, P: AsRef<Path>>(&self, path: &'a P) -> Option<&'a Path> {
let path = path.as_ref(); let path = path.as_ref();
match self.args.path_prefix.as_deref() { if self.args.path_prefix.is_empty() {
Some(prefix) => { Some(path)
let prefix = prefix.trim_start_matches('/'); } else {
path.strip_prefix(prefix).ok() path.strip_prefix(&self.args.path_prefix).ok()
}
None => Some(path),
} }
} }
async fn list_dir( async fn list_dir(&self, entry_path: &Path, base_path: &Path) -> BoxResult<Vec<PathItem>> {
&self,
entry_path: &Path,
base_path: &Path,
include_entry: bool,
) -> BoxResult<Vec<PathItem>> {
let mut paths: Vec<PathItem> = vec![]; 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?; let mut rd = fs::read_dir(entry_path).await?;
while let Ok(Some(entry)) = rd.next_entry().await { while let Ok(Some(entry)) = rd.next_entry().await {
let entry_path = entry.path(); let entry_path = entry.path();
@ -740,11 +791,7 @@ struct PathItem {
} }
impl PathItem { impl PathItem {
pub fn xml(&self, prefix: Option<&String>) -> String { pub fn to_dav_xml(&self, prefix: &str) -> String {
let prefix = match prefix {
Some(value) => format!("/{}/", value),
None => "/".to_owned(),
};
let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822(); let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822();
match self.path_type { match self.path_type {
PathType::Dir | PathType::SymlinkDir => format!( PathType::Dir | PathType::SymlinkDir => format!(
@ -755,9 +802,6 @@ impl PathItem {
<D:displayname>{}</D:displayname> <D:displayname>{}</D:displayname>
<D:getlastmodified>{}</D:getlastmodified> <D:getlastmodified>{}</D:getlastmodified>
<D:resourcetype><D:collection/></D:resourcetype> <D:resourcetype><D:collection/></D:resourcetype>
<D:lockdiscovery/>
<D:supportedlock>
</D:supportedlock>
</D:prop> </D:prop>
<D:status>HTTP/1.1 200 OK</D:status> <D:status>HTTP/1.1 200 OK</D:status>
</D:propstat> </D:propstat>
@ -773,9 +817,6 @@ impl PathItem {
<D:getcontentlength>{}</D:getcontentlength> <D:getcontentlength>{}</D:getcontentlength>
<D:getlastmodified>{}</D:getlastmodified> <D:getlastmodified>{}</D:getlastmodified>
<D:resourcetype></D:resourcetype> <D:resourcetype></D:resourcetype>
<D:lockdiscovery/>
<D:supportedlock>
</D:supportedlock>
</D:prop> </D:prop>
<D:status>HTTP/1.1 200 OK</D:status> <D:status>HTTP/1.1 200 OK</D:status>
</D:propstat> </D:propstat>
@ -834,6 +875,10 @@ fn add_cors(res: &mut Response) {
fn res_propfind(res: &mut Response, content: &str) { fn res_propfind(res: &mut Response, content: &str) {
*res.status_mut() = StatusCode::MULTI_STATUS; *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!( *res.body_mut() = Body::from(format!(
r#"<?xml version="1.0" encoding="utf-8" ?> r#"<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:"> <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 addrs = retrive_listening_addrs(address);
let protocol = if tls { "https" } else { "http" }; let protocol = if tls { "https" } else { "http" };
if addrs.len() == 1 { if addrs.len() == 1 {
eprintln!("Listening on {}://{}:{}", protocol, addrs[0], port); eprintln!(
"Listening on {}://{}:{}{}",
protocol, addrs[0], port, prefix
);
} else { } else {
eprintln!("Listening on:"); eprintln!("Listening on:");
for addr in addrs { for addr in addrs {
eprintln!(" {}://{}:{}", protocol, addr, port); eprintln!(" {}://{}:{}{}", protocol, addr, port, prefix);
} }
eprintln!(); eprintln!();
} }

Loading…
Cancel
Save