diff --git a/src/api.rs b/src/api.rs index 318bd01..987524d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,6 +1,6 @@ use diesel::prelude::*; use iron::prelude::*; -use iron::headers::{Authorization, Basic}; +use iron::headers::{Authorization, Basic, Range}; use iron::{AroundMiddleware, Handler, status}; use mount::Mount; use router::Router; @@ -25,6 +25,7 @@ use errors::*; use index; use playlist; use user; +use serve; use thumbnails::*; use utils::*; use vfs::VFSSource; @@ -443,7 +444,8 @@ fn serve(request: &mut Request, db: &DB) -> IronResult { } if is_song(real_path.as_path()) { - return Ok(Response::with((status::Ok, real_path))); + let range_header = request.headers.get::(); + return serve::deliver(&real_path, range_header); } if is_image(real_path.as_path()) { diff --git a/src/errors.rs b/src/errors.rs index cb9a46d..c63362e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -45,6 +45,7 @@ error_chain! { IncorrectCredentials {} CannotServeDirectory {} UnsupportedFileType {} + FileNotFound {} MissingIndexVersion {} } } diff --git a/src/main.rs b/src/main.rs index e98ffd6..caa5988 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,6 +73,7 @@ mod playlist; mod ui; mod user; mod utils; +mod serve; mod thumbnails; mod vfs; @@ -90,8 +91,8 @@ fn main() { } } - #[cfg(unix)] -fn daemonize(options : &getopts::Matches) -> Result<()> { +#[cfg(unix)] +fn daemonize(options: &getopts::Matches) -> Result<()> { if options.opt_present("f") { return Ok(()); } @@ -112,7 +113,7 @@ fn run() -> Result<()> { options.optopt("p", "port", "set polaris to run on a custom port", "PORT"); options.optopt("d", "database", "set the path to index database", "FILE"); options.optopt("w", "web", "set the path to web client files", "DIRECTORY"); - + #[cfg(unix)] options.optflag("f", "foreground", diff --git a/src/serve.rs b/src/serve.rs new file mode 100644 index 0000000..b27a7cd --- /dev/null +++ b/src/serve.rs @@ -0,0 +1,169 @@ +use std::cmp; +use std::fs::{self, File}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::path::Path; +use iron::headers::{AcceptRanges, ByteRangeSpec, ContentLength, ContentRange, ContentRangeSpec, + Range, RangeUnit}; +use iron::modifier::Modifier; +use iron::modifiers::Header; +use iron::prelude::*; +use iron::response::WriteBody; +use iron::status::{self, Status}; + +use errors::{Error, ErrorKind}; + +pub fn deliver(path: &Path, range_header: Option<&Range>) -> IronResult { + + match fs::metadata(path) { + Ok(meta) => meta, + Err(e) => { + let status = match e.kind() { + io::ErrorKind::NotFound => status::NotFound, + io::ErrorKind::PermissionDenied => status::Forbidden, + _ => status::InternalServerError, + }; + return Err(IronError::new(e, status)); + } + }; + + let accept_range_header = Header(AcceptRanges(vec![RangeUnit::Bytes])); + let range_header = range_header.map(|h| h.clone()); + + match range_header { + None => Ok(Response::with((status::Ok, path, accept_range_header))), + Some(range) => { + match range { + Range::Bytes(vec_range) => { + if let Ok(partial_file) = PartialFile::from_path(path, vec_range) { + Ok(Response::with((status::Ok, partial_file, accept_range_header))) + } else { + Err(Error::from(ErrorKind::FileNotFound).into()) + } + } + _ => Ok(Response::with(status::RangeNotSatisfiable)), + } + } + } +} + +pub enum PartialFileRange { + AllFrom(u64), + FromTo(u64, u64), + Last(u64), +} + +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), + } + } +} + +pub struct PartialFile { + file: File, + range: PartialFileRange, +} + +impl From> for PartialFileRange { + fn from(v: Vec) -> PartialFileRange { + match v.into_iter().next() { + None => PartialFileRange::AllFrom(0), + Some(byte_range) => PartialFileRange::from(byte_range), + } + } +} + +impl PartialFile { + pub fn new(file: File, range: Range) -> PartialFile + where Range: Into + { + let range = range.into(); + PartialFile { + file: file, + range: range, + } + } + + pub fn from_path, Range>(path: P, range: Range) -> Result + where Range: Into + { + let file = File::open(path.as_ref())?; + Ok(Self::new(file, range)) + } +} + + +impl Modifier for PartialFile { + 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 && 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); + } + } +} + +struct PartialContentBody { + pub file: File, + pub offset: u64, + pub len: u64, +} + +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(|_| ()) + } +}