diff options
author | metamuffin <metamuffin@disroot.org> | 2023-02-26 16:18:45 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-02-26 16:18:45 +0100 |
commit | a3fb25019336ab9238d73f29a004b71cfc31a032 (patch) | |
tree | 783b91075f61f4b89eef35f08064ca15b0ddb2f9 /src/files.rs | |
parent | c3371bd7e3eb40fad374fe85a994806c2d27488e (diff) | |
download | gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar.bz2 gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar.zst |
support range requests
Diffstat (limited to 'src/files.rs')
-rw-r--r-- | src/files.rs | 100 |
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 { |