diff --git a/README.md b/README.md
index 78d0d24..a57f9ca 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ Duf is a fully functional file server.
- Upload zip file then unzip
- Partial responses (Parallel/Resume download)
- Support https/tls
+- Support webdav
- Easy to use with curl
## Install
diff --git a/assets/index.js b/assets/index.js
index f220993..a9d9498 100644
--- a/assets/index.js
+++ b/assets/index.js
@@ -1,3 +1,14 @@
+/**
+ * @typedef {object} PathItem
+ * @property {"Dir"|"SymlinkDir"|"File"|"SymlinkFile"} path_type
+ * @property {boolean} is_symlink
+ * @property {string} base_name
+ * @property {string} name
+ * @property {number} mtime
+ * @property {number} size
+ */
+
+
/**
* @type Element
*/
@@ -8,9 +19,21 @@ let $pathsTable, $pathsTableBody, $uploadersTable;
let baseDir;
class Uploader {
+ /**
+ * @type number
+ */
idx;
+ /**
+ * @type File
+ */
file;
+ /**
+ * @type string
+ */
name;
+ /**
+ * @type Element
+ */
$uploadStatus;
static globalIdx = 0;
constructor(file, dirs) {
@@ -40,7 +63,7 @@ class Uploader {
ajax.upload.addEventListener("progress", e => this.progress(e), false);
ajax.addEventListener("readystatechange", () => {
if(ajax.readyState === 4) {
- if (ajax.status == 200) {
+ if (ajax.status >= 200 && ajax.status < 300) {
this.complete();
} else {
this.fail();
@@ -67,6 +90,10 @@ class Uploader {
}
}
+/**
+ * Add breadcumb
+ * @param {string} value
+ */
function addBreadcrumb(value) {
const $breadcrumb = document.querySelector(".breadcrumb");
const parts = value.split("/").filter(v => !!v);
@@ -89,6 +116,11 @@ function addBreadcrumb(value) {
}
}
+/**
+ * Add pathitem
+ * @param {PathItem} file
+ * @param {number} index
+ */
function addPath(file, index) {
const url = getUrl(file.name)
let actionDelete = "";
@@ -123,7 +155,7 @@ function addPath(file, index) {
$pathsTableBody.insertAdjacentHTML("beforeend", `
- ${getSvg(file.path_type)}
+ ${getSvg(file)}
${file.name}
|
${formatMtime(file.mtime)} |
@@ -132,6 +164,11 @@ function addPath(file, index) {
`)
}
+/**
+ * Delete pathitem
+ * @param {number} index
+ * @returns
+ */
async function deletePath(index) {
const file = DATA.paths[index];
if (!file) return;
@@ -142,7 +179,7 @@ async function deletePath(index) {
const res = await fetch(getUrl(file.name), {
method: "DELETE",
});
- if (res.status === 200) {
+ if (res.status >= 200 && res.status < 300) {
document.getElementById(`addPath${index}`).remove();
DATA.paths[index] = null;
if (!DATA.paths.find(v => !!v)) {
@@ -201,8 +238,13 @@ function getUrl(name) {
return url;
}
-function getSvg(path_type) {
- switch (path_type) {
+/**
+ * Get svg icon
+ * @param {PathItem} file
+ * @returns
+ */
+function getSvg(file) {
+ switch (file.path_type) {
case "Dir":
return ``;
case "File":
diff --git a/src/args.rs b/src/args.rs
index c5e510d..2bcd05f 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -134,7 +134,9 @@ impl Args {
let address = matches.value_of("address").unwrap_or_default().to_owned();
let port = matches.value_of_t::("port")?;
let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
- let path_prefix = matches.value_of("path-prefix").map(|v| v.to_owned());
+ let path_prefix = matches
+ .value_of("path-prefix")
+ .map(|v| v.trim_matches('/').to_owned());
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");
diff --git a/src/server.rs b/src/server.rs
index d1f38e7..f883f54 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -4,7 +4,7 @@ use async_walkdir::WalkDir;
use async_zip::read::seek::ZipFileReader;
use async_zip::write::{EntryOptions, ZipFileWriter};
use async_zip::Compression;
-use chrono::Local;
+use chrono::{Local, TimeZone, Utc};
use futures::stream::StreamExt;
use futures::TryStreamExt;
use get_if_addrs::get_if_addrs;
@@ -18,7 +18,7 @@ use hyper::header::{
WWW_AUTHENTICATE,
};
use hyper::service::{make_service_fn, service_fn};
-use hyper::{Body, Method, StatusCode};
+use hyper::{Body, Method, StatusCode, Uri};
use percent_encoding::percent_decode;
use rustls::ServerConfig;
use serde::Serialize;
@@ -189,8 +189,8 @@ impl InnerService {
return Ok(res);
}
- match *req.method() {
- Method::GET => {
+ match req.method() {
+ &Method::GET => {
let headers = req.headers();
if is_dir {
if render_index || render_spa {
@@ -212,28 +212,46 @@ impl InnerService {
status!(res, StatusCode::NOT_FOUND);
}
}
- Method::OPTIONS => {
- status!(res, StatusCode::NO_CONTENT);
+ &Method::OPTIONS => {
+ self.handle_method_options(&mut res);
}
- Method::PUT => {
+ &Method::PUT => {
if !allow_upload || (!allow_delete && is_file) {
status!(res, StatusCode::FORBIDDEN);
} else {
self.handle_upload(path, req, &mut res).await?;
}
}
- Method::DELETE => {
+ &Method::DELETE => {
if !allow_delete {
status!(res, StatusCode::FORBIDDEN);
} else if !is_miss {
- self.handle_delete(path, is_dir).await?
+ self.handle_delete(path, is_dir, &mut res).await?
} else {
status!(res, StatusCode::NOT_FOUND);
}
}
- _ => {
- status!(res, StatusCode::METHOD_NOT_ALLOWED);
- }
+ method => match method.as_str() {
+ "PROPFIND" => {
+ if is_dir {
+ self.handle_propfind_dir(path, &mut res).await?;
+ } else 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?
+ }
+ "MOVE" if allow_upload && allow_delete && !is_miss => {
+ self.handle_move(path, req.headers(), &mut res).await?
+ }
+ _ => {
+ status!(res, StatusCode::METHOD_NOT_ALLOWED);
+ }
+ },
}
Ok(res)
}
@@ -244,20 +262,7 @@ impl InnerService {
mut req: Request,
res: &mut Response,
) -> BoxResult<()> {
- let ensure_parent = match path.parent() {
- Some(parent) => match fs::metadata(parent).await {
- Ok(meta) => meta.is_dir(),
- Err(_) => {
- fs::create_dir_all(parent).await?;
- true
- }
- },
- None => false,
- };
- if !ensure_parent {
- status!(res, StatusCode::FORBIDDEN);
- return Ok(());
- }
+ ensure_path_parent(path).await?;
let mut file = fs::File::create(&path).await?;
@@ -280,34 +285,31 @@ impl InnerService {
fs::remove_file(&path).await?;
}
+ status!(res, StatusCode::CREATED);
Ok(())
}
- async fn handle_delete(&self, path: &Path, is_dir: bool) -> BoxResult<()> {
+ async fn handle_delete(&self, path: &Path, is_dir: bool, res: &mut Response) -> BoxResult<()> {
match is_dir {
true => fs::remove_dir_all(path).await?,
false => fs::remove_file(path).await?,
}
+
+ status!(res, StatusCode::NO_CONTENT);
Ok(())
}
async fn handle_ls_dir(&self, path: &Path, exist: bool, res: &mut Response) -> BoxResult<()> {
- let mut paths: Vec = vec![];
+ let mut paths = vec![];
if exist {
- let mut rd = match fs::read_dir(path).await {
- Ok(rd) => rd,
+ paths = match self.list_dir(path, path, false).await {
+ Ok(paths) => paths,
Err(_) => {
status!(res, StatusCode::FORBIDDEN);
return Ok(());
}
- };
- while let Some(entry) = rd.next_entry().await? {
- let entry_path = entry.path();
- if let Ok(Some(item)) = self.to_pathitem(entry_path, path.to_path_buf()).await {
- paths.push(item);
- }
}
- }
+ };
self.send_index(path, paths, res)
}
@@ -461,6 +463,110 @@ impl InnerService {
Ok(())
}
+ 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("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(());
+ }
+ };
+ let output = paths
+ .iter()
+ .map(|v| v.xml(self.args.path_prefix.as_ref()))
+ .fold(String::new(), |mut acc, v| {
+ acc.push_str(&v);
+ acc
+ });
+ res_propfind(res, &output);
+ Ok(())
+ }
+
+ 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()));
+ } else {
+ status!(res, StatusCode::NOT_FOUND);
+ }
+ Ok(())
+ }
+
+ async fn handle_mkcol(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
+ fs::create_dir_all(path).await?;
+ status!(res, StatusCode::CREATED);
+ Ok(())
+ }
+
+ async fn handle_copy(
+ &self,
+ path: &Path,
+ headers: &HeaderMap,
+ res: &mut Response,
+ ) -> BoxResult<()> {
+ let dest = match self.extract_dest(headers) {
+ Some(dest) => dest,
+ None => {
+ status!(res, StatusCode::BAD_REQUEST);
+ return Ok(());
+ }
+ };
+
+ let meta = fs::symlink_metadata(path).await?;
+ if meta.is_dir() {
+ status!(res, StatusCode::BAD_REQUEST);
+ return Ok(());
+ }
+
+ ensure_path_parent(&dest).await?;
+
+ fs::copy(path, &dest).await?;
+
+ status!(res, StatusCode::NO_CONTENT);
+ Ok(())
+ }
+
+ async fn handle_move(
+ &self,
+ path: &Path,
+ headers: &HeaderMap,
+ res: &mut Response,
+ ) -> BoxResult<()> {
+ let dest = match self.extract_dest(headers) {
+ Some(dest) => dest,
+ None => {
+ status!(res, StatusCode::BAD_REQUEST);
+ return Ok(());
+ }
+ };
+
+ ensure_path_parent(&dest).await?;
+
+ fs::rename(path, &dest).await?;
+
+ status!(res, StatusCode::NO_CONTENT);
+ Ok(())
+ }
+
fn send_index(
&self,
path: &Path,
@@ -547,11 +653,7 @@ impl InnerService {
if !self.args.allow_delete && fs::metadata(&entry_path).await.is_ok() {
continue;
}
- if let Some(parent) = entry_path.parent() {
- if fs::symlink_metadata(parent).await.is_err() {
- fs::create_dir_all(&parent).await?;
- }
- }
+ ensure_path_parent(&entry_path).await?;
let mut outfile = fs::File::create(&entry_path).await?;
let mut reader = zip.entry_reader(i).await?;
io::copy(&mut reader, &mut outfile).await?;
@@ -560,6 +662,12 @@ impl InnerService {
Ok(())
}
+ fn extract_dest(&self, headers: &HeaderMap) -> Option {
+ let dest = headers.get("Destination")?.to_str().ok()?;
+ let uri: Uri = dest.parse().ok()?;
+ self.extract_path(uri.path())
+ }
+
fn extract_path(&self, path: &str) -> Option {
let decoded_path = percent_decode(path[1..].as_bytes()).decode_utf8().ok()?;
let slashes_switched = if cfg!(windows) {
@@ -585,6 +693,26 @@ impl InnerService {
}
}
+ async fn list_dir(
+ &self,
+ entry_path: &Path,
+ base_path: &Path,
+ include_entry: bool,
+ ) -> BoxResult> {
+ let mut paths: Vec = 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();
+ if let Ok(Some(item)) = self.to_pathitem(entry_path.as_path(), base_path).await {
+ paths.push(item);
+ }
+ }
+ Ok(paths)
+ }
+
async fn to_pathitem>(
&self,
path: P,
@@ -610,9 +738,15 @@ impl InnerService {
PathType::Dir | PathType::SymlinkDir => None,
PathType::File | PathType::SymlinkFile => Some(meta.len()),
};
+ let base_name = rel_path
+ .file_name()
+ .and_then(|v| v.to_str())
+ .unwrap_or("/")
+ .to_owned();
let name = normalize_path(rel_path);
Ok(Some(PathItem {
path_type,
+ base_name,
name,
mtime,
size,
@@ -620,7 +754,7 @@ impl InnerService {
}
}
-#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Debug, Serialize)]
struct IndexData {
breadcrumb: String,
paths: Vec,
@@ -631,11 +765,63 @@ struct IndexData {
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
struct PathItem {
path_type: PathType,
+ base_name: String,
name: String,
mtime: u64,
size: Option,
}
+impl PathItem {
+ pub fn xml(&self, prefix: Option<&String>) -> String {
+ let prefix = match prefix {
+ Some(value) => format!("/{}/", value),
+ None => "/".to_owned(),
+ };
+ let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822();
+ match self.path_type {
+ PathType::Dir | PathType::SymlinkDir => format!(
+ r#"
+{}{}
+
+
+{}
+{}
+
+
+
+
+
+HTTP/1.1 200 OK
+
+"#,
+ prefix, self.name, self.base_name, mtime
+ ),
+ PathType::File | PathType::SymlinkFile => format!(
+ r#"
+{}{}
+
+
+{}
+{}
+{}
+
+
+
+
+
+HTTP/1.1 200 OK
+
+"#,
+ prefix,
+ self.name,
+ self.base_name,
+ self.size.unwrap_or_default(),
+ mtime
+ ),
+ }
+ }
+}
+
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
enum PathType {
Dir,
@@ -659,6 +845,15 @@ fn normalize_path>(path: P) -> String {
}
}
+async fn ensure_path_parent(path: &Path) -> BoxResult<()> {
+ if let Some(parent) = path.parent() {
+ if fs::symlink_metadata(parent).await.is_err() {
+ fs::create_dir_all(&parent).await?;
+ }
+ }
+ Ok(())
+}
+
fn add_cors(res: &mut Response) {
res.headers_mut()
.typed_insert(AccessControlAllowOrigin::ANY);
@@ -669,6 +864,17 @@ fn add_cors(res: &mut Response) {
);
}
+fn res_propfind(res: &mut Response, content: &str) {
+ *res.status_mut() = StatusCode::MULTI_STATUS;
+ *res.body_mut() = Body::from(format!(
+ r#"
+
+{}
+"#,
+ content,
+ ));
+}
+
async fn zip_dir(writer: &mut W, dir: &Path) -> BoxResult<()> {
let mut writer = ZipFileWriter::new(writer);
let mut walkdir = WalkDir::new(dir);