replace resource.rs with new http-entity crate
This crate is a slightly-more-polished and MIT-licensed version of resource.rs. So far it has one advantage: running the tests doesn't require RUST_TEST_THREADS=1.
This commit is contained in:
parent
86dd36d7a5
commit
fee4141dc6
|
@ -9,6 +9,7 @@ dependencies = [
|
|||
"ffmpeg 0.2.0-alpha.2 (git+https://github.com/scottlamb/rust-ffmpeg?branch=2.x)",
|
||||
"ffmpeg-sys 2.8.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"http-entity 0.0.1 (git+https://github.com/scottlamb/http-entity)",
|
||||
"hyper 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -186,6 +187,17 @@ dependencies = [
|
|||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-entity"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/scottlamb/http-entity#49e13e116ffc84f518f39c8a2e57d8066a1f3387"
|
||||
dependencies = [
|
||||
"hyper 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.2.0"
|
||||
|
@ -833,6 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
"checksum gcc 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)" = "771e4a97ff6f237cf0f7d5f5102f6e28bb9743814b6198d684da5c58b76c11e0"
|
||||
"checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518"
|
||||
"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58"
|
||||
"checksum http-entity 0.0.1 (git+https://github.com/scottlamb/http-entity)" = "<none>"
|
||||
"checksum httparse 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8abece705b1d32c478f49447b3a575cd07f6e362ff12518f2ee2c9b9ced64e"
|
||||
"checksum hyper 0.9.13 (registry+https://github.com/rust-lang/crates.io-index)" = "86ea0c0ff7e6ef09eff72234800ddb48b6263277936e7ecd6ecd3250345d705f"
|
||||
"checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11"
|
||||
|
|
|
@ -13,6 +13,7 @@ chan = "0.1"
|
|||
chan-signal = "0.1"
|
||||
docopt = "0.6"
|
||||
fnv = "1.0"
|
||||
http-entity = { git = "https://github.com/scottlamb/http-entity" }
|
||||
hyper = "0.9"
|
||||
lazy_static = "0.2"
|
||||
libc = "0.2"
|
||||
|
|
|
@ -139,7 +139,7 @@ For instructions, you can skip to "[Camera configuration and hard disk mounting]
|
|||
|
||||
Once prerequisites are installed, Moonfire NVR can be built as follows:
|
||||
|
||||
$ RUST_TEST_THREADS=1 cargo test
|
||||
$ cargo test
|
||||
$ cargo build --release
|
||||
$ sudo install -m 755 target/release/moonfire-nvr /usr/local/bin
|
||||
|
||||
|
|
2
prep.sh
2
prep.sh
|
@ -158,7 +158,7 @@ if [ ! -x "${SERVICE_BIN}" ]; then
|
|||
echo
|
||||
exit 1
|
||||
fi
|
||||
if ! RUST_TEST_THREADS=1 cargo test; then
|
||||
if ! cargo test; then
|
||||
echo "test failed. Try to run the following manually for more info"
|
||||
echo "RUST_TEST_THREADS=1 cargo test --verbose"
|
||||
echo
|
||||
|
|
|
@ -38,6 +38,7 @@ extern crate docopt;
|
|||
#[macro_use] extern crate ffmpeg;
|
||||
extern crate ffmpeg_sys;
|
||||
extern crate fnv;
|
||||
extern crate http_entity;
|
||||
extern crate hyper;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate libc;
|
||||
|
@ -76,7 +77,6 @@ mod mmapfile;
|
|||
mod mp4;
|
||||
mod pieces;
|
||||
mod recording;
|
||||
mod resource;
|
||||
mod stream;
|
||||
mod streamer;
|
||||
mod strutil;
|
||||
|
|
|
@ -38,7 +38,7 @@ use std::io;
|
|||
use std::ops::Range;
|
||||
|
||||
/// Memory-mapped file slice.
|
||||
/// This struct is meant to be used in constructing an implementation of the `resource::Resource`
|
||||
/// This struct is meant to be used in constructing an implementation of the `http_entity::Entity`
|
||||
/// or `pieces::ContextWriter` traits. The file in question should be immutable, as files shrinking
|
||||
/// during `mmap` will cause the process to fail with `SIGBUS`. Moonfire NVR sample files satisfy
|
||||
/// this requirement:
|
||||
|
|
17
src/mp4.rs
17
src/mp4.rs
|
@ -83,6 +83,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
|||
use db;
|
||||
use dir;
|
||||
use error::{Error, Result};
|
||||
use http_entity;
|
||||
use hyper::header;
|
||||
use mmapfile;
|
||||
use mime;
|
||||
|
@ -91,7 +92,6 @@ use pieces;
|
|||
use pieces::ContextWriter;
|
||||
use pieces::Slices;
|
||||
use recording::{self, TIME_UNITS_PER_SEC};
|
||||
use resource;
|
||||
use smallvec::SmallVec;
|
||||
use std::cell::RefCell;
|
||||
use std::cmp;
|
||||
|
@ -1153,7 +1153,7 @@ impl Mp4File {
|
|||
}
|
||||
}
|
||||
|
||||
impl resource::Resource for Mp4File {
|
||||
impl http_entity::Entity<Error> for Mp4File {
|
||||
fn content_type(&self) -> mime::Mime { "video/mp4".parse().unwrap() }
|
||||
fn last_modified(&self) -> &header::HttpDate { &self.last_modified }
|
||||
fn etag(&self) -> Option<&header::EntityTag> { Some(&self.etag) }
|
||||
|
@ -1181,11 +1181,12 @@ mod tests {
|
|||
use db;
|
||||
use dir;
|
||||
use ffmpeg;
|
||||
use error::Error;
|
||||
#[cfg(nightly)] use hyper;
|
||||
use hyper::header;
|
||||
use openssl::crypto::hash;
|
||||
use recording::{self, TIME_UNITS_PER_SEC};
|
||||
use resource::{self, Resource};
|
||||
use http_entity::{self, Entity};
|
||||
#[cfg(nightly)] use self::test::Bencher;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
|
@ -1217,7 +1218,7 @@ mod tests {
|
|||
}
|
||||
|
||||
/// Returns the SHA-1 digest of the given `Resource`.
|
||||
fn digest(r: &Resource) -> Vec<u8> {
|
||||
fn digest(r: &http_entity::Entity<Error>) -> Vec<u8> {
|
||||
let mut sha1 = Sha1::new();
|
||||
r.write_to(0 .. r.len(), &mut sha1).unwrap();
|
||||
sha1.finish()
|
||||
|
@ -1236,12 +1237,12 @@ mod tests {
|
|||
/// stack, not backward. Panics on error.
|
||||
#[derive(Clone)]
|
||||
struct BoxCursor<'a> {
|
||||
mp4: &'a resource::Resource,
|
||||
mp4: &'a http_entity::Entity<Error>,
|
||||
stack: Vec<Mp4Box>,
|
||||
}
|
||||
|
||||
impl<'a> BoxCursor<'a> {
|
||||
pub fn new(mp4: &'a resource::Resource) -> BoxCursor<'a> {
|
||||
pub fn new(mp4: &'a http_entity::Entity<Error>) -> BoxCursor<'a> {
|
||||
BoxCursor{
|
||||
mp4: mp4,
|
||||
stack: Vec::new(),
|
||||
|
@ -1346,7 +1347,7 @@ mod tests {
|
|||
|
||||
/// Finds the `moov/trak` that has a `tkhd` associated with the given `track_id`, which must
|
||||
/// exist.
|
||||
fn find_track(mp4: & resource::Resource, track_id: u32) -> Track {
|
||||
fn find_track(mp4: &http_entity::Entity<Error>, track_id: u32) -> Track {
|
||||
let mut cursor = BoxCursor::new(mp4);
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"moov"));
|
||||
|
@ -1766,7 +1767,7 @@ mod tests {
|
|||
let (db, dir) = (db.db.clone(), db.dir.clone());
|
||||
let _ = server.handle(move |req: Request, res: Response<Fresh>| {
|
||||
let mp4 = create_mp4_from_db(db.clone(), dir.clone(), 0, 0, false);
|
||||
resource::serve(&mp4, &req, res).unwrap();
|
||||
http_entity::serve(&mp4, &req, res).unwrap();
|
||||
});
|
||||
});
|
||||
BenchServer{
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tools for implementing a `resource::Resource` body composed from many "slices".
|
||||
//! Tools for implementing a `http_entity::Entity` body composed from many "slices".
|
||||
|
||||
use error::{Error, Result};
|
||||
use std::fmt;
|
||||
|
@ -50,7 +50,7 @@ struct SliceInfo<W> {
|
|||
/// Writes a byte range to the given `io::Write` given a context argument; meant for use with
|
||||
/// `Slices`.
|
||||
pub trait ContextWriter<Ctx> {
|
||||
/// Writes `r` to `out`, as in `resource::Resource::write_to`.
|
||||
/// Writes `r` to `out`, as in `http_entity::Entity::write_to`.
|
||||
/// The additional argument `ctx` is as supplied to the `Slices`.
|
||||
/// The additional argument `l` is the length of this slice, as determined by the `Slices`.
|
||||
fn write_to(&self, ctx: &Ctx, r: Range<u64>, l: u64, out: &mut io::Write) -> Result<()>;
|
||||
|
@ -122,7 +122,7 @@ impl<W, C> Slices<W, C> where W: ContextWriter<C> {
|
|||
pub fn num(&self) -> usize { self.slices.len() }
|
||||
|
||||
/// Writes `range` to `out`.
|
||||
/// This interface mirrors `resource::Resource::write_to`, with the additional `ctx` argument.
|
||||
/// This interface mirrors `http_entity::Entity::write_to`, with the additional `ctx` argument.
|
||||
pub fn write_to(&self, ctx: &C, range: Range<u64>, out: &mut io::Write) -> Result<()> {
|
||||
if range.start > range.end || range.end > self.len {
|
||||
return Err(Error{
|
||||
|
|
760
src/resource.rs
760
src/resource.rs
|
@ -1,760 +0,0 @@
|
|||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// In addition, as a special exception, the copyright holders give
|
||||
// permission to link the code of portions of this program with the
|
||||
// OpenSSL library under certain conditions as described in each
|
||||
// individual source file, and distribute linked combinations including
|
||||
// the two.
|
||||
//
|
||||
// You must obey the GNU General Public License in all respects for all
|
||||
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||
// exception, you may extend this exception to your version of the
|
||||
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||
// so, delete this exception statement from your version. If you delete
|
||||
// this exception statement from all source files in the program, then
|
||||
// also delete it here.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
extern crate core;
|
||||
extern crate hyper;
|
||||
extern crate time;
|
||||
|
||||
use error::Result;
|
||||
use hyper::server::{Request, Response};
|
||||
use hyper::header;
|
||||
use hyper::method::Method;
|
||||
use hyper::net::Fresh;
|
||||
use mime;
|
||||
use smallvec::SmallVec;
|
||||
use std::cmp;
|
||||
use std::io;
|
||||
use std::ops::Range;
|
||||
|
||||
/// An HTTP resource for GET and HEAD serving.
|
||||
pub trait Resource {
|
||||
/// Returns the length of the slice in bytes.
|
||||
fn len(&self) -> u64;
|
||||
|
||||
/// Writes bytes within this slice indicated by `range` to `out.`
|
||||
/// TODO: different result type?
|
||||
fn write_to(&self, range: Range<u64>, out: &mut io::Write) -> Result<()>;
|
||||
|
||||
fn content_type(&self) -> mime::Mime;
|
||||
fn etag(&self) -> Option<&header::EntityTag>;
|
||||
fn last_modified(&self) -> &header::HttpDate;
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum ResolvedRanges {
|
||||
AbsentOrInvalid,
|
||||
NotSatisfiable,
|
||||
Satisfiable(SmallVec<[Range<u64>; 1]>)
|
||||
}
|
||||
|
||||
fn parse_range_header(range: Option<&header::Range>, resource_len: u64) -> ResolvedRanges {
|
||||
if let Some(&header::Range::Bytes(ref byte_ranges)) = range {
|
||||
let mut ranges: SmallVec<[Range<u64>; 1]> = SmallVec::new();
|
||||
for range in byte_ranges {
|
||||
match *range {
|
||||
header::ByteRangeSpec::FromTo(range_from, range_to) => {
|
||||
let end = cmp::min(range_to + 1, resource_len);
|
||||
if range_from >= end {
|
||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
||||
continue;
|
||||
}
|
||||
ranges.push(Range{start: range_from, end: end});
|
||||
},
|
||||
header::ByteRangeSpec::AllFrom(range_from) => {
|
||||
if range_from >= resource_len {
|
||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
||||
continue;
|
||||
}
|
||||
ranges.push(Range{start: range_from, end: resource_len});
|
||||
},
|
||||
header::ByteRangeSpec::Last(last) => {
|
||||
if last >= resource_len {
|
||||
debug!("Range {:?} not satisfiable with length {:?}", range, resource_len);
|
||||
continue;
|
||||
}
|
||||
ranges.push(Range{start: resource_len - last,
|
||||
end: resource_len});
|
||||
},
|
||||
}
|
||||
}
|
||||
if !ranges.is_empty() {
|
||||
debug!("Ranges {:?} all satisfiable with length {:?}", range, resource_len);
|
||||
return ResolvedRanges::Satisfiable(ranges);
|
||||
}
|
||||
return ResolvedRanges::NotSatisfiable;
|
||||
}
|
||||
ResolvedRanges::AbsentOrInvalid
|
||||
}
|
||||
|
||||
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
|
||||
fn none_match(etag: Option<&header::EntityTag>, req: &Request) -> bool {
|
||||
match req.headers.get::<header::IfNoneMatch>() {
|
||||
Some(&header::IfNoneMatch::Any) => false,
|
||||
Some(&header::IfNoneMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
if item.weak_eq(some_etag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
||||
fn any_match(etag: Option<&header::EntityTag>, req: &Request) -> bool {
|
||||
match req.headers.get::<header::IfMatch>() {
|
||||
// The absent header and "If-Match: *" cases differ only when there is no entity to serve.
|
||||
// We always have an entity to serve, so consider them identical.
|
||||
None | Some(&header::IfMatch::Any) => true,
|
||||
Some(&header::IfMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
if item.strong_eq(some_etag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves GET and HEAD requests for a given byte-ranged resource.
|
||||
/// Handles conditional & subrange requests.
|
||||
/// The caller is expected to have already determined the correct resource and appended
|
||||
/// Expires, Cache-Control, and Vary headers.
|
||||
///
|
||||
/// TODO: is it appropriate to include those headers on all response codes used in this function?
|
||||
///
|
||||
/// TODO: check HTTP rules about weak vs strong comparisons with range requests. I don't think I'm
|
||||
/// doing this correctly.
|
||||
pub fn serve(rsrc: &Resource, req: &Request, mut res: Response<Fresh>) -> Result<()> {
|
||||
if req.method != Method::Get && req.method != Method::Head {
|
||||
*res.status_mut() = hyper::status::StatusCode::MethodNotAllowed;
|
||||
res.headers_mut().set(header::ContentType(mime!(Text/Plain)));
|
||||
res.headers_mut().set(header::Allow(vec![Method::Get, Method::Head]));
|
||||
res.send(b"This resource only supports GET and HEAD.")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let last_modified = rsrc.last_modified();
|
||||
let etag = rsrc.etag();
|
||||
res.headers_mut().set(header::AcceptRanges(vec![header::RangeUnit::Bytes]));
|
||||
res.headers_mut().set(header::LastModified(*last_modified));
|
||||
if let Some(some_etag) = etag {
|
||||
res.headers_mut().set(header::ETag(some_etag.clone()));
|
||||
}
|
||||
|
||||
if let Some(&header::IfUnmodifiedSince(ref since)) = req.headers.get() {
|
||||
if last_modified.0.to_timespec() > since.0.to_timespec() {
|
||||
*res.status_mut() = hyper::status::StatusCode::PreconditionFailed;
|
||||
res.send(b"Precondition failed")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if !any_match(etag, req) {
|
||||
*res.status_mut() = hyper::status::StatusCode::PreconditionFailed;
|
||||
res.send(b"Precondition failed")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !none_match(etag, req) {
|
||||
*res.status_mut() = hyper::status::StatusCode::NotModified;
|
||||
res.send(b"")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(&header::IfModifiedSince(ref since)) = req.headers.get() {
|
||||
if last_modified <= since {
|
||||
*res.status_mut() = hyper::status::StatusCode::NotModified;
|
||||
res.send(b"")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let mut range_hdr = req.headers.get::<header::Range>();
|
||||
|
||||
// See RFC 2616 section 10.2.7: a Partial Content response should include certain
|
||||
// entity-headers or not based on the If-Range response.
|
||||
let include_entity_headers_on_range = match req.headers.get::<header::IfRange>() {
|
||||
Some(&header::IfRange::EntityTag(ref if_etag)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
if if_etag.strong_eq(some_etag) {
|
||||
false
|
||||
} else {
|
||||
range_hdr = None;
|
||||
true
|
||||
}
|
||||
} else {
|
||||
range_hdr = None;
|
||||
true
|
||||
}
|
||||
},
|
||||
Some(&header::IfRange::Date(ref if_date)) => {
|
||||
// The to_timespec conversion appears necessary because in the If-Range off the wire,
|
||||
// fields such as tm_yday are absent, causing strict equality to spuriously fail.
|
||||
if if_date.0.to_timespec() != last_modified.0.to_timespec() {
|
||||
range_hdr = None;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
None => true,
|
||||
};
|
||||
let len = rsrc.len();
|
||||
let (range, include_entity_headers) = match parse_range_header(range_hdr, len) {
|
||||
ResolvedRanges::AbsentOrInvalid => (0 .. len, true),
|
||||
ResolvedRanges::Satisfiable(rs) => {
|
||||
if rs.len() == 1 {
|
||||
res.headers_mut().set(header::ContentRange(
|
||||
header::ContentRangeSpec::Bytes{
|
||||
range: Some((rs[0].start, rs[0].end-1)),
|
||||
instance_length: Some(len)}));
|
||||
*res.status_mut() = hyper::status::StatusCode::PartialContent;
|
||||
(rs[0].clone(), include_entity_headers_on_range)
|
||||
} else {
|
||||
// Ignore multi-part range headers for now. They require additional complexity, and
|
||||
// I don't see clients sending them in the wild.
|
||||
(0 .. len, true)
|
||||
}
|
||||
},
|
||||
ResolvedRanges::NotSatisfiable => {
|
||||
res.headers_mut().set(header::ContentRange(
|
||||
header::ContentRangeSpec::Bytes{
|
||||
range: None,
|
||||
instance_length: Some(len)}));
|
||||
*res.status_mut() = hyper::status::StatusCode::RangeNotSatisfiable;
|
||||
res.send(b"")?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
if include_entity_headers {
|
||||
res.headers_mut().set(header::ContentType(rsrc.content_type()));
|
||||
}
|
||||
res.headers_mut().set(header::ContentLength(range.end - range.start));
|
||||
let mut stream = res.start()?;
|
||||
if req.method == Method::Get {
|
||||
rsrc.write_to(range, &mut stream)?;
|
||||
}
|
||||
stream.end()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use error::Result;
|
||||
use hyper;
|
||||
use hyper::header::{self, ByteRangeSpec, ContentRangeSpec, EntityTag};
|
||||
use hyper::header::Range::Bytes;
|
||||
use mime;
|
||||
use smallvec::SmallVec;
|
||||
use std::io::{Read, Write};
|
||||
use std::ops::Range;
|
||||
use std::sync::Mutex;
|
||||
use super::{ResolvedRanges, parse_range_header};
|
||||
use super::*;
|
||||
use testutil;
|
||||
use time;
|
||||
|
||||
/// Tests the specific examples enumerated in RFC 2616 section 14.35.1.
|
||||
#[test]
|
||||
fn test_resolve_ranges_rfc() {
|
||||
let mut v = SmallVec::new();
|
||||
|
||||
v.push(0 .. 500);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 499)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(500 .. 1000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 999)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(9500 .. 10000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::Last(500)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(9500 .. 10000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(9500)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(0 .. 1);
|
||||
v.push(9999 .. 10000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0),
|
||||
ByteRangeSpec::Last(1)])),
|
||||
10000));
|
||||
|
||||
// Non-canonical ranges. Possibly the point of these is that the adjacent and overlapping
|
||||
// ranges are supposed to be coalesced into one? I'm not going to do that for now.
|
||||
|
||||
v.clear();
|
||||
v.push(500 .. 601);
|
||||
v.push(601 .. 1000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 600),
|
||||
ByteRangeSpec::FromTo(601, 999)])),
|
||||
10000));
|
||||
|
||||
v.clear();
|
||||
v.push(500 .. 701);
|
||||
v.push(601 .. 1000);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(500, 700),
|
||||
ByteRangeSpec::FromTo(601, 999)])),
|
||||
10000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_ranges_satisfiability() {
|
||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(10000)])),
|
||||
10000));
|
||||
|
||||
let mut v = SmallVec::new();
|
||||
v.push(0 .. 500);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 499),
|
||||
ByteRangeSpec::AllFrom(10000)])),
|
||||
10000));
|
||||
|
||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::Last(1)])), 0));
|
||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0)])), 0));
|
||||
assert_eq!(ResolvedRanges::NotSatisfiable,
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::AllFrom(0)])), 0));
|
||||
|
||||
v.clear();
|
||||
v.push(0 .. 1);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 0)])), 1));
|
||||
|
||||
v.clear();
|
||||
v.push(0 .. 500);
|
||||
assert_eq!(ResolvedRanges::Satisfiable(v.clone()),
|
||||
parse_range_header(Some(&Bytes(vec![ByteRangeSpec::FromTo(0, 10000)])),
|
||||
500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_ranges_absent_or_invalid() {
|
||||
assert_eq!(ResolvedRanges::AbsentOrInvalid, parse_range_header(None, 10000));
|
||||
}
|
||||
|
||||
struct FakeResource {
|
||||
etag: Option<EntityTag>,
|
||||
mime: mime::Mime,
|
||||
last_modified: header::HttpDate,
|
||||
body: &'static [u8],
|
||||
}
|
||||
|
||||
impl Resource for FakeResource {
|
||||
fn len(&self) -> u64 { self.body.len() as u64 }
|
||||
fn write_to(&self, range: Range<u64>, out: &mut Write) -> Result<()> {
|
||||
Ok(out.write_all(&self.body[range.start as usize .. range.end as usize])?)
|
||||
}
|
||||
fn content_type(&self) -> mime::Mime { self.mime.clone() }
|
||||
fn etag(&self) -> Option<&EntityTag> { self.etag.as_ref() }
|
||||
fn last_modified(&self) -> &header::HttpDate { &self.last_modified }
|
||||
}
|
||||
|
||||
fn new_server() -> String {
|
||||
let mut listener = hyper::net::HttpListener::new("127.0.0.1:0").unwrap();
|
||||
use hyper::net::NetworkListener;
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let server = hyper::Server::new(listener);
|
||||
use std::thread::spawn;
|
||||
spawn(move || {
|
||||
use hyper::server::{Request, Response, Fresh};
|
||||
let _ = server.handle(move |req: Request, res: Response<Fresh>| {
|
||||
let l = RESOURCE.lock().unwrap();
|
||||
let resource = l.as_ref().unwrap();
|
||||
serve(resource, &req, res).unwrap();
|
||||
});
|
||||
});
|
||||
format!("http://{}:{}/", addr.ip(), addr.port())
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref RESOURCE: Mutex<Option<FakeResource>> = { Mutex::new(None) };
|
||||
static ref SERVER: String = { new_server() };
|
||||
static ref SOME_DATE: header::HttpDate = {
|
||||
header::HttpDate(time::at_utc(time::Timespec::new(1430006400i64, 0)))
|
||||
};
|
||||
static ref LATER_DATE: header::HttpDate = {
|
||||
header::HttpDate(time::at_utc(time::Timespec::new(1430092800i64, 0)))
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_without_etag() {
|
||||
testutil::init();
|
||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
||||
etag: None,
|
||||
mime: mime!(Application/OctetStream),
|
||||
last_modified: *SOME_DATE,
|
||||
body: b"01234",
|
||||
});
|
||||
let client = hyper::Client::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
// Full body.
|
||||
let mut resp = client.get(&*SERVER).send().unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// If-Match any should still send the full body.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(header::IfMatch::Any)
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// If-Match by etag doesn't match (as this request has no etag).
|
||||
let resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PreconditionFailed, resp.status);
|
||||
|
||||
// If-None-Match any.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Any)
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// If-None-Match by etag doesn't match (as this request has no etag).
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Unmodified since supplied date.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(header::IfModifiedSince(*SOME_DATE))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// Range serving - basic case.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
||||
range: Some((1, 3)),
|
||||
instance_length: Some(5),
|
||||
})), resp.headers.get());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"123", &buf[..]);
|
||||
|
||||
// Range serving - multiple ranges. Currently falls back to whole range.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(0, 1),
|
||||
ByteRangeSpec::FromTo(3, 4)]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Range serving - not satisfiable.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::AllFrom(500)]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::RangeNotSatisfiable, resp.status);
|
||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
||||
range: None,
|
||||
instance_length: Some(5),
|
||||
})), resp.headers.get());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// Range serving - matching If-Range by date honors the range.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::Date(*SOME_DATE))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
||||
range: Some((1, 3)),
|
||||
instance_length: Some(5),
|
||||
})), resp.headers.get());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"123", &buf[..]);
|
||||
|
||||
// Range serving - non-matching If-Range by date ignores the range.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::Date(*LATER_DATE))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Range serving - this resource has no etag, so any If-Range by etag ignores the range.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::EntityTag(EntityTag::strong("foo".to_owned())))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_with_strong_etag() {
|
||||
testutil::init();
|
||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
||||
etag: Some(EntityTag::strong("foo".to_owned())),
|
||||
mime: mime!(Application/OctetStream),
|
||||
last_modified: *SOME_DATE,
|
||||
body: b"01234",
|
||||
});
|
||||
let client = hyper::Client::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
// If-Match any should still send the full body.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(header::IfMatch::Any)
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// If-Match by matching etag should send the full body.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// If-Match by etag which doesn't match.
|
||||
let resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfMatch::Items(vec![EntityTag::strong("bar".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PreconditionFailed, resp.status);
|
||||
|
||||
// If-None-Match by etag which matches.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// If-None-Match by etag which doesn't match.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::strong("bar".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Range serving - If-Range matching by etag.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::EntityTag(EntityTag::strong("foo".to_owned())))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PartialContent, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(Some(&header::ContentRange(ContentRangeSpec::Bytes{
|
||||
range: Some((1, 3)),
|
||||
instance_length: Some(5),
|
||||
})), resp.headers.get());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"123", &buf[..]);
|
||||
|
||||
// Range serving - If-Range not matching by etag.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::EntityTag(EntityTag::strong("bar".to_owned())))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_with_weak_etag() {
|
||||
testutil::init();
|
||||
*RESOURCE.lock().unwrap() = Some(FakeResource{
|
||||
etag: Some(EntityTag::weak("foo".to_owned())),
|
||||
mime: mime!(Application/OctetStream),
|
||||
last_modified: *SOME_DATE,
|
||||
body: b"01234",
|
||||
});
|
||||
let client = hyper::Client::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
// If-Match any should still send the full body.
|
||||
let mut resp = client.get(&*SERVER)
|
||||
.header(header::IfMatch::Any)
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// If-Match by etag doesn't match because matches use the strong comparison function.
|
||||
let resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfMatch::Items(vec![EntityTag::weak("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::PreconditionFailed, resp.status);
|
||||
|
||||
// If-None-Match by identical weak etag is sufficient.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::weak("foo".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::NotModified, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"", &buf[..]);
|
||||
|
||||
// If-None-Match by etag which doesn't match.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(header::IfNoneMatch::Items(vec![EntityTag::weak("bar".to_owned())]))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
|
||||
// Range serving - If-Range matching by weak etag isn't sufficient.
|
||||
let mut resp =
|
||||
client.get(&*SERVER)
|
||||
.header(Bytes(vec![ByteRangeSpec::FromTo(1, 3)]))
|
||||
.header(header::IfRange::EntityTag(EntityTag::weak("foo".to_owned())))
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(hyper::status::StatusCode::Ok, resp.status);
|
||||
assert_eq!(Some(&header::ContentType(mime!(Application/OctetStream))),
|
||||
resp.headers.get::<header::ContentType>());
|
||||
assert_eq!(None, resp.headers.get::<header::ContentRange>());
|
||||
buf.clear();
|
||||
resp.read_to_end(&mut buf).unwrap();
|
||||
assert_eq!(b"01234", &buf[..]);
|
||||
}
|
||||
}
|
|
@ -35,12 +35,12 @@ use core::str::FromStr;
|
|||
use db;
|
||||
use dir::SampleFileDir;
|
||||
use error::{Error, Result};
|
||||
use http_entity;
|
||||
use hyper::{header,server,status};
|
||||
use hyper::uri::RequestUri;
|
||||
use mime;
|
||||
use mp4;
|
||||
use recording;
|
||||
use resource;
|
||||
use serde_json;
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
|
@ -416,7 +416,7 @@ impl Handler {
|
|||
}
|
||||
builder.include_timestamp_subtitle_track(include_ts);
|
||||
let mp4 = builder.build(self.db.clone(), self.dir.clone())?;
|
||||
resource::serve(&mp4, req, res)?;
|
||||
http_entity::serve(&mp4, req, res)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue