From 3032052923063f4f2b4735b8bf80fdd7d67c000e Mon Sep 17 00:00:00 2001 From: sigoden Date: Tue, 31 May 2022 08:38:30 +0800 Subject: [PATCH] feat: distinct upload and delete operation --- README.md | 19 ++++++++++--- src/args.rs | 30 ++++++++++++++------- src/assets/index.js | 6 ++--- src/server.rs | 65 ++++++++++++++++++++++++++++++--------------- 4 files changed, 83 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 978c015..3dec277 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,25 @@ duf duf folder_name ``` -Only serve static files, disable editing operations such as update or delete +Allow upload operations + ``` -duf --readonly +duf --allow-upload ``` -Finally, run this command to see a list of all available option +Allow all operations -### Curl +``` +duf --allow-all +``` + +Use http authentication + +``` +duf --auth user:pass +``` + +### Api Download a file ``` diff --git a/src/args.rs b/src/args.rs index 97ab01e..58a38a9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -35,10 +35,20 @@ fn app() -> clap::Command<'static> { .help("Path to a directory for serving files"), ) .arg( - Arg::new("readonly") - .short('r') - .long("readonly") - .help("Disable change operations such as update or delete"), + Arg::new("allow-all") + .short('A') + .long("allow-all") + .help("Allow all operations"), + ) + .arg( + Arg::new("allow-upload") + .long("allow-upload") + .help("Allow upload operation"), + ) + .arg( + Arg::new("allow-delete") + .long("allo-delete") + .help("Allow delete operation"), ) .arg( Arg::new("auth") @@ -68,9 +78,10 @@ pub struct Args { pub address: String, pub port: u16, pub path: PathBuf, - pub readonly: bool, pub auth: Option, pub no_auth_read: bool, + pub allow_upload: bool, + pub allow_delete: bool, pub cors: bool, } @@ -82,21 +93,22 @@ impl Args { pub fn parse(matches: ArgMatches) -> BoxResult { let address = matches.value_of("address").unwrap_or_default().to_owned(); let port = matches.value_of_t::("port")?; - let path = matches.value_of_os("path").unwrap_or_default(); - let path = Args::parse_path(path)?; - let readonly = matches.is_present("readonly"); + let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?; let cors = matches.is_present("cors"); let auth = matches.value_of("auth").map(|v| v.to_owned()); let no_auth_read = matches.is_present("no-auth-read"); + let allow_upload = matches.is_present("allow-all") || matches.is_present("allow-upload"); + let allow_delete = matches.is_present("allow-all") || matches.is_present("allow-delete"); Ok(Args { address, port, path, - readonly, auth, no_auth_read, cors, + allow_delete, + allow_upload, }) } diff --git a/src/assets/index.js b/src/assets/index.js index db4846d..9bf35a3 100644 --- a/src/assets/index.js +++ b/src/assets/index.js @@ -97,7 +97,7 @@ function addPath(file, index) { `; } - if (!DATA.readonly) { + if (DATA.allow_delete) { actionDelete = `
@@ -137,7 +137,7 @@ async function deletePath(index) { throw new Error(await res.text()) } } catch (err) { - alert(`Fail to delete ${file.name}, ${err.message}`); + alert(`Cannot delete \`${file.name}\`, ${err.message}`); } } @@ -191,7 +191,7 @@ function ready() { addBreadcrumb(DATA.breadcrumb); DATA.paths.forEach((file, index) => addPath(file, index)); - if (!DATA.readonly) { + if (DATA.allow_upload) { document.querySelector(".upload-control").classList.remove(["hidden"]); document.getElementById("file").addEventListener("change", e => { const files = e.target.files; diff --git a/src/server.rs b/src/server.rs index d93ecba..b7fc0bb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -33,6 +33,13 @@ const INDEX_CSS: &str = include_str!("assets/index.css"); const INDEX_JS: &str = include_str!("assets/index.js"); const BUF_SIZE: usize = 1024 * 16; +macro_rules! status { + ($res:ident, $status:expr) => { + *$res.status_mut() = $status; + *$res.body_mut() = Body::from($status.canonical_reason().unwrap_or_default()); + }; +} + pub async fn serve(args: Args) -> BoxResult<()> { let address = args.address()?; let inner = Arc::new(InnerService::new(args)); @@ -68,14 +75,18 @@ impl InnerService { let uri = req.uri().clone(); let cors = self.args.cors; - let mut res = self.handle(req).await.unwrap_or_else(|e| { - let mut res = Response::default(); - *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - *res.body_mut() = Body::from(e.to_string()); - res - }); - - info!(r#""{} {}" - {}"#, method, uri, res.status()); + let mut res = match self.handle(req).await { + Ok(res) => { + info!(r#""{} {}" - {}"#, method, uri, res.status()); + res + } + Err(err) => { + let mut res = Response::default(); + status!(res, StatusCode::INTERNAL_SERVER_ERROR); + error!(r#""{} {}" - {} {}"#, method, uri, res.status(), err); + res + } + }; if cors { add_cors(&mut res); @@ -95,7 +106,7 @@ impl InnerService { let filepath = match self.extract_path(path) { Some(v) => v, None => { - *res.status_mut() = StatusCode::FORBIDDEN; + status!(res, StatusCode::FORBIDDEN); return Ok(res); } }; @@ -106,8 +117,10 @@ impl InnerService { let meta = fs::metadata(filepath).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 readonly = self.args.readonly; + let allow_upload = self.args.allow_upload; + let allow_delete = self.args.allow_delete; match *req.method() { Method::GET if is_dir && query == "zip" => { @@ -117,7 +130,7 @@ impl InnerService { self.handle_query_dir(filepath, &query[3..], &mut res) .await? } - Method::GET if !is_dir && !is_miss => { + Method::GET if is_file => { self.handle_send_file(filepath, req.headers(), &mut res) .await? } @@ -125,12 +138,20 @@ impl InnerService { self.handle_ls_dir(filepath, false, &mut res).await? } Method::GET => self.handle_ls_dir(filepath, true, &mut res).await?, - Method::OPTIONS => *res.status_mut() = StatusCode::NO_CONTENT, - Method::PUT if readonly => *res.status_mut() = StatusCode::FORBIDDEN, + Method::OPTIONS => { + status!(res, StatusCode::NO_CONTENT); + } + Method::PUT if !allow_upload || (!allow_delete && is_file) => { + status!(res, StatusCode::FORBIDDEN); + } Method::PUT => self.handle_upload(filepath, req, &mut res).await?, - Method::DELETE if !is_miss && readonly => *res.status_mut() = StatusCode::FORBIDDEN, + Method::DELETE if !allow_delete => { + status!(res, StatusCode::FORBIDDEN); + } Method::DELETE if !is_miss => self.handle_delete(filepath, is_dir).await?, - _ => *res.status_mut() = StatusCode::NOT_FOUND, + _ => { + status!(res, StatusCode::NOT_FOUND); + } } Ok(res) @@ -153,7 +174,7 @@ impl InnerService { None => false, }; if !ensure_parent { - *res.status_mut() = StatusCode::FORBIDDEN; + status!(res, StatusCode::FORBIDDEN); return Ok(()); } @@ -288,7 +309,7 @@ impl InnerService { res.headers_mut().typed_insert(last_modified); res.headers_mut().typed_insert(etag); if fresh { - *res.status_mut() = StatusCode::NOT_MODIFIED; + status!(res, StatusCode::NOT_MODIFIED); return Ok(()); } } @@ -316,7 +337,8 @@ impl InnerService { let data = IndexData { breadcrumb: normalize_path(rel_path), paths, - readonly: self.args.readonly, + allow_upload: self.args.allow_upload, + allow_delete: self.args.allow_delete, }; let data = serde_json::to_string(&data).unwrap(); let output = INDEX_HTML.replace( @@ -347,7 +369,7 @@ impl InnerService { let mut it = v.split(' '); (it.next(), it.next()) }) { - Some((Some("Basic "), Some(tail))) => base64::decode(tail) + Some((Some("Basic"), Some(tail))) => base64::decode(tail) .ok() .and_then(|v| String::from_utf8(v).ok()) .map(|v| v.as_str() == auth) @@ -359,7 +381,7 @@ impl InnerService { } }; if !pass { - *res.status_mut() = StatusCode::UNAUTHORIZED; + status!(res, StatusCode::UNAUTHORIZED); res.headers_mut() .insert(WWW_AUTHENTICATE, HeaderValue::from_static("Basic")); } @@ -386,7 +408,8 @@ impl InnerService { struct IndexData { breadcrumb: String, paths: Vec, - readonly: bool, + allow_upload: bool, + allow_delete: bool, } #[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]