diff --git a/Cargo.lock b/Cargo.lock
index 510b8a4..b311f77 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -306,6 +306,7 @@ dependencies = [
"headers",
"hyper",
"log",
+ "mime_guess",
"percent-encoding",
"serde",
"serde_json",
@@ -636,6 +637,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
[[package]]
name = "miniz_oxide"
version = "0.5.1"
@@ -991,6 +1002,15 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index a19e352..47d94fd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ simple_logger = "2.1.0"
async_zip = "0.0.7"
async-walkdir = "0.2.0"
headers = "0.3.7"
+mime_guess = "2.0.4"
[profile.release]
lto = true
diff --git a/src/index.html b/src/index.html
index 1009c16..19b6809 100644
--- a/src/index.html
+++ b/src/index.html
@@ -4,7 +4,7 @@
- Duf
+ Duf file server
__STYLE__
diff --git a/src/server.rs b/src/server.rs
index e88da23..c72fe42 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -5,7 +5,10 @@ use async_zip::write::{EntryOptions, ZipFileWriter};
use async_zip::Compression;
use futures::stream::StreamExt;
use futures::TryStreamExt;
-use headers::{AccessControlAllowHeaders, AccessControlAllowOrigin, HeaderMapExt};
+use headers::{
+ AccessControlAllowHeaders, AccessControlAllowOrigin, ContentType, ETag, HeaderMapExt,
+ IfModifiedSince, IfNoneMatch, LastModified,
+};
use hyper::header::{HeaderValue, ACCEPT, CONTENT_TYPE, ORIGIN, RANGE, WWW_AUTHENTICATE};
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, StatusCode};
@@ -121,7 +124,7 @@ impl InnerService {
}
self.handle_ls_dir(path.as_path(), true).await
} else {
- self.handle_send_file(path.as_path()).await
+ self.handle_send_file(&req, path.as_path()).await
}
}
Err(_) => {
@@ -234,11 +237,42 @@ impl InnerService {
Ok(Response::new(body))
}
- async fn handle_send_file(&self, path: &Path) -> BoxResult {
- let file = fs::File::open(path).await?;
+ async fn handle_send_file(&self, req: &Request, path: &Path) -> BoxResult {
+ let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
+ let (file, meta) = (file?, meta?);
+ let mut res = Response::default();
+ if let Ok(mtime) = meta.modified() {
+ let mtime_value = get_timestamp(&mtime);
+ let size = meta.len();
+ let etag = format!(r#""{}-{}""#, mtime_value, size)
+ .parse::()
+ .unwrap();
+ let last_modified = LastModified::from(mtime);
+ let fresh = {
+ // `If-None-Match` takes presedence over `If-Modified-Since`.
+ if let Some(if_none_match) = req.headers().typed_get::() {
+ !if_none_match.precondition_passes(&etag)
+ } else if let Some(if_modified_since) = req.headers().typed_get::()
+ {
+ !if_modified_since.is_modified(mtime)
+ } else {
+ false
+ }
+ };
+ res.headers_mut().typed_insert(last_modified);
+ res.headers_mut().typed_insert(etag);
+ if fresh {
+ *res.status_mut() = StatusCode::NOT_MODIFIED;
+ return Ok(res);
+ }
+ }
+ if let Some(mime) = mime_guess::from_path(&path).first() {
+ res.headers_mut().typed_insert(ContentType::from(mime));
+ }
let stream = FramedRead::new(file, BytesCodec::new());
let body = Body::wrap_stream(stream);
- Ok(Response::new(body))
+ *res.body_mut() = body;
+ Ok(res)
}
fn send_index(&self, path: &Path, mut paths: Vec) -> BoxResult {
@@ -254,7 +288,7 @@ impl InnerService {
INDEX_HTML.replace("__STYLE__", &format!("", INDEX_CSS));
output = output.replace("__DATA__", &data);
- Ok(hyper::Response::builder().body(output.into()).unwrap())
+ Ok(Response::new(output.into()))
}
fn auth_guard(&self, req: &Request) -> BoxResult {
@@ -311,7 +345,7 @@ struct IndexData {
struct PathItem {
path_type: PathType,
name: String,
- mtime: Option,
+ mtime: u64,
size: Option,
}
@@ -336,11 +370,7 @@ async fn get_path_item>(path: P, base_path: P) -> BoxResult PathType::SymlinkFile,
(false, false) => PathType::File,
};
- let mtime = meta
- .modified()?
- .duration_since(SystemTime::UNIX_EPOCH)
- .ok()
- .map(|v| v.as_millis() as u64);
+ let mtime = get_timestamp(&meta.modified()?);
let size = match path_type {
PathType::Dir | PathType::SymlinkDir => None,
PathType::File | PathType::SymlinkFile => Some(meta.len()),
@@ -354,6 +384,12 @@ async fn get_path_item>(path: P, base_path: P) -> BoxResult u64 {
+ time.duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap()
+ .as_millis() as u64
+}
+
fn normalize_path>(path: P) -> String {
let path = path.as_ref().to_str().unwrap_or_default();
if cfg!(windows) {