From 74b9eaf151668980a34a98420e6d7d8ead9dc20f Mon Sep 17 00:00:00 2001 From: Jose Quintana Date: Fri, 30 Apr 2021 12:16:57 +0200 Subject: [PATCH] refactor: just one file metadata per request as possible --- src/fs.rs | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------- 1 file changed, 144 insertions(+), 75 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 4f5ff62..c0b3cc5 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -40,20 +40,47 @@ pub async fn handle_request( path: &str, ) -> Result, StatusCode> { let base = Arc::new(base.into()); - let path = path_from_tail(base, path).await?; - file_reply(headers, path).await + let res = path_from_tail(base, path).await?; + file_reply(headers, res).await +} + +fn path_from_tail( + base: Arc, + tail: &str, +) -> impl Future> + Send { + future::ready(sanitize_path(base.as_ref(), tail)).and_then(|mut buf| async { + match tokio::fs::metadata(&buf).await { + Ok(meta) => { + let mut auto_index = false; + if meta.is_dir() { + tracing::debug!("dir: appending index.html to directory path"); + buf.push("index.html"); + auto_index = true; + } + tracing::trace!("dir: {:?}", buf); + Ok((ArcPath(Arc::new(buf)), meta, auto_index)) + } + Err(err) => { + tracing::debug!("file not found: {:?}", err); + Err(StatusCode::NOT_FOUND) + } + } + }) } /// Reply with a file content. fn file_reply( headers: &HeaderMap, - path: ArcPath, + res: (ArcPath, Metadata, bool), ) -> impl Future, StatusCode>> + Send { + // TODO: directory listing + + let (path, meta, auto_index) = res; let conditionals = get_conditional_headers(headers); TkFile::open(path.clone()).then(move |res| match res { - Ok(f) => Either::Left(file_conditional(f, path, conditionals)), + Ok(f) => Either::Left(file_conditional(f, path, meta, auto_index, conditionals)), Err(err) => { - let rej = match err.kind() { + let status = match err.kind() { io::ErrorKind::NotFound => { tracing::debug!("file not found: {:?}", path.as_ref().display()); StatusCode::NOT_FOUND @@ -71,7 +98,7 @@ fn file_reply( StatusCode::INTERNAL_SERVER_ERROR } }; - Either::Right(future::err(rej)) + Either::Right(future::err(status)) } }) } @@ -90,25 +117,6 @@ fn get_conditional_headers(header_list: &HeaderMap) -> Conditionals } } -fn path_from_tail( - base: Arc, - tail: &str, -) -> impl Future> + Send { - future::ready(sanitize_path(base.as_ref(), tail)).and_then(|mut buf| async { - let is_dir = tokio::fs::metadata(buf.clone()) - .await - .map(|m| m.is_dir()) - .unwrap_or(false); - - if is_dir { - tracing::debug!("dir: appending index.html to directory path"); - buf.push("index.html"); - } - tracing::trace!("dir: {:?}", buf); - Ok(ArcPath(Arc::new(buf))) - }) -} - fn sanitize_path(base: impl AsRef, tail: &str) -> Result { let mut buf = PathBuf::from(base.as_ref()); let p = match percent_decode_str(tail).decode_utf8() { @@ -198,59 +206,22 @@ impl Conditionals { fn file_conditional( f: TkFile, path: ArcPath, + meta: Metadata, + auto_index: bool, conditionals: Conditionals, ) -> impl Future, StatusCode>> + Send { - file_metadata(f).map_ok(move |(file, meta)| { - let mut len = meta.len(); - let modified = meta.modified().ok().map(LastModified::from); - - match conditionals.check(modified) { - Cond::NoBody(resp) => resp, - Cond::WithBody(range) => { - bytes_range(range, len) - .map(|(start, end)| { - let sub_len = end - start; - let buf_size = optimal_buf_size(&meta); - let stream = file_stream(file, buf_size, (start, end)); - let body = Body::wrap_stream(stream); - - let mut resp = Response::new(body); - - if sub_len != len { - *resp.status_mut() = StatusCode::PARTIAL_CONTENT; - resp.headers_mut().typed_insert( - ContentRange::bytes(start..end, len).expect("valid ContentRange"), - ); - - len = sub_len; - } - - let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream(); - - resp.headers_mut().typed_insert(ContentLength(len)); - resp.headers_mut().typed_insert(ContentType::from(mime)); - resp.headers_mut().typed_insert(AcceptRanges::bytes()); - - if let Some(last_modified) = modified { - resp.headers_mut().typed_insert(last_modified); - } - - resp - }) - .unwrap_or_else(|BadRange| { - // bad byte range - let mut resp = Response::new(Body::empty()); - *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; - resp.headers_mut() - .typed_insert(ContentRange::unsatisfied_bytes(len)); - resp - }) - } - } - }) + file_metadata(f, meta, auto_index) + .map_ok(|(file, meta)| response_body(file, meta, path, conditionals)) } -async fn file_metadata(f: TkFile) -> Result<(TkFile, Metadata), StatusCode> { +async fn file_metadata( + f: TkFile, + meta: Metadata, + auto_index: bool, +) -> Result<(TkFile, Metadata), StatusCode> { + if !auto_index { + return Ok((f, meta)); + } match f.metadata().await { Ok(meta) => Ok((f, meta)), Err(err) => { @@ -260,6 +231,59 @@ async fn file_metadata(f: TkFile) -> Result<(TkFile, Metadata), StatusCode> { } } +fn response_body( + file: TkFile, + meta: Metadata, + path: ArcPath, + conditionals: Conditionals, +) -> Response { + let mut len = meta.len(); + let modified = meta.modified().ok().map(LastModified::from); + match conditionals.check(modified) { + Cond::NoBody(resp) => resp, + Cond::WithBody(range) => { + bytes_range(range, len) + .map(|(start, end)| { + let sub_len = end - start; + let buf_size = optimal_buf_size(&meta); + let stream = file_stream(file, buf_size, (start, end)); + let body = Body::wrap_stream(stream); + + let mut resp = Response::new(body); + + if sub_len != len { + *resp.status_mut() = StatusCode::PARTIAL_CONTENT; + resp.headers_mut().typed_insert( + ContentRange::bytes(start..end, len).expect("valid ContentRange"), + ); + + len = sub_len; + } + + let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream(); + + resp.headers_mut().typed_insert(ContentLength(len)); + resp.headers_mut().typed_insert(ContentType::from(mime)); + resp.headers_mut().typed_insert(AcceptRanges::bytes()); + + if let Some(last_modified) = modified { + resp.headers_mut().typed_insert(last_modified); + } + + resp + }) + .unwrap_or_else(|BadRange| { + // bad byte range + let mut resp = Response::new(Body::empty()); + *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; + resp.headers_mut() + .typed_insert(ContentRange::unsatisfied_bytes(len)); + resp + }) + } + } +} + struct BadRange; fn bytes_range(range: Option, max_len: u64) -> Result<(u64, u64), BadRange> { @@ -280,7 +304,14 @@ fn bytes_range(range: Option, max_len: u64) -> Result<(u64, u64), BadRang let end = match end { Bound::Unbounded => max_len, - Bound::Included(s) => s + 1, + Bound::Included(s) => { + // For the special case where s == the file size + if s == max_len { + s + } else { + s + 1 + } + } Bound::Excluded(s) => s, }; @@ -380,3 +411,41 @@ fn get_block_size(metadata: &Metadata) -> usize { fn get_block_size(_metadata: &Metadata) -> usize { DEFAULT_READ_BUF_SIZE } + +#[cfg(test)] +mod tests { + use super::sanitize_path; + use bytes::BytesMut; + + #[test] + fn test_sanitize_path() { + let base = "/var/www"; + + fn p(s: &str) -> &::std::path::Path { + s.as_ref() + } + + assert_eq!( + sanitize_path(base, "/foo.html").unwrap(), + p("/var/www/foo.html") + ); + + // bad paths + sanitize_path(base, "/../foo.html").expect_err("dot dot"); + + sanitize_path(base, "/C:\\/foo.html").expect_err("C:\\"); + } + + #[test] + fn test_reserve_at_least() { + let mut buf = BytesMut::new(); + let cap = 8_192; + + assert_eq!(buf.len(), 0); + assert_eq!(buf.capacity(), 0); + + super::reserve_at_least(&mut buf, cap); + assert_eq!(buf.len(), 0); + assert_eq!(buf.capacity(), cap); + } +} -- libgit2 1.7.2