mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-03-13 21:12:55 -04:00
upgrade to hyper 0.13 ecosystem
This doesn't take much advantage of async fns so far. For example, the with_{form,json}_body functions are still designed to be used with future combinators when it'd be more natural to call them from async fns now. But it's a start. Similarly, this still uses the old version of reqwest. Small steps. Requires Rust 1.40 now. (1.39 is a requirement of async, and 1.40 is a requirement of http-serve 0.2.0.)
This commit is contained in:
parent
fce0c5b014
commit
8af7bca6c2
@ -23,7 +23,7 @@ matrix:
|
||||
script:
|
||||
- ci/script-rust.sh
|
||||
- language: rust
|
||||
rust: 1.36.0
|
||||
rust: 1.40.0
|
||||
script:
|
||||
- ci/script-rust.sh
|
||||
- language: node_js
|
||||
|
2724
Cargo.lock
generated
2724
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@ -20,7 +20,7 @@ members = ["base", "db", "ffmpeg"]
|
||||
[dependencies]
|
||||
base = { package = "moonfire-base", path = "base" }
|
||||
base64 = "0.10.0"
|
||||
bytes = "0.4.6"
|
||||
bytes = "0.5.3"
|
||||
byteorder = "1.0"
|
||||
cstr = "0.1.7"
|
||||
cursive = "0.12"
|
||||
@ -28,12 +28,11 @@ db = { package = "moonfire-db", path = "db" }
|
||||
docopt = "1.0"
|
||||
failure = "0.1.1"
|
||||
ffmpeg = { package = "moonfire-ffmpeg", path = "ffmpeg" }
|
||||
futures = "0.1"
|
||||
futures-cpupool = "0.1"
|
||||
futures = "0.3"
|
||||
fnv = "1.0"
|
||||
http = "0.1.5"
|
||||
http-serve = "0.1.0"
|
||||
hyper = "0.12.9"
|
||||
http = "0.2.0"
|
||||
http-serve = "0.2.0"
|
||||
hyper = "0.13.0"
|
||||
lazy_static = "1.0"
|
||||
libc = "0.2"
|
||||
log = { version = "0.4", features = ["release_max_level_info"] }
|
||||
@ -52,8 +51,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
smallvec = "0.6"
|
||||
time = "0.1"
|
||||
tokio = "0.1.8"
|
||||
tokio-signal = "0.2"
|
||||
tokio = { version = "0.2.0", features = ["blocking", "macros", "rt-threaded", "signal"] }
|
||||
url = "1.4"
|
||||
uuid = { version = "0.7", features = ["serde", "std", "v4"] }
|
||||
|
||||
|
@ -48,7 +48,7 @@ $ sudo apt-get install \
|
||||
tzdata
|
||||
```
|
||||
|
||||
Next, you need Rust 1.36+ and Cargo. The easiest way to install them is by
|
||||
Next, you need Rust 1.40+ and Cargo. The easiest way to install them is by
|
||||
following the instructions at [rustup.rs](https://www.rustup.rs/).
|
||||
|
||||
Finally, building the UI requires [yarn](https://yarnpkg.com/en/).
|
||||
|
@ -40,7 +40,7 @@ fi
|
||||
NODE_MIN_VERSION="8"
|
||||
YARN_MIN_VERSION="1.0"
|
||||
CARGO_MIN_VERSION="0.2"
|
||||
RUSTC_MIN_VERSION="1.36"
|
||||
RUSTC_MIN_VERSION="1.40"
|
||||
|
||||
normalizeDirPath()
|
||||
{
|
||||
|
52
src/body.rs
52
src/body.rs
@ -32,77 +32,81 @@
|
||||
|
||||
use base::Error;
|
||||
use futures::{Stream, stream};
|
||||
use hyper::body::Payload;
|
||||
use reffers::ARefs;
|
||||
use reffers::ARefss;
|
||||
use std::error::Error as StdError;
|
||||
use std::pin::Pin;
|
||||
|
||||
pub struct Chunk(ARefs<'static, [u8]>);
|
||||
pub struct Chunk(ARefss<'static, [u8]>);
|
||||
|
||||
//pub type CompatError = ::failure::Compat<Error>;
|
||||
pub type BoxedError = Box<dyn StdError + Send + Sync>;
|
||||
pub type BodyStream = Box<dyn Stream<Item = Chunk, Error = BoxedError> + Send + 'static>;
|
||||
pub type BodyStream = Box<dyn Stream<Item = Result<Chunk, BoxedError>> + Send + Sync + 'static>;
|
||||
|
||||
pub fn wrap_error(e: Error) -> BoxedError {
|
||||
Box::new(e.compat())
|
||||
}
|
||||
|
||||
impl From<ARefs<'static, [u8]>> for Chunk {
|
||||
fn from(r: ARefs<'static, [u8]>) -> Self { Chunk(r) }
|
||||
impl From<ARefss<'static, [u8]>> for Chunk {
|
||||
fn from(r: ARefss<'static, [u8]>) -> Self { Chunk(r) }
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for Chunk {
|
||||
fn from(r: &'static [u8]) -> Self { Chunk(ARefs::new(r)) }
|
||||
fn from(r: &'static [u8]) -> Self { Chunk(ARefss::new(r)) }
|
||||
}
|
||||
|
||||
impl From<&'static str> for Chunk {
|
||||
fn from(r: &'static str) -> Self { Chunk(ARefs::new(r.as_bytes())) }
|
||||
fn from(r: &'static str) -> Self { Chunk(ARefss::new(r.as_bytes())) }
|
||||
}
|
||||
|
||||
impl From<String> for Chunk {
|
||||
fn from(r: String) -> Self { Chunk(ARefs::new(r.into_bytes()).map(|v| &v[..])) }
|
||||
fn from(r: String) -> Self { Chunk(ARefss::new(r.into_bytes()).map(|v| &v[..])) }
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Chunk {
|
||||
fn from(r: Vec<u8>) -> Self { Chunk(ARefs::new(r).map(|v| &v[..])) }
|
||||
fn from(r: Vec<u8>) -> Self { Chunk(ARefss::new(r).map(|v| &v[..])) }
|
||||
}
|
||||
|
||||
impl ::bytes::Buf for Chunk {
|
||||
fn remaining(&self) -> usize { self.0.len() }
|
||||
fn bytes(&self) -> &[u8] { &*self.0 }
|
||||
fn advance(&mut self, cnt: usize) {
|
||||
self.0 = ::std::mem::replace(&mut self.0, ARefs::new(&[][..])).map(|b| &b[cnt..]);
|
||||
self.0 = ::std::mem::replace(&mut self.0, ARefss::new(&[][..])).map(|b| &b[cnt..]);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Body(BodyStream);
|
||||
pub struct Body(Pin<BodyStream>);
|
||||
|
||||
impl Payload for Body {
|
||||
impl hyper::body::HttpBody for Body {
|
||||
type Data = Chunk;
|
||||
type Error = BoxedError;
|
||||
|
||||
fn poll_data(&mut self) -> ::futures::Poll<Option<Self::Data>, Self::Error> {
|
||||
self.0.poll()
|
||||
fn poll_data(self: Pin<&mut Self>, cx: &mut std::task::Context)
|
||||
-> std::task::Poll<Option<Result<Self::Data, Self::Error>>> {
|
||||
// This is safe because the pin is not structural.
|
||||
// https://doc.rust-lang.org/std/pin/#pinning-is-not-structural-for-field
|
||||
// (The field _holds_ a pin, but isn't itself pinned.)
|
||||
unsafe { self.get_unchecked_mut() }.0.as_mut().poll_next(cx)
|
||||
//Pin::from(self.0).as_mut().poll_next(cx)
|
||||
}
|
||||
|
||||
fn poll_trailers(self: Pin<&mut Self>, _cx: &mut std::task::Context)
|
||||
-> std::task::Poll<Result<Option<http::header::HeaderMap>, Self::Error>> {
|
||||
std::task::Poll::Ready(Ok(None))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BodyStream> for Body {
|
||||
fn from(b: BodyStream) -> Self { Body(b) }
|
||||
fn from(b: BodyStream) -> Self { Body(Pin::from(b)) }
|
||||
}
|
||||
|
||||
impl<C: Into<Chunk>> From<C> for Body {
|
||||
fn from(c: C) -> Self {
|
||||
Body(Box::new(stream::once(Ok(c.into()))))
|
||||
Body(Box::pin(stream::once(futures::future::ok(c.into()))))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for Body {
|
||||
fn from(e: Error) -> Self {
|
||||
Body(Box::new(stream::once(Err(wrap_error(e)))))
|
||||
Body(Box::pin(stream::once(futures::future::err(wrap_error(e)))))
|
||||
}
|
||||
}
|
||||
|
||||
//impl<C: Into<Chunk>> From<C> for Body {
|
||||
// fn from(c: C) -> Self {
|
||||
// Body(Box::new(stream::once(Ok(c.into()))))
|
||||
// }
|
||||
//}
|
||||
|
@ -35,15 +35,16 @@ use crate::web;
|
||||
use db::{dir, writer};
|
||||
use failure::{Error, ResultExt, bail};
|
||||
use fnv::FnvHashMap;
|
||||
use futures::{Future, Stream};
|
||||
use log::{error, info, warn};
|
||||
use futures::future::FutureExt;
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use log::{info, warn};
|
||||
use serde::Deserialize;
|
||||
use std::error::Error as StdError;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use tokio;
|
||||
use tokio_signal::unix::{Signal, SIGINT, SIGTERM};
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
// These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles).
|
||||
// They seem to be correct for Linux and macOS at least.
|
||||
@ -92,14 +93,6 @@ struct Args {
|
||||
flag_trust_forward_hdrs: bool,
|
||||
}
|
||||
|
||||
fn setup_shutdown() -> impl Future<Item = (), Error = ()> + Send {
|
||||
let int = Signal::new(SIGINT).flatten_stream().into_future();
|
||||
let term = Signal::new(SIGTERM).flatten_stream().into_future();
|
||||
int.select(term)
|
||||
.map(|_| ())
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
fn trim_zoneinfo(p: &str) -> &str {
|
||||
for zp in &ZONEINFO_PATHS {
|
||||
if p.starts_with(zp) {
|
||||
@ -173,7 +166,8 @@ struct Syncer {
|
||||
join: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
#[tokio::main]
|
||||
pub async fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
let clocks = clock::RealClocks {};
|
||||
let (_db_dir, conn) = super::open_conn(
|
||||
@ -274,18 +268,29 @@ pub fn run() -> Result<(), Error> {
|
||||
|
||||
// Start the web interface.
|
||||
let addr = args.flag_http_addr.parse().unwrap();
|
||||
let server = ::hyper::server::Server::bind(&addr).tcp_nodelay(true).serve(
|
||||
move || Ok::<_, Box<dyn StdError + Send + Sync>>(s.clone()));
|
||||
let make_svc = make_service_fn(move |_conn| {
|
||||
futures::future::ok::<_, std::convert::Infallible>(service_fn({
|
||||
let mut s = s.clone();
|
||||
move |req| Pin::from(s.serve(req))
|
||||
}))
|
||||
});
|
||||
let server = ::hyper::server::Server::bind(&addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(make_svc);
|
||||
|
||||
let shutdown = setup_shutdown().shared();
|
||||
let mut int = signal(SignalKind::interrupt())?;
|
||||
let mut term = signal(SignalKind::terminate())?;
|
||||
let shutdown = futures::future::select(
|
||||
Box::pin(int.recv()),
|
||||
Box::pin(term.recv()));
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel();
|
||||
let server = server.with_graceful_shutdown(shutdown_rx.map(|_| ()));
|
||||
let server_handle = tokio::spawn(server);
|
||||
|
||||
info!("Ready to serve HTTP requests");
|
||||
let reactor = ::std::thread::spawn({
|
||||
let shutdown = shutdown.clone();
|
||||
|| tokio::run(server.with_graceful_shutdown(shutdown.map(|_| ()))
|
||||
.map_err(|e| error!("hyper error: {}", e)))
|
||||
});
|
||||
shutdown.wait().unwrap();
|
||||
shutdown.await;
|
||||
shutdown_tx.send(()).unwrap();
|
||||
|
||||
info!("Shutting down streamers.");
|
||||
shutdown_streamers.store(true, Ordering::SeqCst);
|
||||
@ -306,7 +311,7 @@ pub fn run() -> Result<(), Error> {
|
||||
db.lock().clear_watches();
|
||||
|
||||
info!("Waiting for HTTP requests to finish.");
|
||||
reactor.join().unwrap();
|
||||
server_handle.await??;
|
||||
info!("Exiting.");
|
||||
Ok(())
|
||||
}
|
||||
|
352
src/mp4.rs
352
src/mp4.rs
@ -91,7 +91,7 @@ use log::{debug, error, trace, warn};
|
||||
use memmap;
|
||||
use openssl::hash;
|
||||
use parking_lot::Once;
|
||||
use reffers::ARefs;
|
||||
use reffers::ARefss;
|
||||
use crate::slices::{self, Slices};
|
||||
use smallvec::SmallVec;
|
||||
use std::cell::UnsafeCell;
|
||||
@ -613,7 +613,7 @@ impl Slice {
|
||||
|
||||
fn wrap_index<F>(&self, mp4: &File, r: Range<u64>, f: &F) -> Result<Chunk, Error>
|
||||
where F: Fn(&[u8], SegmentLengths) -> &[u8] {
|
||||
let mp4 = ARefs::new(mp4.0.clone());
|
||||
let mp4 = ARefss::new(mp4.0.clone());
|
||||
let r = r.start as usize .. r.end as usize;
|
||||
let p = self.p();
|
||||
Ok(mp4.try_map(|mp4| Ok::<_, Error>(&mp4.segments[p].get_index(&mp4.db, f)?[r]))?.into())
|
||||
@ -630,7 +630,7 @@ impl Slice {
|
||||
mp4.0.db.lock()
|
||||
.with_recording_playback(s.s.id, &mut |playback| s.truns(playback, pos, len))
|
||||
.err_kind(ErrorKind::Unknown)?;
|
||||
let truns = ARefs::new(truns);
|
||||
let truns = ARefss::new(truns);
|
||||
Ok(truns.map(|t| &t[r.start as usize .. r.end as usize]).into())
|
||||
}
|
||||
}
|
||||
@ -641,7 +641,7 @@ impl slices::Slice for Slice {
|
||||
|
||||
fn end(&self) -> u64 { return self.0 & 0xFF_FF_FF_FF_FF }
|
||||
fn get_range(&self, f: &File, range: Range<u64>, len: u64)
|
||||
-> Box<dyn Stream<Item = Self::Chunk, Error = BoxedError> + Send> {
|
||||
-> Box<dyn Stream<Item = Result<Self::Chunk, BoxedError>> + Send + Sync> {
|
||||
trace!("getting mp4 slice {:?}'s range {:?} / {}", self, range, len);
|
||||
let p = self.p();
|
||||
let res = match self.t() {
|
||||
@ -651,11 +651,11 @@ impl slices::Slice for Slice {
|
||||
Ok(part.into())
|
||||
},
|
||||
SliceType::Buf => {
|
||||
let r = ARefs::new(f.0.clone());
|
||||
let r = ARefss::new(f.0.clone());
|
||||
Ok(r.map(|f| &f.buf[p+range.start as usize .. p+range.end as usize]).into())
|
||||
},
|
||||
SliceType::VideoSampleEntry => {
|
||||
let r = ARefs::new(f.0.clone());
|
||||
let r = ARefss::new(f.0.clone());
|
||||
Ok(r.map(|f| &f.video_sample_entries[p]
|
||||
.data[range.start as usize .. range.end as usize]).into())
|
||||
},
|
||||
@ -667,7 +667,7 @@ impl slices::Slice for Slice {
|
||||
SliceType::SubtitleSampleData => f.0.get_subtitle_sample_data(p, range.clone(), len),
|
||||
SliceType::Truns => self.wrap_truns(f, range.clone(), len as usize),
|
||||
};
|
||||
Box::new(stream::once(res
|
||||
Box::new(stream::once(futures::future::ready(res
|
||||
.map_err(|e| wrap_error(e))
|
||||
.and_then(move |c| {
|
||||
if c.remaining() != (range.end - range.start) as usize {
|
||||
@ -677,7 +677,7 @@ impl slices::Slice for Slice {
|
||||
self, range, c.remaining())));
|
||||
}
|
||||
Ok(c)
|
||||
})))
|
||||
}))))
|
||||
}
|
||||
|
||||
fn get_slices(ctx: &File) -> &Slices<Self> { &ctx.0.slices }
|
||||
@ -1444,7 +1444,7 @@ impl FileInner {
|
||||
let r = s.s.sample_file_range();
|
||||
pos += r.end - r.start;
|
||||
}
|
||||
Ok(ARefs::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
||||
Ok(ARefss::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
||||
}
|
||||
|
||||
/// Gets a `Chunk` of video sample data from disk.
|
||||
@ -1470,7 +1470,7 @@ impl FileInner {
|
||||
.map(&f).err_kind(ErrorKind::Internal)?
|
||||
});
|
||||
use core::ops::Deref;
|
||||
Ok(ARefs::new(mmap).map(|m| m.deref()).into())
|
||||
Ok(ARefss::new(mmap).map(|m| m.deref()).into())
|
||||
}
|
||||
|
||||
fn get_subtitle_sample_data(&self, i: usize, r: Range<u64>, l: u64) -> Result<Chunk, Error> {
|
||||
@ -1487,7 +1487,7 @@ impl FileInner {
|
||||
write!(v, "{}", tm.strftime(SUBTITLE_TEMPLATE).err_kind(ErrorKind::Internal)?)
|
||||
.expect("Vec write shouldn't fail");
|
||||
}
|
||||
Ok(ARefs::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
||||
Ok(ARefss::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1512,13 +1512,13 @@ impl http_serve::Entity for File {
|
||||
}
|
||||
mime.extend_from_slice(b"\"");
|
||||
hdrs.insert(http::header::CONTENT_TYPE,
|
||||
http::header::HeaderValue::from_shared(mime.freeze()).unwrap());
|
||||
http::header::HeaderValue::from_maybe_shared(mime.freeze()).unwrap());
|
||||
}
|
||||
fn last_modified(&self) -> Option<SystemTime> { Some(self.0.last_modified) }
|
||||
fn etag(&self) -> Option<HeaderValue> { Some(self.0.etag.clone()) }
|
||||
fn len(&self) -> u64 { self.0.slices.len() }
|
||||
fn get_range(&self, range: Range<u64>)
|
||||
-> Box<dyn Stream<Item = Self::Data, Error = Self::Error> + Send> {
|
||||
-> Box<dyn Stream<Item = Result<Self::Data, Self::Error>> + Send + Sync> {
|
||||
self.0.slices.get_range(self, range)
|
||||
}
|
||||
}
|
||||
@ -1553,41 +1553,41 @@ mod tests {
|
||||
use db::recording::{self, TIME_UNITS_PER_SEC};
|
||||
use db::testutil::{self, TestDb, TEST_STREAM_ID};
|
||||
use db::writer;
|
||||
use futures::Future;
|
||||
use futures::Stream as FuturesStream;
|
||||
use futures::stream::TryStreamExt;
|
||||
use log::info;
|
||||
use openssl::hash;
|
||||
use http_serve::{self, Entity};
|
||||
use std::fs;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::str;
|
||||
use super::*;
|
||||
|
||||
fn fill_slice<E: http_serve::Entity>(slice: &mut [u8], e: &E, start: u64)
|
||||
async fn fill_slice<E: http_serve::Entity>(slice: &mut [u8], e: &E, start: u64)
|
||||
where E::Error : ::std::fmt::Debug {
|
||||
let mut p = 0;
|
||||
e.get_range(start .. start + slice.len() as u64)
|
||||
.for_each(|chunk| {
|
||||
Pin::from(e.get_range(start .. start + slice.len() as u64))
|
||||
.try_for_each(|chunk| {
|
||||
let c: &[u8] = chunk.bytes();
|
||||
slice[p .. p + c.len()].copy_from_slice(c);
|
||||
p += c.len();
|
||||
Ok::<_, E::Error>(())
|
||||
futures::future::ok::<_, E::Error>(())
|
||||
})
|
||||
.wait()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Returns the SHA-1 digest of the given `Entity`.
|
||||
fn digest<E: http_serve::Entity>(e: &E) -> hash::DigestBytes
|
||||
async fn digest<E: http_serve::Entity>(e: &E) -> hash::DigestBytes
|
||||
where E::Error : ::std::fmt::Debug {
|
||||
e.get_range(0 .. e.len())
|
||||
.fold(hash::Hasher::new(hash::MessageDigest::sha1()).unwrap(), |mut sha1, chunk| {
|
||||
Pin::from(e.get_range(0 .. e.len()))
|
||||
.try_fold(hash::Hasher::new(hash::MessageDigest::sha1()).unwrap(), |mut sha1, chunk| {
|
||||
let c: &[u8] = chunk.bytes();
|
||||
sha1.update(c).unwrap();
|
||||
Ok::<_, E::Error>(sha1)
|
||||
futures::future::ok::<_, E::Error>(sha1)
|
||||
})
|
||||
.wait()
|
||||
.await
|
||||
.unwrap()
|
||||
.finish()
|
||||
.unwrap()
|
||||
@ -1618,14 +1618,14 @@ mod tests {
|
||||
|
||||
/// Pushes the box at the given position onto the stack (returning true), or returns
|
||||
/// false if pos == max.
|
||||
fn internal_push(&mut self, pos: u64, max: u64) -> bool {
|
||||
async fn internal_push(&mut self, pos: u64, max: u64) -> bool {
|
||||
if pos == max { return false; }
|
||||
let mut hdr = [0u8; 16];
|
||||
fill_slice(&mut hdr[..8], &self.mp4, pos);
|
||||
fill_slice(&mut hdr[..8], &self.mp4, pos).await;
|
||||
let (len, hdr_len, boxtype_slice) = match BigEndian::read_u32(&hdr[..4]) {
|
||||
0 => (self.mp4.len() - pos, 8, &hdr[4..8]),
|
||||
1 => {
|
||||
fill_slice(&mut hdr[8..], &self.mp4, pos + 8);
|
||||
fill_slice(&mut hdr[8..], &self.mp4, pos + 8).await;
|
||||
(BigEndian::read_u64(&hdr[8..16]), 16, &hdr[4..8])
|
||||
},
|
||||
l => (l as u64, 8, &hdr[4..8]),
|
||||
@ -1661,53 +1661,53 @@ mod tests {
|
||||
|
||||
/// Gets the specified byte range within the current box (excluding length and type).
|
||||
/// Must not be at EOF.
|
||||
pub fn get(&self, start: u64, buf: &mut [u8]) {
|
||||
pub async fn get(&self, start: u64, buf: &mut [u8]) {
|
||||
let interior = &self.stack.last().expect("at root").interior;
|
||||
assert!(start + (buf.len() as u64) <= interior.end - interior.start,
|
||||
"path={} start={} buf.len={} interior={:?}",
|
||||
self.path(), start, buf.len(), interior);
|
||||
fill_slice(buf, &self.mp4, start+interior.start);
|
||||
fill_slice(buf, &self.mp4, start+interior.start).await;
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> Vec<u8> {
|
||||
pub async fn get_all(&self) -> Vec<u8> {
|
||||
let interior = self.stack.last().expect("at root").interior.clone();
|
||||
let len = (interior.end - interior.start) as usize;
|
||||
trace!("get_all: start={}, len={}", interior.start, len);
|
||||
let mut out = Vec::with_capacity(len);
|
||||
unsafe { out.set_len(len) };
|
||||
fill_slice(&mut out[..], &self.mp4, interior.start);
|
||||
fill_slice(&mut out[..], &self.mp4, interior.start).await;
|
||||
out
|
||||
}
|
||||
|
||||
/// Gets the specified u32 within the current box (excluding length and type).
|
||||
/// Must not be at EOF.
|
||||
pub fn get_u32(&self, p: u64) -> u32 {
|
||||
pub async fn get_u32(&self, p: u64) -> u32 {
|
||||
let mut buf = [0u8; 4];
|
||||
self.get(p, &mut buf);
|
||||
self.get(p, &mut buf).await;
|
||||
BigEndian::read_u32(&buf[..])
|
||||
}
|
||||
|
||||
pub fn get_u64(&self, p: u64) -> u64 {
|
||||
pub async fn get_u64(&self, p: u64) -> u64 {
|
||||
let mut buf = [0u8; 8];
|
||||
self.get(p, &mut buf);
|
||||
self.get(p, &mut buf).await;
|
||||
BigEndian::read_u64(&buf[..])
|
||||
}
|
||||
|
||||
/// Navigates to the next box after the current one, or up if the current one is last.
|
||||
pub fn next(&mut self) -> bool {
|
||||
pub async fn next(&mut self) -> bool {
|
||||
let old = self.stack.pop().expect("positioned at root; there is no next");
|
||||
let max = self.stack.last().map(|b| b.interior.end).unwrap_or_else(|| self.mp4.len());
|
||||
self.internal_push(old.interior.end, max)
|
||||
self.internal_push(old.interior.end, max).await
|
||||
}
|
||||
|
||||
/// Finds the next box of the given type after the current one, or navigates up if absent.
|
||||
pub fn find(&mut self, boxtype: &[u8]) -> bool {
|
||||
pub async fn find(&mut self, boxtype: &[u8]) -> bool {
|
||||
trace!("looking for {}", str::from_utf8(boxtype).unwrap());
|
||||
loop {
|
||||
if &self.stack.last().unwrap().boxtype[..] == boxtype {
|
||||
return true;
|
||||
}
|
||||
if !self.next() {
|
||||
if !self.next().await {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1717,10 +1717,11 @@ mod tests {
|
||||
pub fn up(&mut self) { self.stack.pop(); }
|
||||
|
||||
/// Moves down the stack. Must be positioned on a box with children.
|
||||
pub fn down(&mut self) {
|
||||
pub async fn down(&mut self) {
|
||||
let range = self.stack.last().map(|b| b.interior.clone())
|
||||
.unwrap_or_else(|| 0 .. self.mp4.len());
|
||||
assert!(self.internal_push(range.start, range.end), "no children in {}", self.path());
|
||||
assert!(self.internal_push(range.start, range.end).await,
|
||||
"no children in {}", self.path());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1732,17 +1733,17 @@ mod tests {
|
||||
|
||||
/// Finds the `moov/trak` that has a `tkhd` associated with the given `track_id`, which must
|
||||
/// exist.
|
||||
fn find_track(mp4: File, track_id: u32) -> Track {
|
||||
async fn find_track(mp4: File, track_id: u32) -> Track {
|
||||
let mut cursor = BoxCursor::new(mp4);
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"moov"));
|
||||
cursor.down();
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"moov").await);
|
||||
cursor.down().await;
|
||||
loop {
|
||||
assert!(cursor.find(b"trak"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"tkhd"));
|
||||
assert!(cursor.find(b"trak").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"tkhd").await);
|
||||
let mut version = [0u8; 1];
|
||||
cursor.get(0, &mut version);
|
||||
cursor.get(0, &mut version).await;
|
||||
|
||||
// Let id_pos be the offset after the FullBox section of the track_id.
|
||||
let id_pos = match version[0] {
|
||||
@ -1750,27 +1751,27 @@ mod tests {
|
||||
1 => 16, // ...64-bit times...
|
||||
v => panic!("unexpected tkhd version {}", v),
|
||||
};
|
||||
let cur_track_id = cursor.get_u32(4 + id_pos);
|
||||
let cur_track_id = cursor.get_u32(4 + id_pos).await;
|
||||
trace!("found moov/trak/tkhd with id {}; want {}", cur_track_id, track_id);
|
||||
if cur_track_id == track_id {
|
||||
break;
|
||||
}
|
||||
cursor.up();
|
||||
assert!(cursor.next());
|
||||
assert!(cursor.next().await);
|
||||
}
|
||||
let edts_cursor;
|
||||
if cursor.find(b"edts") {
|
||||
if cursor.find(b"edts").await {
|
||||
edts_cursor = Some(cursor.clone());
|
||||
cursor.up();
|
||||
} else {
|
||||
edts_cursor = None;
|
||||
};
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"mdia"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"minf"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"stbl"));
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"mdia").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"minf").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"stbl").await);
|
||||
Track{
|
||||
edts_cursor: edts_cursor,
|
||||
stbl_cursor: cursor,
|
||||
@ -1833,17 +1834,16 @@ mod tests {
|
||||
builder.build(tdb.db.clone(), tdb.dirs_by_stream_id.clone()).unwrap()
|
||||
}
|
||||
|
||||
fn write_mp4(mp4: &File, dir: &Path) -> String {
|
||||
async fn write_mp4(mp4: &File, dir: &Path) -> String {
|
||||
let mut filename = dir.to_path_buf();
|
||||
filename.push("clip.new.mp4");
|
||||
let mut out = fs::OpenOptions::new().write(true).create_new(true).open(&filename).unwrap();
|
||||
use ::std::io::Write;
|
||||
mp4.get_range(0 .. mp4.len())
|
||||
.for_each(|chunk| {
|
||||
out.write_all(chunk.bytes())?;
|
||||
Ok(())
|
||||
Pin::from(mp4.get_range(0 .. mp4.len()))
|
||||
.try_for_each(|chunk| {
|
||||
futures::future::ready(out.write_all(chunk.bytes()).map_err(|e| e.into()))
|
||||
})
|
||||
.wait()
|
||||
.await
|
||||
.unwrap();
|
||||
info!("wrote {:?}", filename);
|
||||
filename.to_str().unwrap().to_string()
|
||||
@ -1909,8 +1909,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Tests sample table for a simple video index of all sync frames.
|
||||
#[test]
|
||||
fn test_all_sync_frames() {
|
||||
#[tokio::test]
|
||||
async fn test_all_sync_frames() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
let mut r = db::RecordingToInsert::default();
|
||||
@ -1923,12 +1923,12 @@ mod tests {
|
||||
|
||||
// Time range [2, 2+4+6+8) means the 2nd, 3rd, and 4th samples should be included.
|
||||
let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2 .. 2+4+6+8).unwrap();
|
||||
let track = find_track(mp4, 1);
|
||||
let track = find_track(mp4, 1).await;
|
||||
assert!(track.edts_cursor.is_none());
|
||||
let mut cursor = track.stbl_cursor;
|
||||
cursor.down();
|
||||
cursor.find(b"stts");
|
||||
assert_eq!(cursor.get_all(), &[
|
||||
cursor.down().await;
|
||||
cursor.find(b"stts").await;
|
||||
assert_eq!(cursor.get_all().await, &[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x03, // entry_count
|
||||
|
||||
@ -1938,8 +1938,8 @@ mod tests {
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08,
|
||||
]);
|
||||
|
||||
cursor.find(b"stsz");
|
||||
assert_eq!(cursor.get_all(), &[
|
||||
cursor.find(b"stsz").await;
|
||||
assert_eq!(cursor.get_all().await, &[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x00, // sample_size
|
||||
0x00, 0x00, 0x00, 0x03, // sample_count
|
||||
@ -1950,8 +1950,8 @@ mod tests {
|
||||
0x00, 0x00, 0x00, 0x0c,
|
||||
]);
|
||||
|
||||
cursor.find(b"stss");
|
||||
assert_eq!(cursor.get_all(), &[
|
||||
cursor.find(b"stss").await;
|
||||
assert_eq!(cursor.get_all().await, &[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x03, // entry_count
|
||||
|
||||
@ -1963,8 +1963,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Tests sample table and edit list for a video index with half sync frames.
|
||||
#[test]
|
||||
fn test_half_sync_frames() {
|
||||
#[tokio::test]
|
||||
async fn test_half_sync_frames() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
let mut r = db::RecordingToInsert::default();
|
||||
@ -1978,13 +1978,13 @@ mod tests {
|
||||
// Time range [2+4+6, 2+4+6+8) means the 4th sample should be included.
|
||||
// The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
|
||||
let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2+4+6 .. 2+4+6+8).unwrap();
|
||||
let track = find_track(mp4, 1);
|
||||
let track = find_track(mp4, 1).await;
|
||||
|
||||
// Examine edts. It should skip the 3rd frame.
|
||||
let mut cursor = track.edts_cursor.unwrap();
|
||||
cursor.down();
|
||||
cursor.find(b"elst");
|
||||
assert_eq!(cursor.get_all(), &[
|
||||
cursor.down().await;
|
||||
cursor.find(b"elst").await;
|
||||
assert_eq!(cursor.get_all().await, &[
|
||||
0x01, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x01, // length
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // segment_duration
|
||||
@ -1994,9 +1994,9 @@ mod tests {
|
||||
|
||||
// Examine stbl.
|
||||
let mut cursor = track.stbl_cursor;
|
||||
cursor.down();
|
||||
cursor.find(b"stts");
|
||||
assert_eq!(cursor.get_all(), &[
|
||||
cursor.down().await;
|
||||
cursor.find(b"stts").await;
|
||||
assert_eq!(cursor.get_all().await, &[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x02, // entry_count
|
||||
|
||||
@ -2005,8 +2005,8 @@ mod tests {
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08,
|
||||
]);
|
||||
|
||||
cursor.find(b"stsz");
|
||||
assert_eq!(cursor.get_all(), &[
|
||||
cursor.find(b"stsz").await;
|
||||
assert_eq!(cursor.get_all().await, &[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x00, // sample_size
|
||||
0x00, 0x00, 0x00, 0x02, // sample_count
|
||||
@ -2016,8 +2016,8 @@ mod tests {
|
||||
0x00, 0x00, 0x00, 0x0c,
|
||||
]);
|
||||
|
||||
cursor.find(b"stss");
|
||||
assert_eq!(cursor.get_all(), &[
|
||||
cursor.find(b"stss").await;
|
||||
assert_eq!(cursor.get_all().await, &[
|
||||
0x00, 0x00, 0x00, 0x00, // version + flags
|
||||
0x00, 0x00, 0x00, 0x01, // entry_count
|
||||
|
||||
@ -2026,16 +2026,16 @@ mod tests {
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_segments() {
|
||||
#[tokio::test]
|
||||
async fn test_no_segments() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
let e = make_mp4_from_encoders(Type::Normal, &db, vec![], 0 .. 0).err().unwrap();
|
||||
assert_eq!(e.to_string(), "Invalid argument: no video_sample_entries");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_segment() {
|
||||
#[tokio::test]
|
||||
async fn test_multi_segment() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
let mut encoders = Vec::new();
|
||||
@ -2054,25 +2054,25 @@ mod tests {
|
||||
// This should include samples 3 and 4 only, both sync frames.
|
||||
let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1+2 .. 1+2+3+4).unwrap();
|
||||
let mut cursor = BoxCursor::new(mp4);
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"moov"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"trak"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"mdia"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"minf"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"stbl"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"stss"));
|
||||
assert_eq!(cursor.get_u32(4), 2); // entry_count
|
||||
assert_eq!(cursor.get_u32(8), 1);
|
||||
assert_eq!(cursor.get_u32(12), 2);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"moov").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"trak").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"mdia").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"minf").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"stbl").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"stss").await);
|
||||
assert_eq!(cursor.get_u32(4).await, 2); // entry_count
|
||||
assert_eq!(cursor.get_u32(8).await, 1);
|
||||
assert_eq!(cursor.get_u32(12).await, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_duration_recording() {
|
||||
#[tokio::test]
|
||||
async fn test_zero_duration_recording() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
let mut encoders = Vec::new();
|
||||
@ -2088,17 +2088,17 @@ mod tests {
|
||||
|
||||
// Multi-segment recording with an edit list, encoding with a zero-duration recording.
|
||||
let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1 .. 2+3).unwrap();
|
||||
let track = find_track(mp4, 1);
|
||||
let track = find_track(mp4, 1).await;
|
||||
let mut cursor = track.edts_cursor.unwrap();
|
||||
cursor.down();
|
||||
cursor.find(b"elst");
|
||||
assert_eq!(cursor.get_u32(4), 1); // entry_count
|
||||
assert_eq!(cursor.get_u64(8), 4); // segment_duration
|
||||
assert_eq!(cursor.get_u64(16), 1); // media_time
|
||||
cursor.down().await;
|
||||
cursor.find(b"elst").await;
|
||||
assert_eq!(cursor.get_u32(4).await, 1); // entry_count
|
||||
assert_eq!(cursor.get_u64(8).await, 4); // segment_duration
|
||||
assert_eq!(cursor.get_u64(16).await, 1); // media_time
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_segment() {
|
||||
#[tokio::test]
|
||||
async fn test_media_segment() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
let mut r = db::RecordingToInsert::default();
|
||||
@ -2114,45 +2114,45 @@ mod tests {
|
||||
let mp4 = make_mp4_from_encoders(Type::MediaSegment, &db, vec![r],
|
||||
2+4+6 .. 2+4+6+8+1).unwrap();
|
||||
let mut cursor = BoxCursor::new(mp4);
|
||||
cursor.down();
|
||||
cursor.down().await;
|
||||
|
||||
let mut mdat = cursor.clone();
|
||||
assert!(mdat.find(b"mdat"));
|
||||
assert!(mdat.find(b"mdat").await);
|
||||
|
||||
assert!(cursor.find(b"moof"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"traf"));
|
||||
cursor.down();
|
||||
assert!(cursor.find(b"trun"));
|
||||
assert_eq!(cursor.get_u32(4), 2);
|
||||
assert_eq!(cursor.get_u32(8) as u64, mdat.interior().start);
|
||||
assert_eq!(cursor.get_u32(12), 174063616); // first_sample_flags
|
||||
assert_eq!(cursor.get_u32(16), 6); // sample duration
|
||||
assert_eq!(cursor.get_u32(20), 9); // sample size
|
||||
assert_eq!(cursor.get_u32(24), 8); // sample duration
|
||||
assert_eq!(cursor.get_u32(28), 12); // sample size
|
||||
assert!(cursor.next());
|
||||
assert!(cursor.find(b"moof").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"traf").await);
|
||||
cursor.down().await;
|
||||
assert!(cursor.find(b"trun").await);
|
||||
assert_eq!(cursor.get_u32(4).await, 2);
|
||||
assert_eq!(cursor.get_u32(8).await as u64, mdat.interior().start);
|
||||
assert_eq!(cursor.get_u32(12).await, 174063616); // first_sample_flags
|
||||
assert_eq!(cursor.get_u32(16).await, 6); // sample duration
|
||||
assert_eq!(cursor.get_u32(20).await, 9); // sample size
|
||||
assert_eq!(cursor.get_u32(24).await, 8); // sample duration
|
||||
assert_eq!(cursor.get_u32(28).await, 12); // sample size
|
||||
assert!(cursor.next().await);
|
||||
assert_eq!(cursor.name(), "trun");
|
||||
assert_eq!(cursor.get_u32(4), 1);
|
||||
assert_eq!(cursor.get_u32(8) as u64, mdat.interior().start + 9 + 12);
|
||||
assert_eq!(cursor.get_u32(12), 174063616); // first_sample_flags
|
||||
assert_eq!(cursor.get_u32(16), 1); // sample duration
|
||||
assert_eq!(cursor.get_u32(20), 15); // sample size
|
||||
assert_eq!(cursor.get_u32(4).await, 1);
|
||||
assert_eq!(cursor.get_u32(8).await as u64, mdat.interior().start + 9 + 12);
|
||||
assert_eq!(cursor.get_u32(12).await, 174063616); // first_sample_flags
|
||||
assert_eq!(cursor.get_u32(16).await, 1); // sample duration
|
||||
assert_eq!(cursor.get_u32(20).await, 15); // sample size
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip() {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path());
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||
compare_mp4s(&new_filename, 0, 0);
|
||||
|
||||
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
||||
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
||||
// combine ranges from the new format with ranges from the old format.
|
||||
let sha1 = digest(&mp4);
|
||||
let sha1 = digest(&mp4).await;
|
||||
assert_eq!("17376879bcf872dd4ad1197225a32d5473fb0dc6", strutil::hex(&sha1[..]));
|
||||
const EXPECTED_ETAG: &'static str = "\"953dcf1a61debe785d5dec3ae2d3992a819b68ae\"";
|
||||
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
||||
@ -2161,19 +2161,19 @@ mod tests {
|
||||
db.syncer_join.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_with_subtitles() {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_with_subtitles() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mp4 = create_mp4_from_db(&db, 0, 0, true);
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path());
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||
compare_mp4s(&new_filename, 0, 0);
|
||||
|
||||
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
||||
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
||||
// combine ranges from the new format with ranges from the old format.
|
||||
let sha1 = digest(&mp4);
|
||||
let sha1 = digest(&mp4).await;
|
||||
assert_eq!("1cd90e0b49747cc54c953153d6709f2fb5df6b14", strutil::hex(&sha1[..]));
|
||||
const EXPECTED_ETAG: &'static str = "\"736655313f10747528a663190517620cdffea6d0\"";
|
||||
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
||||
@ -2182,19 +2182,19 @@ mod tests {
|
||||
db.syncer_join.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_with_edit_list() {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_with_edit_list() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mp4 = create_mp4_from_db(&db, 1, 0, false);
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path());
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||
compare_mp4s(&new_filename, 1, 0);
|
||||
|
||||
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
||||
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
||||
// combine ranges from the new format with ranges from the old format.
|
||||
let sha1 = digest(&mp4);
|
||||
let sha1 = digest(&mp4).await;
|
||||
assert_eq!("49893e3997da6bc625a04b09abf4b1ddbe0bc85d", strutil::hex(&sha1[..]));
|
||||
const EXPECTED_ETAG: &'static str = "\"e87ed99dea31b7c4d1e9186045abaf5ac3c2d2f8\"";
|
||||
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
||||
@ -2203,19 +2203,19 @@ mod tests {
|
||||
db.syncer_join.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_with_shorten() {
|
||||
#[tokio::test]
|
||||
async fn test_round_trip_with_shorten() {
|
||||
testutil::init();
|
||||
let db = TestDb::new(RealClocks {});
|
||||
copy_mp4_to_db(&db);
|
||||
let mp4 = create_mp4_from_db(&db, 0, 1, false);
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path());
|
||||
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
||||
compare_mp4s(&new_filename, 0, 1);
|
||||
|
||||
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
||||
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
||||
// combine ranges from the new format with ranges from the old format.
|
||||
let sha1 = digest(&mp4);
|
||||
let sha1 = digest(&mp4).await;
|
||||
assert_eq!("0615feaa3c50a7889fb0e6842de3bd3d3143bc78", strutil::hex(&sha1[..]));
|
||||
const EXPECTED_ETAG: &'static str = "\"6f0d21a6027b0e444f404a68527dbf5c9a5c1a26\"";
|
||||
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
||||
@ -2232,12 +2232,10 @@ mod bench {
|
||||
use base::clock::RealClocks;
|
||||
use db::recording;
|
||||
use db::testutil::{self, TestDb};
|
||||
use futures::{Future, future};
|
||||
use futures::future;
|
||||
use hyper;
|
||||
use http::header;
|
||||
use http_serve;
|
||||
use lazy_static::lazy_static;
|
||||
use std::error::Error as StdError;
|
||||
use super::tests::create_mp4_from_db;
|
||||
use url::Url;
|
||||
|
||||
@ -2259,17 +2257,24 @@ mod bench {
|
||||
testutil::add_dummy_recordings_to_db(&db.db, 60);
|
||||
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
||||
let p = mp4.0.initial_sample_byte_pos;
|
||||
let (tx, rx) = ::std::sync::mpsc::channel();
|
||||
::std::thread::spawn(move || {
|
||||
let addr = "127.0.0.1:0".parse().unwrap();
|
||||
let server = hyper::server::Server::bind(&addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(move || Ok::<_, Box<dyn StdError + Send + Sync>>(
|
||||
MyService(mp4.clone())));
|
||||
tx.send(server.local_addr()).unwrap();
|
||||
::tokio::run(server.map_err(|e| panic!(e)));
|
||||
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
||||
future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
||||
let mp4 = mp4.clone();
|
||||
move |req| future::ok::<hyper::Response<crate::body::Body>, hyper::Error>(
|
||||
http_serve::serve(mp4.clone(), &req))
|
||||
}))
|
||||
});
|
||||
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let srv = rt.enter(|| {
|
||||
let addr = ([127, 0, 0, 1], 0).into();
|
||||
hyper::server::Server::bind(&addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(make_svc)
|
||||
});
|
||||
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
||||
::std::thread::spawn(move || {
|
||||
rt.block_on(srv).unwrap();
|
||||
});
|
||||
let addr = rx.recv().unwrap();
|
||||
BenchServer {
|
||||
url: Url::parse(&format!("http://{}:{}/", addr.ip(), addr.port())).unwrap(),
|
||||
generated_len: p,
|
||||
@ -2277,19 +2282,6 @@ mod bench {
|
||||
}
|
||||
}
|
||||
|
||||
struct MyService(super::File);
|
||||
|
||||
impl hyper::service::Service for MyService {
|
||||
type ReqBody = hyper::Body;
|
||||
type ResBody = crate::body::Body;
|
||||
type Error = crate::body::BoxedError;
|
||||
type Future = future::FutureResult<::http::Response<Self::ResBody>, Self::Error>;
|
||||
|
||||
fn call(&mut self, req: ::http::Request<Self::ReqBody>) -> Self::Future {
|
||||
future::ok(http_serve::serve(self.0.clone(), &req))
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVER: BenchServer = { BenchServer::new() };
|
||||
}
|
||||
@ -2332,7 +2324,7 @@ mod bench {
|
||||
let mut run = || {
|
||||
let mut resp =
|
||||
client.get(server.url.clone())
|
||||
.header(header::RANGE, format!("bytes=0-{}", p - 1))
|
||||
.header(reqwest::header::RANGE, format!("bytes=0-{}", p - 1))
|
||||
.send()
|
||||
.unwrap();
|
||||
buf.clear();
|
||||
|
@ -33,15 +33,16 @@
|
||||
use base::format_err_t;
|
||||
use crate::body::{BoxedError, wrap_error};
|
||||
use failure::{Error, bail};
|
||||
use futures::{Stream, stream};
|
||||
use futures::{Stream, stream, stream::StreamExt};
|
||||
use std::fmt;
|
||||
use std::ops::Range;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// Gets a byte range given a context argument.
|
||||
/// Each `Slice` instance belongs to a single `Slices`.
|
||||
pub trait Slice : fmt::Debug + Sized + Sync + 'static {
|
||||
type Ctx: Send + Clone;
|
||||
type Chunk: Send;
|
||||
type Ctx: Send + Sync + Clone;
|
||||
type Chunk: Send + Sync;
|
||||
|
||||
/// The byte position (relative to the start of the `Slices`) of the end of this slice,
|
||||
/// exclusive. Note the starting position (and thus length) are inferred from the previous
|
||||
@ -52,7 +53,7 @@ pub trait Slice : fmt::Debug + Sized + Sync + 'static {
|
||||
/// 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 get_range(&self, ctx: &Self::Ctx, r: Range<u64>, len: u64)
|
||||
-> Box<dyn Stream<Item = Self::Chunk, Error = BoxedError> + Send>;
|
||||
-> Box<dyn Stream<Item = Result<Self::Chunk, BoxedError>> + Sync + Send>;
|
||||
|
||||
fn get_slices(ctx: &Self::Ctx) -> &Slices<Self>;
|
||||
}
|
||||
@ -111,9 +112,9 @@ impl<S> Slices<S> where S: Slice {
|
||||
/// Writes `range` to `out`.
|
||||
/// This interface mirrors `http_serve::Entity::write_to`, with the additional `ctx` argument.
|
||||
pub fn get_range(&self, ctx: &S::Ctx, range: Range<u64>)
|
||||
-> Box<dyn Stream<Item = S::Chunk, Error = BoxedError> + Send> {
|
||||
-> Box<dyn Stream<Item = Result<S::Chunk, BoxedError>> + Sync + Send> {
|
||||
if range.start > range.end || range.end > self.len {
|
||||
return Box::new(stream::once(Err(wrap_error(format_err_t!(
|
||||
return Box::new(stream::once(futures::future::err(wrap_error(format_err_t!(
|
||||
Internal, "Bad range {:?} for slice of length {}", range, self.len)))));
|
||||
}
|
||||
|
||||
@ -133,15 +134,15 @@ impl<S> Slices<S> where S: Slice {
|
||||
let (body, min_end);
|
||||
{
|
||||
let self_ = S::get_slices(&c);
|
||||
if i == self_.slices.len() { return None }
|
||||
if i == self_.slices.len() { return futures::future::ready(None) }
|
||||
let s = &self_.slices[i];
|
||||
if range.end == slice_start + start_pos { return None }
|
||||
if range.end == slice_start + start_pos { return futures::future::ready(None) }
|
||||
let s_end = s.end();
|
||||
min_end = ::std::cmp::min(range.end, s_end);
|
||||
let l = s_end - slice_start;
|
||||
body = s.get_range(&c, start_pos .. min_end - slice_start, l);
|
||||
};
|
||||
Some(Ok::<_, BoxedError>((body, (c, i+1, 0, min_end))))
|
||||
futures::future::ready(Some((Pin::from(body), (c, i+1, 0, min_end))))
|
||||
});
|
||||
Box::new(bodies.flatten())
|
||||
}
|
||||
@ -151,10 +152,10 @@ impl<S> Slices<S> where S: Slice {
|
||||
mod tests {
|
||||
use crate::body::BoxedError;
|
||||
use db::testutil;
|
||||
use futures::{Future, Stream};
|
||||
use futures::stream;
|
||||
use futures::stream::{self, Stream, TryStreamExt};
|
||||
use lazy_static::lazy_static;
|
||||
use std::ops::Range;
|
||||
use std::pin::Pin;
|
||||
use super::{Slice, Slices};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@ -176,8 +177,8 @@ mod tests {
|
||||
fn end(&self) -> u64 { self.end }
|
||||
|
||||
fn get_range(&self, _ctx: &&'static Slices<FakeSlice>, r: Range<u64>, _l: u64)
|
||||
-> Box<dyn Stream<Item = FakeChunk, Error = BoxedError> + Send> {
|
||||
Box::new(stream::once(Ok(FakeChunk{slice: self.name, range: r})))
|
||||
-> Box<dyn Stream<Item = Result<FakeChunk, BoxedError>> + Send + Sync> {
|
||||
Box::new(stream::once(futures::future::ok(FakeChunk{slice: self.name, range: r})))
|
||||
}
|
||||
|
||||
fn get_slices(ctx: &&'static Slices<FakeSlice>) -> &'static Slices<Self> { *ctx }
|
||||
@ -195,33 +196,37 @@ mod tests {
|
||||
};
|
||||
}
|
||||
|
||||
async fn get_range(r: Range<u64>) -> Vec<FakeChunk> {
|
||||
Pin::from(SLICES.get_range(&&*SLICES, r)).try_collect().await.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn size() {
|
||||
testutil::init();
|
||||
assert_eq!(5 + 13 + 7 + 17 + 19, SLICES.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn exact_slice() {
|
||||
#[tokio::test]
|
||||
pub async fn exact_slice() {
|
||||
// Test writing exactly slice b.
|
||||
testutil::init();
|
||||
let out = SLICES.get_range(&&*SLICES, 5 .. 18).collect().wait().unwrap();
|
||||
let out = get_range(5 .. 18).await;
|
||||
assert_eq!(&[FakeChunk{slice: "b", range: 0 .. 13}], &out[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn offset_first() {
|
||||
#[tokio::test]
|
||||
pub async fn offset_first() {
|
||||
// Test writing part of slice a.
|
||||
testutil::init();
|
||||
let out = SLICES.get_range(&&*SLICES, 1 .. 3).collect().wait().unwrap();
|
||||
let out = get_range(1 .. 3).await;
|
||||
assert_eq!(&[FakeChunk{slice: "a", range: 1 .. 3}], &out[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn offset_mid() {
|
||||
#[tokio::test]
|
||||
pub async fn offset_mid() {
|
||||
// Test writing part of slice b, all of slice c, and part of slice d.
|
||||
testutil::init();
|
||||
let out = SLICES.get_range(&&*SLICES, 17 .. 26).collect().wait().unwrap();
|
||||
let out = get_range(17 .. 26).await;
|
||||
assert_eq!(&[
|
||||
FakeChunk{slice: "b", range: 12 .. 13},
|
||||
FakeChunk{slice: "c", range: 0 .. 7},
|
||||
@ -229,11 +234,11 @@ mod tests {
|
||||
], &out[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn everything() {
|
||||
#[tokio::test]
|
||||
pub async fn everything() {
|
||||
// Test writing the whole Slices.
|
||||
testutil::init();
|
||||
let out = SLICES.get_range(&&*SLICES, 0 .. 61).collect().wait().unwrap();
|
||||
let out = get_range(0 .. 61).await;
|
||||
assert_eq!(&[
|
||||
FakeChunk{slice: "a", range: 0 .. 5},
|
||||
FakeChunk{slice: "b", range: 0 .. 13},
|
||||
@ -243,10 +248,10 @@ mod tests {
|
||||
], &out[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn at_end() {
|
||||
#[tokio::test]
|
||||
pub async fn at_end() {
|
||||
testutil::init();
|
||||
let out = SLICES.get_range(&&*SLICES, 61 .. 61).collect().wait().unwrap();
|
||||
let out = get_range(61 .. 61).await;
|
||||
let empty: &[FakeChunk] = &[];
|
||||
assert_eq!(empty, &out[..]);
|
||||
}
|
||||
|
226
src/web.rs
226
src/web.rs
@ -30,6 +30,7 @@
|
||||
|
||||
use base::clock::Clocks;
|
||||
use base::{ErrorKind, ResultExt, bail_t, strutil};
|
||||
use bytes::Bytes;
|
||||
use crate::body::{Body, BoxedError};
|
||||
use crate::json;
|
||||
use crate::mp4;
|
||||
@ -41,8 +42,8 @@ use db::{auth, recording};
|
||||
use db::dir::SampleFileDir;
|
||||
use failure::{Error, bail, format_err};
|
||||
use fnv::FnvHashMap;
|
||||
use futures::{Future, Stream, future};
|
||||
use futures_cpupool;
|
||||
use futures::future::{self, Future, TryFutureExt};
|
||||
use futures::stream::{Stream, StreamExt, TryStreamExt};
|
||||
use http::{Request, Response, status::StatusCode};
|
||||
use http_serve;
|
||||
use http::header::{self, HeaderValue};
|
||||
@ -56,6 +57,7 @@ use std::fs;
|
||||
use std::net::IpAddr;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use url::form_urlencoded;
|
||||
use uuid::Uuid;
|
||||
@ -68,6 +70,9 @@ lazy_static! {
|
||||
Regex::new(r"^(\d+)(-\d+)?(@\d+)?(?:\.(\d+)?-(\d+)?)?$").unwrap();
|
||||
}
|
||||
|
||||
type BoxedFuture = Box<dyn Future<Output = Result<Response<Body>, BoxedError>> +
|
||||
Sync + Send + 'static>;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum Path {
|
||||
TopLevel, // "/api/"
|
||||
@ -254,7 +259,6 @@ struct ServiceInner {
|
||||
db: Arc<db::Database>,
|
||||
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
|
||||
ui_files: HashMap<String, UiFile>,
|
||||
pool: futures_cpupool::CpuPool,
|
||||
time_zone_name: String,
|
||||
allow_unauthenticated_permissions: Option<db::Permissions>,
|
||||
trust_forward_hdrs: bool,
|
||||
@ -505,11 +509,12 @@ impl ServiceInner {
|
||||
|
||||
fn static_file(&self, req: &Request<::hyper::Body>, path: &str) -> ResponseResult {
|
||||
let s = self.ui_files.get(path).ok_or_else(|| not_found("no such static file"))?;
|
||||
let f = fs::File::open(&s.path).map_err(internal_server_err)?;
|
||||
let f = tokio::task::block_in_place(move || {
|
||||
fs::File::open(&s.path).map_err(internal_server_err)
|
||||
})?;
|
||||
let mut hdrs = http::HeaderMap::new();
|
||||
hdrs.insert(header::CONTENT_TYPE, s.mime.clone());
|
||||
let e = http_serve::ChunkedReadFile::new(f, Some(self.pool.clone()), hdrs)
|
||||
.map_err(internal_server_err)?;
|
||||
let e = http_serve::ChunkedReadFile::new(f, hdrs).map_err(internal_server_err)?;
|
||||
Ok(http_serve::serve(e, &req))
|
||||
}
|
||||
|
||||
@ -552,7 +557,7 @@ impl ServiceInner {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn login(&self, req: &Request<::hyper::Body>, body: hyper::Chunk) -> ResponseResult {
|
||||
fn login(&self, req: &Request<::hyper::Body>, body: Bytes) -> ResponseResult {
|
||||
let mut username = None;
|
||||
let mut password = None;
|
||||
for (key, value) in form_urlencoded::parse(&body) {
|
||||
@ -583,23 +588,24 @@ impl ServiceInner {
|
||||
flags)
|
||||
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
||||
let s_suffix = if is_secure {
|
||||
"; HttpOnly; Secure; SameSite=Strict; Max-Age=2147483648; Path=/"
|
||||
&b"; HttpOnly; Secure; SameSite=Strict; Max-Age=2147483648; Path=/"[..]
|
||||
} else {
|
||||
"; HttpOnly; SameSite=Strict; Max-Age=2147483648; Path=/"
|
||||
&b"; HttpOnly; SameSite=Strict; Max-Age=2147483648; Path=/"[..]
|
||||
};
|
||||
let mut encoded = [0u8; 64];
|
||||
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
|
||||
let mut cookie = BytesMut::with_capacity("s=".len() + encoded.len() + s_suffix.len());
|
||||
cookie.put("s=");
|
||||
cookie.put(&b"s="[..]);
|
||||
cookie.put(&encoded[..]);
|
||||
cookie.put(s_suffix);
|
||||
Ok(Response::builder()
|
||||
.header(header::SET_COOKIE, cookie.freeze())
|
||||
.header(header::SET_COOKIE, HeaderValue::from_maybe_shared(cookie.freeze())
|
||||
.expect("cookie can't have invalid bytes"))
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.body(b""[..].into()).unwrap())
|
||||
}
|
||||
|
||||
fn logout(&self, req: &Request<hyper::Body>, body: hyper::Chunk) -> ResponseResult {
|
||||
fn logout(&self, req: &Request<hyper::Body>, body: Bytes) -> ResponseResult {
|
||||
// Parse parameters.
|
||||
let mut csrf = None;
|
||||
for (key, value) in form_urlencoded::parse(&body) {
|
||||
@ -649,7 +655,7 @@ impl ServiceInner {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn post_signals(&self, req: &Request<hyper::Body>, caller: Caller, body: hyper::Chunk)
|
||||
fn post_signals(&self, req: &Request<hyper::Body>, caller: Caller, body: Bytes)
|
||||
-> ResponseResult {
|
||||
if !caller.permissions.update_signals {
|
||||
return Err(plain_response(StatusCode::UNAUTHORIZED, "update_signals required"));
|
||||
@ -769,13 +775,10 @@ fn extract_sid(req: &Request<hyper::Body>) -> Option<auth::RawSessionId> {
|
||||
/// `application/x-www-form-urlencoded`, returns an appropriate error response instead.
|
||||
///
|
||||
/// Use with `and_then` to chain logic which consumes the form body.
|
||||
fn with_form_body(mut req: Request<hyper::Body>)
|
||||
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
|
||||
Error = Response<Body>> +
|
||||
Send + 'static> {
|
||||
async fn with_form_body(mut req: Request<hyper::Body>)
|
||||
-> Result<(Request<hyper::Body>, Bytes), Response<Body>> {
|
||||
if *req.method() != http::method::Method::POST {
|
||||
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST expected")));
|
||||
return Err(plain_response(StatusCode::METHOD_NOT_ALLOWED, "POST expected"));
|
||||
}
|
||||
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
||||
Some(t) if t == "application/x-www-form-urlencoded" => true,
|
||||
@ -783,25 +786,21 @@ fn with_form_body(mut req: Request<hyper::Body>)
|
||||
_ => false,
|
||||
};
|
||||
if !correct_mime_type {
|
||||
return Box::new(future::err(bad_req(
|
||||
"expected application/x-www-form-urlencoded request body")));
|
||||
return Err(bad_req("expected application/x-www-form-urlencoded request body"));
|
||||
}
|
||||
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
||||
Box::new(b.concat2()
|
||||
.map(|b| (req, b))
|
||||
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
|
||||
e))))
|
||||
match hyper::body::to_bytes(b).await {
|
||||
Ok(b) => Ok((req, b)),
|
||||
Err(e) => Err(internal_server_err(format_err!("unable to read request body: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove redundancy with above. Probably better to just always expect requests in json
|
||||
// format rather than using the form style for login/logout.
|
||||
fn with_json_body(mut req: Request<hyper::Body>)
|
||||
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
|
||||
Error = Response<Body>> +
|
||||
Send + 'static> {
|
||||
async fn with_json_body(mut req: Request<hyper::Body>)
|
||||
-> Result<(Request<hyper::Body>, Bytes), Response<Body>> {
|
||||
if *req.method() != http::method::Method::POST {
|
||||
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST expected")));
|
||||
return Err(plain_response(StatusCode::METHOD_NOT_ALLOWED, "POST expected"));
|
||||
}
|
||||
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
||||
Some(t) if t == "application/json" => true,
|
||||
@ -809,14 +808,13 @@ fn with_json_body(mut req: Request<hyper::Body>)
|
||||
_ => false,
|
||||
};
|
||||
if !correct_mime_type {
|
||||
return Box::new(future::err(bad_req(
|
||||
"expected application/json request body")));
|
||||
return Err(bad_req("expected application/json request body"));
|
||||
}
|
||||
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
||||
Box::new(b.concat2()
|
||||
.map(|b| (req, b))
|
||||
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
|
||||
e))))
|
||||
match hyper::body::to_bytes(b).await {
|
||||
Ok(b) => Ok((req, b)),
|
||||
Err(e) => Err(internal_server_err(format_err!("unable to read request body: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -859,7 +857,6 @@ impl Service {
|
||||
db: config.db,
|
||||
dirs_by_stream_id,
|
||||
ui_files,
|
||||
pool: futures_cpupool::Builder::new().pool_size(1).name_prefix("static").create(),
|
||||
allow_unauthenticated_permissions: config.allow_unauthenticated_permissions,
|
||||
trust_forward_hdrs: config.trust_forward_hdrs,
|
||||
time_zone_name: config.time_zone_name,
|
||||
@ -914,7 +911,7 @@ impl Service {
|
||||
}
|
||||
let stream_id;
|
||||
let open_id;
|
||||
let (sub_tx, sub_rx) = futures::sync::mpsc::unbounded();
|
||||
let (sub_tx, sub_rx) = futures::channel::mpsc::unbounded();
|
||||
{
|
||||
let mut db = self.0.db.lock();
|
||||
open_id = match db.open {
|
||||
@ -934,9 +931,8 @@ impl Service {
|
||||
.expect("stream_id refed by camera");
|
||||
}
|
||||
let inner = self.0.clone();
|
||||
let body: crate::body::BodyStream = Box::new(sub_rx
|
||||
.map_err(|()| unreachable!())
|
||||
.and_then(move |live| -> Result<_, base::Error> {
|
||||
let body = sub_rx
|
||||
.map(move |live| -> Result<_, base::Error> {
|
||||
let mut builder = mp4::FileBuilder::new(mp4::Type::MediaSegment);
|
||||
let mut vse_id = None;
|
||||
{
|
||||
@ -960,7 +956,6 @@ impl Service {
|
||||
let mp4 = builder.build(inner.db.clone(), inner.dirs_by_stream_id.clone())?;
|
||||
let mut hdrs = http::header::HeaderMap::new();
|
||||
mp4.add_headers(&mut hdrs);
|
||||
//Ok(format!("{:?}\n\n", mp4).into())
|
||||
let mime_type = hdrs.get(http::header::CONTENT_TYPE).unwrap();
|
||||
let len = mp4.len();
|
||||
use futures::stream::once;
|
||||
@ -977,16 +972,17 @@ impl Service {
|
||||
live.off_90k.start,
|
||||
live.off_90k.end,
|
||||
&vse_id);
|
||||
let v: Vec<crate::body::BodyStream> = vec![
|
||||
Box::new(once(Ok(hdr.into()))),
|
||||
mp4.get_range(0 .. len),
|
||||
Box::new(once(Ok("\r\n\r\n".into())))
|
||||
let v: Vec<Pin<crate::body::BodyStream>> = vec![
|
||||
Box::pin(once(futures::future::ok(hdr.into()))),
|
||||
Pin::from(mp4.get_range(0 .. len)),
|
||||
Box::pin(once(futures::future::ok("\r\n\r\n".into())))
|
||||
];
|
||||
Ok(futures::stream::iter_ok::<_, crate::body::BoxedError>(v))
|
||||
})
|
||||
.map_err(|e| Box::new(e.compat()))
|
||||
.flatten()
|
||||
.flatten());
|
||||
Ok(futures::stream::iter(v).flatten())
|
||||
});
|
||||
let body = body.map_err::<BoxedError, _>(|e| Box::new(e.compat()));
|
||||
let _: &dyn Stream<Item = Result<_, BoxedError>> = &body;
|
||||
let body = body.try_flatten();
|
||||
let body: crate::body::BodyStream = Box::new(body);
|
||||
let body: Body = body.into();
|
||||
Ok(http::Response::builder()
|
||||
.header("X-Open-Id", open_id.to_string())
|
||||
@ -996,32 +992,24 @@ impl Service {
|
||||
}
|
||||
|
||||
fn signals(&self, req: Request<hyper::Body>, caller: Caller)
|
||||
-> Box<dyn Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static> {
|
||||
-> Box<dyn Future<Output = Result<Response<Body>, Response<Body>>> + Send + Sync + 'static> {
|
||||
use http::method::Method;
|
||||
match *req.method() {
|
||||
Method::POST => Box::new(with_json_body(req)
|
||||
.and_then({
|
||||
let s = self.0.clone();
|
||||
move |(req, b)| s.post_signals(&req, caller, b)
|
||||
move |(req, b)| future::ready(s.post_signals(&req, caller, b))
|
||||
})),
|
||||
Method::GET | Method::HEAD => Box::new(future::result(self.0.get_signals(&req))),
|
||||
Method::GET | Method::HEAD => Box::new(future::ready(self.0.get_signals(&req))),
|
||||
_ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST, GET, or HEAD expected"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ::hyper::service::Service for Service {
|
||||
type ReqBody = ::hyper::Body;
|
||||
type ResBody = Body;
|
||||
type Error = BoxedError;
|
||||
type Future = Box<dyn Future<Item = Response<Self::ResBody>, Error = Self::Error> + Send + 'static>;
|
||||
|
||||
fn call(&mut self, req: Request<::hyper::Body>) -> Self::Future {
|
||||
fn wrap<R>(is_private: bool, r: R)
|
||||
-> Box<dyn Future<Item = Response<Body>, Error = BoxedError> + Send + 'static>
|
||||
where R: Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static {
|
||||
return Box::new(r.or_else(|e| Ok(e)).map(move |mut r| {
|
||||
pub fn serve(&mut self, req: Request<::hyper::Body>) -> BoxedFuture {
|
||||
fn wrap<R>(is_private: bool, r: R) -> BoxedFuture
|
||||
where R: Future<Output = Result<Response<Body>, Response<Body>>> + Send + Sync + 'static {
|
||||
return Box::new(r.or_else(|e| futures::future::ok(e)).map_ok(move |mut r| {
|
||||
if is_private {
|
||||
r.headers_mut().insert("Cache-Control", HeaderValue::from_static("private"));
|
||||
}
|
||||
@ -1030,8 +1018,8 @@ impl ::hyper::service::Service for Service {
|
||||
}
|
||||
|
||||
fn wrap_r(is_private: bool, r: ResponseResult)
|
||||
-> Box<dyn Future<Item = Response<Body>, Error = BoxedError> + Send + 'static> {
|
||||
return wrap(is_private, future::result(r))
|
||||
-> Box<dyn Future<Output = Result<Response<Body>, BoxedError>> + Send + Sync + 'static> {
|
||||
return wrap(is_private, future::ready(r))
|
||||
}
|
||||
|
||||
let p = Path::decode(req.uri().path());
|
||||
@ -1066,13 +1054,13 @@ impl ::hyper::service::Service for Service {
|
||||
Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
|
||||
Path::Login => wrap(true, with_form_body(req).and_then({
|
||||
let s = self.clone();
|
||||
move |(req, b)| { s.0.login(&req, b) }
|
||||
move |(req, b)| future::ready(s.0.login(&req, b))
|
||||
})),
|
||||
Path::Logout => wrap(true, with_form_body(req).and_then({
|
||||
let s = self.clone();
|
||||
move |(req, b)| { s.0.logout(&req, b) }
|
||||
move |(req, b)| future::ready(s.0.logout(&req, b))
|
||||
})),
|
||||
Path::Signals => wrap(true, self.signals(req, caller)),
|
||||
Path::Signals => wrap(true, Pin::from(self.signals(req, caller))),
|
||||
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
|
||||
}
|
||||
}
|
||||
@ -1081,11 +1069,9 @@ impl ::hyper::service::Service for Service {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use db::testutil::{self, TestDb};
|
||||
use futures::Future;
|
||||
use http::header;
|
||||
use futures::future::FutureExt;
|
||||
use log::info;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error as StdError;
|
||||
use super::Segments;
|
||||
|
||||
struct Server {
|
||||
@ -1093,14 +1079,13 @@ mod tests {
|
||||
base_url: String,
|
||||
//test_camera_uuid: Uuid,
|
||||
handle: Option<::std::thread::JoinHandle<()>>,
|
||||
shutdown_tx: Option<futures::sync::oneshot::Sender<()>>,
|
||||
shutdown_tx: Option<futures::channel::oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
fn new(allow_unauthenticated_permissions: Option<db::Permissions>) -> Server {
|
||||
let db = TestDb::new(base::clock::RealClocks {});
|
||||
let (shutdown_tx, shutdown_rx) = futures::sync::oneshot::channel::<()>();
|
||||
let addr = "127.0.0.1:0".parse().unwrap();
|
||||
let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel::<()>();
|
||||
let service = super::Service::new(super::Config {
|
||||
db: db.db.clone(),
|
||||
ui_dir: None,
|
||||
@ -1108,12 +1093,22 @@ mod tests {
|
||||
trust_forward_hdrs: true,
|
||||
time_zone_name: "".to_owned(),
|
||||
}).unwrap();
|
||||
let server = hyper::server::Server::bind(&addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(move || Ok::<_, Box<dyn StdError + Send + Sync>>(service.clone()));
|
||||
let addr = server.local_addr(); // resolve port 0 to a real ephemeral port number.
|
||||
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
||||
futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
||||
let mut s = service.clone();
|
||||
move |req| std::pin::Pin::from(s.serve(req))
|
||||
}))
|
||||
});
|
||||
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let srv = rt.enter(|| {
|
||||
let addr = ([127, 0, 0, 1], 0).into();
|
||||
hyper::server::Server::bind(&addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(make_svc)
|
||||
});
|
||||
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
||||
let handle = ::std::thread::spawn(move || {
|
||||
::tokio::run(server.with_graceful_shutdown(shutdown_rx).map_err(|e| panic!(e)));
|
||||
rt.block_on(srv.with_graceful_shutdown(shutdown_rx.map(|_| ()))).unwrap();
|
||||
});
|
||||
|
||||
// Create a user.
|
||||
@ -1141,14 +1136,14 @@ mod tests {
|
||||
struct SessionCookie(Option<String>);
|
||||
|
||||
impl SessionCookie {
|
||||
pub fn new(headers: &http::HeaderMap) -> Self {
|
||||
pub fn new(headers: &reqwest::header::HeaderMap) -> Self {
|
||||
let mut c = SessionCookie::default();
|
||||
c.update(headers);
|
||||
c
|
||||
}
|
||||
|
||||
pub fn update(&mut self, headers: &http::HeaderMap) {
|
||||
for set_cookie in headers.get_all(header::SET_COOKIE) {
|
||||
pub fn update(&mut self, headers: &reqwest::header::HeaderMap) {
|
||||
for set_cookie in headers.get_all(reqwest::header::SET_COOKIE) {
|
||||
let mut set_cookie = set_cookie.to_str().unwrap().split("; ");
|
||||
let c = set_cookie.next().unwrap();
|
||||
let mut clear = false;
|
||||
@ -1256,7 +1251,7 @@ mod tests {
|
||||
let s = Server::new(None);
|
||||
let cli = reqwest::Client::new();
|
||||
let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1267,29 +1262,29 @@ mod tests {
|
||||
let login_url = format!("{}/api/login", &s.base_url);
|
||||
|
||||
let resp = cli.get(&login_url).send().unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::METHOD_NOT_ALLOWED);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::METHOD_NOT_ALLOWED);
|
||||
|
||||
let resp = cli.post(&login_url).send().unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||
|
||||
let mut p = HashMap::new();
|
||||
p.insert("username", "slamb");
|
||||
p.insert("password", "asdf");
|
||||
let resp = cli.post(&login_url).form(&p).send().unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
|
||||
p.insert("password", "hunter2");
|
||||
let resp = cli.post(&login_url).form(&p).send().unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::NO_CONTENT);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
||||
let cookie = SessionCookie::new(resp.headers());
|
||||
info!("cookie: {:?}", cookie);
|
||||
info!("header: {}", cookie.header());
|
||||
|
||||
let resp = cli.get(&format!("{}/api/", &s.base_url))
|
||||
.header(header::COOKIE, cookie.header())
|
||||
.header(reqwest::header::COOKIE, cookie.header())
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::OK);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1301,38 +1296,38 @@ mod tests {
|
||||
p.insert("username", "slamb");
|
||||
p.insert("password", "hunter2");
|
||||
let resp = cli.post(&format!("{}/api/login", &s.base_url)).form(&p).send().unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::NO_CONTENT);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
||||
let cookie = SessionCookie::new(resp.headers());
|
||||
|
||||
// A GET shouldn't work.
|
||||
let resp = cli.get(&format!("{}/api/logout", &s.base_url))
|
||||
.header(header::COOKIE, cookie.header())
|
||||
.header(reqwest::header::COOKIE, cookie.header())
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::METHOD_NOT_ALLOWED);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::METHOD_NOT_ALLOWED);
|
||||
|
||||
// Neither should a POST without a csrf token.
|
||||
let resp = cli.post(&format!("{}/api/logout", &s.base_url))
|
||||
.header(header::COOKIE, cookie.header())
|
||||
.header(reqwest::header::COOKIE, cookie.header())
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||
|
||||
// But it should work with the csrf token.
|
||||
// Retrieve that from the toplevel API request.
|
||||
let toplevel: serde_json::Value = cli.post(&format!("{}/api/", &s.base_url))
|
||||
.header(header::COOKIE, cookie.header())
|
||||
.header(reqwest::header::COOKIE, cookie.header())
|
||||
.send().unwrap()
|
||||
.json().unwrap();
|
||||
let csrf = toplevel.get("session").unwrap().get("csrf").unwrap().as_str();
|
||||
let mut p = HashMap::new();
|
||||
p.insert("csrf", csrf);
|
||||
let resp = cli.post(&format!("{}/api/logout", &s.base_url))
|
||||
.header(header::COOKIE, cookie.header())
|
||||
.header(reqwest::header::COOKIE, cookie.header())
|
||||
.form(&p)
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::NO_CONTENT);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::NO_CONTENT);
|
||||
let mut updated_cookie = cookie.clone();
|
||||
updated_cookie.update(resp.headers());
|
||||
|
||||
@ -1341,10 +1336,10 @@ mod tests {
|
||||
|
||||
// It should also be invalidated server-side.
|
||||
let resp = cli.get(&format!("{}/api/", &s.base_url))
|
||||
.header(header::COOKIE, cookie.header())
|
||||
.header(reqwest::header::COOKIE, cookie.header())
|
||||
.send()
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1357,7 +1352,7 @@ mod tests {
|
||||
let resp = cli.get(
|
||||
&format!("{}/api/cameras/{}/main/view.mp4", &s.base_url, s.db.test_camera_uuid))
|
||||
.send().unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1366,10 +1361,8 @@ mod bench {
|
||||
extern crate test;
|
||||
|
||||
use db::testutil::{self, TestDb};
|
||||
use futures::Future;
|
||||
use hyper;
|
||||
use lazy_static::lazy_static;
|
||||
use std::error::Error as StdError;
|
||||
use uuid::Uuid;
|
||||
|
||||
struct Server {
|
||||
@ -1382,7 +1375,6 @@ mod bench {
|
||||
let db = TestDb::new(::base::clock::RealClocks {});
|
||||
let test_camera_uuid = db.test_camera_uuid;
|
||||
testutil::add_dummy_recordings_to_db(&db.db, 1440);
|
||||
let addr = "127.0.0.1:0".parse().unwrap();
|
||||
let service = super::Service::new(super::Config {
|
||||
db: db.db.clone(),
|
||||
ui_dir: None,
|
||||
@ -1390,12 +1382,22 @@ mod bench {
|
||||
trust_forward_hdrs: false,
|
||||
time_zone_name: "".to_owned(),
|
||||
}).unwrap();
|
||||
let server = hyper::server::Server::bind(&addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(move || Ok::<_, Box<dyn StdError + Send + Sync>>(service.clone()));
|
||||
let addr = server.local_addr(); // resolve port 0 to a real ephemeral port number.
|
||||
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
||||
futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
||||
let mut s = service.clone();
|
||||
move |req| std::pin::Pin::from(s.serve(req))
|
||||
}))
|
||||
});
|
||||
let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let srv = rt.enter(|| {
|
||||
let addr = ([127, 0, 0, 1], 0).into();
|
||||
hyper::server::Server::bind(&addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(make_svc)
|
||||
});
|
||||
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
||||
::std::thread::spawn(move || {
|
||||
::tokio::run(server.map_err(|e| panic!(e)));
|
||||
rt.block_on(srv).unwrap();
|
||||
});
|
||||
Server {
|
||||
base_url: format!("http://{}:{}", addr.ip(), addr.port()),
|
||||
|
Loading…
x
Reference in New Issue
Block a user