Skip to content

Commit

Permalink
feat: partial content delivery
Browse files Browse the repository at this point in the history
this commit adds partial content delivery support
borrowing @Cobrand's PR iron/staticfile#98
commit motivated by static-web-server/static-web-server#15
  • Loading branch information
joseluisq committed May 3, 2020
1 parent d3107bb commit fbe66f6
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod modify_with;
mod prefix;
mod rewrite;
mod staticfile;
mod partial_file;

pub use crate::cache::Cache;
pub use crate::guess_content_type::GuessContentType;
Expand Down
138 changes: 138 additions & 0 deletions src/partial_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// NOTE:
// This file implements Partial Content Delivery which is used as part as this middleware.
// Code below was borrowed from one @Cobrand's PR and adapted to this project.
// More details at https://github.com/iron/staticfile/pull/98

use iron::headers::{ByteRangeSpec, ContentLength, ContentRange, ContentRangeSpec};
use iron::modifier::Modifier;
use iron::response::{Response, WriteBody};
use iron::status::Status;
use std::cmp;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;

pub enum PartialFileRange {
AllFrom(u64),
FromTo(u64, u64),
Last(u64),
}

pub struct PartialFile {
file: File,
range: PartialFileRange,
}

struct PartialContentBody {
pub file: File,
pub offset: u64,
pub len: u64,
}

impl PartialFile {
pub fn new<Range>(file: File, range: Range) -> PartialFile
where
Range: Into<PartialFileRange>,
{
let range = range.into();
PartialFile { file, range }
}

pub fn from_path<P: AsRef<Path>, Range>(path: P, range: Range) -> Result<PartialFile, io::Error>
where
Range: Into<PartialFileRange>,
{
let file = File::open(path.as_ref())?;
Ok(Self::new(file, range))
}
}

impl From<ByteRangeSpec> for PartialFileRange {
fn from(b: ByteRangeSpec) -> PartialFileRange {
match b {
ByteRangeSpec::AllFrom(from) => PartialFileRange::AllFrom(from),
ByteRangeSpec::FromTo(from, to) => PartialFileRange::FromTo(from, to),
ByteRangeSpec::Last(last) => PartialFileRange::Last(last),
}
}
}

impl From<Vec<ByteRangeSpec>> for PartialFileRange {
fn from(v: Vec<ByteRangeSpec>) -> PartialFileRange {
match v.into_iter().next() {
// in the case no value is in "Range", return
// the whole file instead of panicking
// Note that an empty vec should never happen,
// but we can never be too sure
None => PartialFileRange::AllFrom(0),
Some(byte_range) => PartialFileRange::from(byte_range),
}
}
}

impl Modifier<Response> for PartialFile {
#[inline]
fn modify(self, res: &mut Response) {
use self::PartialFileRange::*;

let metadata: Option<_> = self.file.metadata().ok();
let file_length: Option<u64> = metadata.map(|m| m.len());
let range: Option<(u64, u64)> = match (self.range, file_length) {
(FromTo(from, to), Some(file_length)) => {
if from <= to && from < file_length {
Some((from, cmp::min(to, file_length - 1)))
} else {
None
}
}
(AllFrom(from), Some(file_length)) => {
if from < file_length {
Some((from, file_length - 1))
} else {
None
}
}
(Last(last), Some(file_length)) => {
if last < file_length {
Some((file_length - last, file_length - 1))
} else {
Some((0, file_length - 1))
}
}
(_, None) => None,
};

if let Some(range) = range {
let content_range = ContentRange(ContentRangeSpec::Bytes {
range: Some(range),
instance_length: file_length,
});
let content_len = range.1 - range.0 + 1;
res.headers.set(ContentLength(content_len));
res.headers.set(content_range);
let partial_content = PartialContentBody {
file: self.file,
offset: range.0,
len: content_len,
};
res.status = Some(Status::PartialContent);
res.body = Some(Box::new(partial_content));
} else {
if let Some(file_length) = file_length {
res.headers.set(ContentRange(ContentRangeSpec::Bytes {
range: None,
instance_length: Some(file_length),
}));
};
res.status = Some(Status::RangeNotSatisfiable);
}
}
}

impl WriteBody for PartialContentBody {
fn write_body(&mut self, res: &mut dyn Write) -> io::Result<()> {
self.file.seek(SeekFrom::Start(self.offset))?;
let mut limiter = <File as Read>::by_ref(&mut self.file).take(self.len);
io::copy(&mut limiter, res).map(|_| ())
}
}
39 changes: 31 additions & 8 deletions src/staticfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ use std::{error, io};

use iron::headers::{
AcceptEncoding, ContentEncoding, ContentLength, Encoding, HttpDate, IfModifiedSince,
LastModified,
LastModified, AcceptRanges, RangeUnit, Range,
};

use iron::method::Method;
use iron::middleware::Handler;
use iron::modifiers::Header;
use iron::prelude::*;
use iron::status;
use partial_file::PartialFile;

use time;

Expand Down Expand Up @@ -88,7 +90,8 @@ impl Handler for Staticfile {
None => false,
};

let file = match StaticFileWithMetadata::search(file_path, accept_gz) {
// Get current file metadata
let file = match StaticFileWithMetadata::search(&file_path, accept_gz) {
Ok(file) => file,
Err(_) => return Ok(Response::with(status::NotFound)),
};
Expand All @@ -110,12 +113,7 @@ impl Handler for Staticfile {
}
}

let encoding = if file.is_gz {
Encoding::Gzip
} else {
Encoding::Identity
};

let encoding = if file.is_gz { Encoding::Gzip } else { Encoding::Identity };
let encoding = ContentEncoding(vec![encoding]);

let mut resp = match last_modified {
Expand All @@ -139,6 +137,31 @@ impl Handler for Staticfile {
resp.set_mut(Header(ContentLength(file.metadata.len())));
return Ok(resp);
}

// Partial content delivery response
let accept_range_header = Header(AcceptRanges(vec![RangeUnit::Bytes]));
let range_req_header = req.headers.get::<Range>().cloned();

let resp = match range_req_header {
None => {
// Deliver the whole file
resp.set_mut(accept_range_header);
resp
},
Some(range) => {
// Try to deliver partial content
match range {
Range::Bytes(vec_range) => {
if let Ok(partial_file) = PartialFile::from_path(&file_path, vec_range) {
Response::with((status::Ok, partial_file, accept_range_header))
} else {
Response::with(status::NotFound)
}
},
_ => Response::with(status::RangeNotSatisfiable)
}
}
};

Ok(resp)
}
Expand Down

0 comments on commit fbe66f6

Please sign in to comment.