summaryrefslogtreecommitdiff
path: root/src/files.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/files.rs')
-rw-r--r--src/files.rs100
1 files changed, 77 insertions, 23 deletions
diff --git a/src/files.rs b/src/files.rs
index 7ae34ea..555e8ae 100644
--- a/src/files.rs
+++ b/src/files.rs
@@ -1,6 +1,7 @@
use crate::{config::FileserverConfig, ServiceError};
use bytes::{Bytes, BytesMut};
use futures_util::{future, future::Either, ready, stream, FutureExt, Stream, StreamExt};
+use headers::{AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt};
use http_body_util::{combinators::BoxBody, BodyExt, StreamBody};
use humansize::FormatSizeOptions;
use hyper::{
@@ -11,7 +12,8 @@ use hyper::{
};
use log::debug;
use markup::Render;
-use std::{fs::Metadata, io, path::Path, pin::Pin, task::Poll};
+use percent_encoding::percent_decode_str;
+use std::{fs::Metadata, io, ops::Range, path::Path, pin::Pin, task::Poll};
use tokio::{
fs::{read_to_string, File},
io::AsyncSeekExt,
@@ -26,17 +28,19 @@ pub async fn serve_files(
let mut path = config.root.clone();
for seg in rpath.split("/") {
+ let seg = percent_decode_str(seg).decode_utf8()?;
if seg == "" || seg == ".." {
continue; // not ideal
}
- path.push(seg)
+ path.push(seg.as_ref())
}
-
if !path.exists() {
return Err(ServiceError::NotFound);
}
- if path.is_dir() {
+ let metadata = path.metadata()?;
+
+ if metadata.file_type().is_dir() {
if !config.index {
return Err(ServiceError::NotFound);
}
@@ -59,24 +63,30 @@ pub async fn serve_files(
});
}
+ let range = req.headers().typed_get::<headers::Range>();
+ let range = bytes_range(range, metadata.len())?;
+
let file = File::open(path.clone()).await?;
let mut r = Response::new(BoxBody::new(StreamBody::new(
- StreamBody::new(file_stream(file, 4096, (0, u64::MAX)))
+ StreamBody::new(file_stream(file, 4096, range.clone()))
.map(|e| e.map(|e| Frame::data(e)).map_err(ServiceError::Io)),
)));
- r.headers_mut().insert(
- CONTENT_TYPE,
- HeaderValue::from_str(
- // no allocation possible here?
- &mime_guess::from_path(path)
- .first()
- .map(|m| m.to_string())
- .unwrap_or("text/plain".to_string()),
- )
- .unwrap(),
- );
+ if range.end - range.start != metadata.len() {
+ *r.status_mut() = StatusCode::PARTIAL_CONTENT;
+ r.headers_mut().typed_insert(
+ ContentRange::bytes(range.clone(), metadata.len()).expect("valid ContentRange"),
+ );
+ }
+
+ let mime = mime_guess::from_path(path).first_or_octet_stream();
+
+ r.headers_mut()
+ .typed_insert(ContentLength(range.end - range.start));
+ r.headers_mut().typed_insert(ContentType::from(mime));
+ r.headers_mut().typed_insert(AcceptRanges::bytes());
+
Ok(r)
}
@@ -84,13 +94,13 @@ pub async fn serve_files(
fn file_stream(
mut file: File,
buf_size: usize,
- (start, end): (u64, u64),
+ range: Range<u64>,
) -> impl Stream<Item = Result<Bytes, io::Error>> + Send {
use std::io::SeekFrom;
let seek = async move {
- if start != 0 {
- file.seek(SeekFrom::Start(start)).await?;
+ if range.start != 0 {
+ file.seek(SeekFrom::Start(range.start)).await?;
}
Ok(file)
};
@@ -98,7 +108,7 @@ fn file_stream(
seek.into_stream()
.map(move |result| {
let mut buf = BytesMut::new();
- let mut len = end - start;
+ let mut len = range.end - range.start;
let mut f = match result {
Ok(f) => f,
Err(f) => return Either::Left(stream::once(future::err(f))),
@@ -137,6 +147,49 @@ fn file_stream(
.flatten()
}
+// Also adapted from warp
+fn bytes_range(range: Option<headers::Range>, max_len: u64) -> Result<Range<u64>, ServiceError> {
+ use std::ops::Bound;
+
+ let range = if let Some(range) = range {
+ range
+ } else {
+ return Ok(0..max_len);
+ };
+
+ let ret = range
+ .iter()
+ .map(|(start, end)| {
+ let start = match start {
+ Bound::Unbounded => 0,
+ Bound::Included(s) => s,
+ Bound::Excluded(s) => s + 1,
+ };
+
+ let end = match end {
+ Bound::Unbounded => max_len,
+ Bound::Included(s) => {
+ // For the special case where s == the file size
+ if s == max_len {
+ s
+ } else {
+ s + 1
+ }
+ }
+ Bound::Excluded(s) => s,
+ };
+
+ if start < end && end <= max_len {
+ Ok(start..end)
+ } else {
+ Err(ServiceError::BadRange)
+ }
+ })
+ .next()
+ .unwrap_or(Ok(0..max_len));
+ ret
+}
+
fn reserve_at_least(buf: &mut BytesMut, cap: usize) {
if buf.capacity() - buf.len() < cap {
buf.reserve(cap);
@@ -169,6 +222,7 @@ markup::define! {
@markup::doctype()
html {
head {
+ meta[charset="UTF-8"];
title { "Index of " @path }
}
body {
@@ -179,15 +233,15 @@ markup::define! {
}
hr;
table {
- @if path != "/" {
+ @if path != "/" {
tr { td { b { a[href=".."] { "../" } } } }
}
@for (name, meta) in files { tr {
- td { a[href=name] {
+ td { a[href=name] {
@name
@if meta.file_type().is_dir() { "/" }
} }
- td {
+ td {
@if meta.file_type().is_dir() {
i { "directory" }
} else {