diff --git a/src/lib.rs b/src/lib.rs index 00fada8..f298512 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,3 +19,4 @@ pub use static_handler::Cache; mod requested_path; mod static_handler; +mod partial_file; diff --git a/src/partial_file.rs b/src/partial_file.rs new file mode 100644 index 0000000..d4b7455 --- /dev/null +++ b/src/partial_file.rs @@ -0,0 +1,132 @@ +use std::fs::File; +use iron::headers::{ByteRangeSpec, ContentLength, ContentRange, ContentRangeSpec}; +use iron::response::{WriteBody, Response}; +use iron::modifier::Modifier; +use iron::status::Status; +use std::io::{self, SeekFrom, Seek, Read, 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(file: File, range: Range) -> PartialFile + where Range: Into { + let range = range.into(); + PartialFile { + file: file, + range: range, + } + } + + /// Panics if the file doesn't exist + pub fn from_path, Range>(path: P, range: Range) -> PartialFile + where Range: Into { + let file = File::open(path.as_ref()) + .expect(&format!("No such file: {}", path.as_ref().display())); + Self::new(file, range) + } +} + +impl From 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> for PartialFileRange { + fn from(v: Vec) -> 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 for PartialFile { + #[inline] + fn modify(self, res: &mut Response) { + use self::PartialFileRange::*; + let metadata : Option<_> = self.file.metadata().ok(); + let file_length : Option = metadata.map(|m| m.len()); + let range : Option<(u64, u64)> = match (self.range, file_length) { + (FromTo(from, to), Some(file_length)) => { + if from <= to && to < file_length { + Some((from, to)) + } 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 Write) -> io::Result<()> { + self.file.seek(SeekFrom::Start(self.offset))?; + let mut limiter = ::by_ref(&mut self.file).take(self.len); + io::copy(&mut limiter, res).map(|_| ()) + } +} diff --git a/src/static_handler.rs b/src/static_handler.rs index 5663e9a..0d54864 100644 --- a/src/static_handler.rs +++ b/src/static_handler.rs @@ -10,13 +10,16 @@ use std::time::Duration; use iron::prelude::*; use iron::{Handler, Url, status}; +use iron::headers::{AcceptRanges, RangeUnit, Range}; #[cfg(feature = "cache")] use iron::modifier::Modifier; -use iron::modifiers::Redirect; +use iron::modifiers::{Header, Redirect}; use mount::OriginalUrl; use requested_path::RequestedPath; use url; +use partial_file::PartialFile; + /// The static file-serving `Handler`. /// /// This handler serves files from a single filesystem path, which may be absolute or relative. @@ -76,7 +79,17 @@ impl Static { #[cfg(feature = "cache")] fn try_cache>(&self, req: &mut Request, path: P) -> IronResult { match self.cache { - None => Ok(Response::with((status::Ok, path.as_ref()))), + None => { + let accept_range_header = Header(AcceptRanges(vec![RangeUnit::Bytes])); + match req.headers.get::() { + None => Ok(Response::with((status::Ok, path.as_ref(), accept_range_header))), + Some(&Range::Bytes(ref v)) => { + let partial_file = PartialFile::from_path(path.as_ref(),v.clone()); + Ok(Response::with((partial_file, accept_range_header))) + }, + Some(_) => Ok(Response::with((status::RangeNotSatisfiable, accept_range_header))), + } + }, Some(ref cache) => cache.handle(req, path.as_ref()), } } @@ -130,8 +143,27 @@ impl Handler for Static { Some(path) => self.try_cache(req, path), #[cfg(not(feature = "cache"))] Some(path) => { - let path: &Path = &path; - Ok(Response::with((status::Ok, path))) + let accept_range_header = Header(AcceptRanges(vec![RangeUnit::Bytes])); + let range_req_header = req.headers.get::().map(|h|{ + h.clone() + }); + match range_req_header { + None => { + // deliver the whole file + let path: &Path = &path; + Ok(Response::with((status::Ok, path, accept_range_header))) + }, + Some(range) => { + // try to deliver partial content + match range { + Range::Bytes(vec_range) => { + let partial_file = PartialFile::from_path(&path, vec_range); + Ok(Response::with((status::Ok, partial_file, accept_range_header))) + }, + _ => Ok(Response::with(status::RangeNotSatisfiable)) + } + } + } }, } } @@ -190,7 +222,6 @@ impl Cache { use iron::headers::{ContentLength, ContentType, ETag, EntityTag}; use iron::method::Method; use iron::mime::{Mime, TopLevel, SubLevel}; - use iron::modifiers::Header; let seconds = self.duration.as_secs() as u32; let cache = vec![CacheDirective::Public, CacheDirective::MaxAge(seconds)]; @@ -206,9 +237,17 @@ impl Cache { }; Response::with((status::Ok, Header(cont_type), Header(ContentLength(metadata.len())))) } else { - Response::with((status::Ok, path.as_ref())) + match req.headers.get::() { + None => Response::with((status::Ok, path.as_ref())), + Some(&Range::Bytes(ref v)) => { + let partial_file = PartialFile::from_path(path.as_ref(),v.clone()); + Response::with(partial_file) + }, + Some(_) => Response::with(status::RangeNotSatisfiable), + } }; + response.headers.set(AcceptRanges(vec![RangeUnit::Bytes])); response.headers.set(CacheControl(cache)); response.headers.set(LastModified(HttpDate(time::at(modified)))); response.headers.set(ETag(EntityTag::weak(format!("{0:x}-{1:x}.{2:x}", size, modified.sec, modified.nsec))));