feat: implement more webdav methods (#13)
Now you can mount the server as webdav driver on windows.pull/17/head
parent
4605701366
commit
05155aa532
|
@ -3,7 +3,7 @@
|
||||||
[](https://github.com/sigoden/duf/actions/workflows/ci.yaml)
|
[](https://github.com/sigoden/duf/actions/workflows/ci.yaml)
|
||||||
[](https://crates.io/crates/duf)
|
[](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...
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -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
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
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…
Reference in New Issue