Move byte range code into its own file
parent
b43a6c2e4b
commit
b94a3a1e17
|
@ -32,8 +32,6 @@ use tokio::{
|
||||||
};
|
};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
#[cfg (test)]
|
#[cfg (test)]
|
||||||
use always_equal::test::AlwaysEqual;
|
use always_equal::test::AlwaysEqual;
|
||||||
|
|
||||||
|
@ -51,11 +49,17 @@ use ptth_core::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
mod range;
|
||||||
|
|
||||||
use errors::{
|
use errors::{
|
||||||
FileServerError,
|
FileServerError,
|
||||||
MarkdownError,
|
MarkdownError,
|
||||||
};
|
};
|
||||||
|
use range::{
|
||||||
|
check_range,
|
||||||
|
ParsedRange,
|
||||||
|
ValidParsedRange,
|
||||||
|
};
|
||||||
|
|
||||||
mod emoji {
|
mod emoji {
|
||||||
pub const VIDEO: &str = "\u{1f39e}\u{fe0f}";
|
pub const VIDEO: &str = "\u{1f39e}\u{fe0f}";
|
||||||
|
@ -100,71 +104,6 @@ struct TemplateDirPage <'a> {
|
||||||
entries: Vec <TemplateDirEntry>,
|
entries: Vec <TemplateDirEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_range_header (range_str: &str) -> (Option <u64>, Option <u64>) {
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref RE: Regex = Regex::new (r"^bytes=(\d*)-(\d*)$").expect ("Couldn't compile regex for Range header");
|
|
||||||
}
|
|
||||||
|
|
||||||
debug! ("{}", range_str);
|
|
||||||
|
|
||||||
let caps = match RE.captures (range_str) {
|
|
||||||
Some (x) => x,
|
|
||||||
None => return (None, None),
|
|
||||||
};
|
|
||||||
let start = caps.get (1).map (|x| x.as_str ());
|
|
||||||
let end = caps.get (2).map (|x| x.as_str ());
|
|
||||||
|
|
||||||
let start = start.and_then (|x| u64::from_str_radix (x, 10).ok ());
|
|
||||||
|
|
||||||
// HTTP specifies ranges as [start inclusive, end inclusive]
|
|
||||||
// But that's dumb and [start inclusive, end exclusive) is better
|
|
||||||
|
|
||||||
let end = end.and_then (|x| u64::from_str_radix (x, 10).ok ().map (|x| x + 1));
|
|
||||||
|
|
||||||
(start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
#[derive (Debug, PartialEq)]
|
|
||||||
enum ParsedRange {
|
|
||||||
Ok (Range <u64>),
|
|
||||||
PartialContent (Range <u64>),
|
|
||||||
RangeNotSatisfiable (u64),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_range (range_str: Option <&str>, file_len: u64)
|
|
||||||
-> ParsedRange
|
|
||||||
{
|
|
||||||
use ParsedRange::*;
|
|
||||||
|
|
||||||
let not_satisfiable = RangeNotSatisfiable (file_len);
|
|
||||||
|
|
||||||
let range_str = match range_str {
|
|
||||||
None => return Ok (0..file_len),
|
|
||||||
Some (x) => x,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (start, end) = parse_range_header (range_str);
|
|
||||||
|
|
||||||
let start = start.unwrap_or (0);
|
|
||||||
if start >= file_len {
|
|
||||||
return not_satisfiable;
|
|
||||||
}
|
|
||||||
|
|
||||||
let end = end.unwrap_or (file_len);
|
|
||||||
if end > file_len {
|
|
||||||
return not_satisfiable;
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
return not_satisfiable;
|
|
||||||
}
|
|
||||||
|
|
||||||
PartialContent (start..end)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_icon (file_name: &str) -> &'static str {
|
fn get_icon (file_name: &str) -> &'static str {
|
||||||
if
|
if
|
||||||
file_name.ends_with (".mp4") ||
|
file_name.ends_with (".mp4") ||
|
||||||
|
@ -289,8 +228,7 @@ async fn serve_dir (
|
||||||
async fn serve_file (
|
async fn serve_file (
|
||||||
mut f: File,
|
mut f: File,
|
||||||
should_send_body: bool,
|
should_send_body: bool,
|
||||||
range: Range <u64>,
|
range: ValidParsedRange
|
||||||
range_requested: bool
|
|
||||||
)
|
)
|
||||||
-> Result <Response, FileServerError>
|
-> Result <Response, FileServerError>
|
||||||
{
|
{
|
||||||
|
@ -302,6 +240,8 @@ async fn serve_file (
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (range, range_requested) = (range.range, range.range_requested);
|
||||||
|
|
||||||
info! ("Serving range {}-{}", range.start, range.end);
|
info! ("Serving range {}-{}", range.start, range.end);
|
||||||
|
|
||||||
let content_length = range.end - range.start;
|
let content_length = range.end - range.start;
|
||||||
|
@ -435,8 +375,7 @@ struct ServeDirParams {
|
||||||
#[derive (Debug, PartialEq)]
|
#[derive (Debug, PartialEq)]
|
||||||
struct ServeFileParams {
|
struct ServeFileParams {
|
||||||
send_body: bool,
|
send_body: bool,
|
||||||
range: Range <u64>,
|
range: ValidParsedRange,
|
||||||
range_requested: bool,
|
|
||||||
file: AlwaysEqual <File>,
|
file: AlwaysEqual <File>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,8 +447,12 @@ async fn internal_serve_file (
|
||||||
|
|
||||||
Ok (match check_range (range_header, file_len) {
|
Ok (match check_range (range_header, file_len) {
|
||||||
ParsedRange::RangeNotSatisfiable (file_len) => InternalResponse::RangeNotSatisfiable (file_len),
|
ParsedRange::RangeNotSatisfiable (file_len) => InternalResponse::RangeNotSatisfiable (file_len),
|
||||||
ParsedRange::Ok (range) => {
|
ParsedRange::Valid (range) => {
|
||||||
if uri.query () == Some ("as_markdown") {
|
if uri.query () == Some ("as_markdown") {
|
||||||
|
if range.range_requested {
|
||||||
|
return Ok (InternalResponse::InvalidQuery);
|
||||||
|
}
|
||||||
|
|
||||||
const MAX_BUF_SIZE: u32 = 1_000_000;
|
const MAX_BUF_SIZE: u32 = 1_000_000;
|
||||||
if file_len > MAX_BUF_SIZE.into () {
|
if file_len > MAX_BUF_SIZE.into () {
|
||||||
InternalResponse::MarkdownErr (MarkdownError::TooBig)
|
InternalResponse::MarkdownErr (MarkdownError::TooBig)
|
||||||
|
@ -529,22 +472,6 @@ async fn internal_serve_file (
|
||||||
file,
|
file,
|
||||||
send_body,
|
send_body,
|
||||||
range,
|
range,
|
||||||
range_requested: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ParsedRange::PartialContent (range) => {
|
|
||||||
if uri.query ().is_some () {
|
|
||||||
InternalResponse::InvalidQuery
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let file = file.into ();
|
|
||||||
|
|
||||||
InternalResponse::ServeFile (ServeFileParams {
|
|
||||||
file,
|
|
||||||
send_body,
|
|
||||||
range,
|
|
||||||
range_requested: true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -670,8 +597,7 @@ pub async fn serve_all (
|
||||||
file,
|
file,
|
||||||
send_body,
|
send_body,
|
||||||
range,
|
range,
|
||||||
range_requested,
|
}) => serve_file (file.into_inner (), send_body, range).await?,
|
||||||
}) => serve_file (file.into_inner (), send_body, range, range_requested).await?,
|
|
||||||
MarkdownErr (e) => match e {
|
MarkdownErr (e) => match e {
|
||||||
MarkdownError::TooBig => serve_error (StatusCode::InternalServerError, "File is too big to preview as Markdown"),
|
MarkdownError::TooBig => serve_error (StatusCode::InternalServerError, "File is too big to preview as Markdown"),
|
||||||
//MarkdownError::NotMarkdown => serve_error (StatusCode::BadRequest, "File is not Markdown"),
|
//MarkdownError::NotMarkdown => serve_error (StatusCode::BadRequest, "File is not Markdown"),
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
pub fn parse_range_header (range_str: &str) -> (Option <u64>, Option <u64>) {
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref RE: Regex = Regex::new (r"^bytes=(\d*)-(\d*)$").expect ("Couldn't compile regex for Range header");
|
||||||
|
}
|
||||||
|
|
||||||
|
let caps = match RE.captures (range_str) {
|
||||||
|
Some (x) => x,
|
||||||
|
None => return (None, None),
|
||||||
|
};
|
||||||
|
let start = caps.get (1).map (|x| x.as_str ());
|
||||||
|
let end = caps.get (2).map (|x| x.as_str ());
|
||||||
|
|
||||||
|
let start = start.and_then (|x| u64::from_str_radix (x, 10).ok ());
|
||||||
|
|
||||||
|
// HTTP specifies ranges as [start inclusive, end inclusive]
|
||||||
|
// But that's dumb and [start inclusive, end exclusive) is better
|
||||||
|
|
||||||
|
let end = end.and_then (|x| u64::from_str_radix (x, 10).ok ().map (|x| x + 1));
|
||||||
|
|
||||||
|
(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive (Debug, PartialEq)]
|
||||||
|
pub struct ValidParsedRange {
|
||||||
|
pub range: Range <u64>,
|
||||||
|
pub range_requested: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive (Debug, PartialEq)]
|
||||||
|
pub enum ParsedRange {
|
||||||
|
Valid (ValidParsedRange),
|
||||||
|
RangeNotSatisfiable (u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_range (range_str: Option <&str>, file_len: u64)
|
||||||
|
-> ParsedRange
|
||||||
|
{
|
||||||
|
use ParsedRange::*;
|
||||||
|
|
||||||
|
let not_satisfiable = RangeNotSatisfiable (file_len);
|
||||||
|
|
||||||
|
let range_str = match range_str {
|
||||||
|
None => return Valid (ValidParsedRange {
|
||||||
|
range: 0..file_len,
|
||||||
|
range_requested: false,
|
||||||
|
}),
|
||||||
|
Some (x) => x,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (start, end) = parse_range_header (range_str);
|
||||||
|
|
||||||
|
let start = start.unwrap_or (0);
|
||||||
|
if start >= file_len {
|
||||||
|
return not_satisfiable;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = end.unwrap_or (file_len);
|
||||||
|
if end > file_len {
|
||||||
|
return not_satisfiable;
|
||||||
|
}
|
||||||
|
if end < start {
|
||||||
|
return not_satisfiable;
|
||||||
|
}
|
||||||
|
|
||||||
|
Valid (ValidParsedRange {
|
||||||
|
range: start..end,
|
||||||
|
range_requested: true,
|
||||||
|
})
|
||||||
|
}
|
|
@ -34,33 +34,46 @@ fn icons () {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_range_header () {
|
fn parse_range_header () {
|
||||||
|
use super::range::{
|
||||||
|
*,
|
||||||
|
ParsedRange::*,
|
||||||
|
};
|
||||||
|
|
||||||
for (input, expected) in vec! [
|
for (input, expected) in vec! [
|
||||||
("", (None, None)),
|
("", (None, None)),
|
||||||
("bytes=0-", (Some (0), None)),
|
("bytes=0-", (Some (0), None)),
|
||||||
("bytes=0-999", (Some (0), Some (1000))),
|
("bytes=0-999", (Some (0), Some (1000))),
|
||||||
("bytes=111-999", (Some (111), Some (1000))),
|
("bytes=111-999", (Some (111), Some (1000))),
|
||||||
].into_iter () {
|
].into_iter () {
|
||||||
let actual = super::parse_range_header (input);
|
let actual = parse_range_header (input);
|
||||||
assert_eq! (actual, expected);
|
assert_eq! (actual, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
use super::ParsedRange::*;
|
let ok_range = |range| Valid (ValidParsedRange {
|
||||||
|
range,
|
||||||
|
range_requested: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let partial_content = |range| Valid (ValidParsedRange {
|
||||||
|
range,
|
||||||
|
range_requested: true,
|
||||||
|
});
|
||||||
|
|
||||||
for (header, file_len, expected) in vec! [
|
for (header, file_len, expected) in vec! [
|
||||||
(None, 0, Ok (0..0)),
|
(None, 0, ok_range (0..0)),
|
||||||
(None, 1024, Ok (0..1024)),
|
(None, 1024, ok_range (0..1024)),
|
||||||
|
|
||||||
(Some (""), 0, RangeNotSatisfiable (0)),
|
(Some (""), 0, RangeNotSatisfiable (0)),
|
||||||
(Some (""), 1024, PartialContent (0..1024)),
|
(Some (""), 1024, partial_content (0..1024)),
|
||||||
|
|
||||||
(Some ("bytes=0-"), 1024, PartialContent (0..1024)),
|
(Some ("bytes=0-"), 1024, partial_content (0..1024)),
|
||||||
(Some ("bytes=0-999"), 1024, PartialContent (0..1000)),
|
(Some ("bytes=0-999"), 1024, partial_content (0..1000)),
|
||||||
(Some ("bytes=0-1023"), 1024, PartialContent (0..1024)),
|
(Some ("bytes=0-1023"), 1024, partial_content (0..1024)),
|
||||||
(Some ("bytes=111-999"), 1024, PartialContent (111..1000)),
|
(Some ("bytes=111-999"), 1024, partial_content (111..1000)),
|
||||||
(Some ("bytes=111-1023"), 1024, PartialContent (111..1024)),
|
(Some ("bytes=111-1023"), 1024, partial_content (111..1024)),
|
||||||
(Some ("bytes=200-100"), 1024, RangeNotSatisfiable (1024)),
|
(Some ("bytes=200-100"), 1024, RangeNotSatisfiable (1024)),
|
||||||
|
|
||||||
(Some ("bytes=0-"), 512, PartialContent (0..512)),
|
(Some ("bytes=0-"), 512, partial_content (0..512)),
|
||||||
(Some ("bytes=0-1023"), 512, RangeNotSatisfiable (512)),
|
(Some ("bytes=0-1023"), 512, RangeNotSatisfiable (512)),
|
||||||
(Some ("bytes=1000-1023"), 512, RangeNotSatisfiable (512)),
|
(Some ("bytes=1000-1023"), 512, RangeNotSatisfiable (512)),
|
||||||
].into_iter () {
|
].into_iter () {
|
||||||
|
@ -139,14 +152,18 @@ fn file_server () {
|
||||||
("/files/src/?", InvalidQuery),
|
("/files/src/?", InvalidQuery),
|
||||||
(bad_passwords_path, ServeFile (ServeFileParams {
|
(bad_passwords_path, ServeFile (ServeFileParams {
|
||||||
send_body: true,
|
send_body: true,
|
||||||
range: 0..1_048_576,
|
range: ValidParsedRange {
|
||||||
range_requested: false,
|
range: 0..1_048_576,
|
||||||
|
range_requested: false,
|
||||||
|
},
|
||||||
file: AlwaysEqual::testing_blank (),
|
file: AlwaysEqual::testing_blank (),
|
||||||
})),
|
})),
|
||||||
("/files/test/test.md", ServeFile (ServeFileParams {
|
("/files/test/test.md", ServeFile (ServeFileParams {
|
||||||
send_body: true,
|
send_body: true,
|
||||||
range: 0..144,
|
range: ValidParsedRange {
|
||||||
range_requested: false,
|
range: 0..144,
|
||||||
|
range_requested: false,
|
||||||
|
},
|
||||||
file: AlwaysEqual::testing_blank (),
|
file: AlwaysEqual::testing_blank (),
|
||||||
})),
|
})),
|
||||||
] {
|
] {
|
||||||
|
@ -200,8 +217,10 @@ fn file_server () {
|
||||||
|
|
||||||
assert_eq! (resp.expect ("Should be Ok (_)"), ServeFile (ServeFileParams {
|
assert_eq! (resp.expect ("Should be Ok (_)"), ServeFile (ServeFileParams {
|
||||||
send_body: false,
|
send_body: false,
|
||||||
range: 0..1_048_576,
|
range: ValidParsedRange {
|
||||||
range_requested: false,
|
range: 0..1_048_576,
|
||||||
|
range_requested: false,
|
||||||
|
},
|
||||||
file: AlwaysEqual::testing_blank (),
|
file: AlwaysEqual::testing_blank (),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue