mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2024-12-25 06:35:56 -05:00
introduce typed errors and use in mp4 code
Fixes #46. If there are no video_sample_entries, it returns InvalidArgument, which gets mapped to a HTTP 400. Various other failures turn into non-500s as well. There are many places that can & should be using typed errors, but it's a start.
This commit is contained in:
parent
0b0f4ec9ed
commit
f5703b9968
@ -32,6 +32,7 @@
|
||||
|
||||
use failure::Error;
|
||||
use libc;
|
||||
use log::warn;
|
||||
use parking_lot::Mutex;
|
||||
use std::mem;
|
||||
use std::sync::{Arc, mpsc};
|
||||
|
175
base/error.rs
Normal file
175
base/error.rs
Normal file
@ -0,0 +1,175 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2018 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/>.
|
||||
|
||||
use failure::{Backtrace, Context, Fail};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
inner: Context<ErrorKind>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
*self.inner.get_context()
|
||||
}
|
||||
}
|
||||
|
||||
impl Fail for Error {
|
||||
fn cause(&self) -> Option<&Fail> {
|
||||
self.inner.cause()
|
||||
}
|
||||
|
||||
fn backtrace(&self) -> Option<&Backtrace> {
|
||||
self.inner.backtrace()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrorKind> for Error {
|
||||
fn from(kind: ErrorKind) -> Error {
|
||||
Error { inner: Context::new(kind) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Context<ErrorKind>> for Error {
|
||||
fn from(inner: Context<ErrorKind>) -> Error {
|
||||
Error { inner }
|
||||
}
|
||||
}
|
||||
|
||||
/*impl From<failure::Error> for Error {
|
||||
fn from(e: failure::Error) -> Error {
|
||||
Error { inner: e.context(ErrorKind::Unknown) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: std::error::Error + Send + Sync + 'static> From<E> for Error {
|
||||
fn from(e: E) -> Error {
|
||||
let f = e as Fail;
|
||||
Error { inner: f.context(ErrorKind::Unknown) }
|
||||
}
|
||||
}*/
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self.inner.cause() {
|
||||
None => fmt::Display::fmt(&self.kind(), f),
|
||||
Some(c) => write!(f, "{}: {}", self.kind(), c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error kind.
|
||||
///
|
||||
/// These codes are taken from
|
||||
/// [grpc::StatusCode](https://github.com/grpc/grpc/blob/0e00c430827e81d61e1e7164ef04ca21ccbfaa77/include/grpcpp/impl/codegen/status_code_enum.h),
|
||||
/// which is a nice general-purpose classification of errors. See that link for descriptions of
|
||||
/// each error.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
|
||||
pub enum ErrorKind {
|
||||
#[fail(display = "Cancelled")] Cancelled,
|
||||
#[fail(display = "Unknown")] Unknown,
|
||||
#[fail(display = "Invalid argument")] InvalidArgument,
|
||||
#[fail(display = "Deadline exceeded")] DeadlineExceeded,
|
||||
#[fail(display = "Not found")] NotFound,
|
||||
#[fail(display = "Already exists")] AlreadyExists,
|
||||
#[fail(display = "Permission denied")] PermissionDenied,
|
||||
#[fail(display = "Unauthenticated")] Unauthenticated,
|
||||
#[fail(display = "Resource exhausted")] ResourceExhausted,
|
||||
#[fail(display = "Failed precondition")] FailedPrecondition,
|
||||
#[fail(display = "Aborted")] Aborted,
|
||||
#[fail(display = "Out of range")] OutOfRange,
|
||||
#[fail(display = "Unimplemented")] Unimplemented,
|
||||
#[fail(display = "Internal")] Internal,
|
||||
#[fail(display = "Unavailable")] Unavailable,
|
||||
#[fail(display = "Data loss")] DataLoss,
|
||||
#[doc(hidden)] #[fail(display = "__Nonexhaustive")] __Nonexhaustive,
|
||||
}
|
||||
|
||||
/// Extension methods for `Result`.
|
||||
pub trait ResultExt<T, E> {
|
||||
/// Annotates an error with the given kind.
|
||||
/// Example:
|
||||
/// ```
|
||||
/// use moonfire_base::{ErrorKind, ResultExt};
|
||||
/// use std::io::Read;
|
||||
/// let mut buf = [0u8; 1];
|
||||
/// let r = std::io::Cursor::new("").read_exact(&mut buf[..]).err_kind(ErrorKind::Internal);
|
||||
/// assert_eq!(r.unwrap_err().kind(), ErrorKind::Internal);
|
||||
/// ```
|
||||
fn err_kind(self, k: ErrorKind) -> Result<T, Error>;
|
||||
}
|
||||
|
||||
impl<T, E> ResultExt<T, E> for Result<T, E> where E: Into<failure::Error> {
|
||||
fn err_kind(self, k: ErrorKind) -> Result<T, Error> {
|
||||
self.map_err(|e| e.into().context(k).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `failure::bail!`, but the first argument specifies a type as an `ErrorKind`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// use moonfire_base::bail_t;
|
||||
/// let e = || -> Result<(), moonfire_base::Error> {
|
||||
/// bail_t!(Unauthenticated, "unknown user: {}", "slamb");
|
||||
/// }().unwrap_err();
|
||||
/// assert_eq!(e.kind(), moonfire_base::ErrorKind::Unauthenticated);
|
||||
/// assert_eq!(e.to_string(), "Unauthenticated: unknown user: slamb");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! bail_t {
|
||||
($t:ident, $e:expr) => {
|
||||
return Err(failure::err_msg($e).context($crate::ErrorKind::$t).into());
|
||||
};
|
||||
($t:ident, $fmt:expr, $($arg:tt)+) => {
|
||||
return Err(failure::err_msg(format!($fmt, $($arg)+)).context($crate::ErrorKind::$t).into());
|
||||
};
|
||||
}
|
||||
|
||||
/// Like `failure::format_err!`, but the first argument specifies a type as an `ErrorKind`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// use moonfire_base::format_err_t;
|
||||
/// let e = format_err_t!(Unauthenticated, "unknown user: {}", "slamb");
|
||||
/// assert_eq!(e.kind(), moonfire_base::ErrorKind::Unauthenticated);
|
||||
/// assert_eq!(e.to_string(), "Unauthenticated: unknown user: slamb");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! format_err_t {
|
||||
($t:ident, $e:expr) => {
|
||||
Into::<$crate::Error>::into(failure::err_msg($e).context($crate::ErrorKind::$t))
|
||||
};
|
||||
($t:ident, $fmt:expr, $($arg:tt)+) => {
|
||||
Into::<$crate::Error>::into(failure::err_msg(format!($fmt, $($arg)+))
|
||||
.context($crate::ErrorKind::$t))
|
||||
};
|
||||
}
|
@ -28,11 +28,8 @@
|
||||
// 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 failure;
|
||||
extern crate libc;
|
||||
#[macro_use] extern crate log;
|
||||
extern crate parking_lot;
|
||||
extern crate time;
|
||||
|
||||
pub mod clock;
|
||||
#[macro_use] mod error;
|
||||
pub mod strutil;
|
||||
|
||||
pub use crate::error::{Error, ErrorKind, ResultExt};
|
||||
|
@ -30,7 +30,8 @@
|
||||
|
||||
//! Tools for implementing a `http_serve::Entity` body composed from many "slices".
|
||||
|
||||
use failure::Error;
|
||||
use crate::base::Error;
|
||||
use failure::Fail;
|
||||
use futures::{Stream, stream};
|
||||
use hyper::body::Payload;
|
||||
use reffers::ARefs;
|
||||
|
78
src/mp4.rs
78
src/mp4.rs
@ -78,13 +78,12 @@
|
||||
|
||||
extern crate time;
|
||||
|
||||
use crate::base::strutil;
|
||||
use crate::base::{strutil, Error, ErrorKind, ResultExt, bail_t, format_err_t};
|
||||
use bytes::{Buf, BytesMut};
|
||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||
use crate::body::{Chunk, BoxedError, wrap_error};
|
||||
use crate::db::recording::{self, TIME_UNITS_PER_SEC};
|
||||
use crate::db::{self, dir};
|
||||
use failure::Error;
|
||||
use futures::Stream;
|
||||
use futures::stream;
|
||||
use http;
|
||||
@ -372,7 +371,7 @@ impl Segment {
|
||||
fn new(db: &db::LockedDatabase, row: &db::ListRecordingsRow, rel_range_90k: Range<i32>,
|
||||
first_frame_num: u32) -> Result<Self, Error> {
|
||||
Ok(Segment{
|
||||
s: recording::Segment::new(db, row, rel_range_90k)?,
|
||||
s: recording::Segment::new(db, row, rel_range_90k).err_kind(ErrorKind::Unknown)?,
|
||||
index: UnsafeCell::new(Err(())),
|
||||
index_once: ONCE_INIT,
|
||||
first_frame_num,
|
||||
@ -391,7 +390,7 @@ impl Segment {
|
||||
let index: &'a _ = unsafe { &*self.index.get() };
|
||||
match *index {
|
||||
Ok(ref b) => return Ok(f(&b[..], self.lens())),
|
||||
Err(()) => bail!("Unable to build index; see previous error."),
|
||||
Err(()) => bail_t!(Unknown, "Unable to build index; see previous error."),
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,7 +406,7 @@ impl Segment {
|
||||
fn stsz(buf: &[u8], lens: SegmentLengths) -> &[u8] { &buf[lens.stts .. lens.stts + lens.stsz] }
|
||||
fn stss(buf: &[u8], lens: SegmentLengths) -> &[u8] { &buf[lens.stts + lens.stsz ..] }
|
||||
|
||||
fn build_index(&self, playback: &db::RecordingPlayback) -> Result<Box<[u8]>, Error> {
|
||||
fn build_index(&self, playback: &db::RecordingPlayback) -> Result<Box<[u8]>, failure::Error> {
|
||||
let s = &self.s;
|
||||
let lens = self.lens();
|
||||
let len = lens.stts + lens.stsz + lens.stss;
|
||||
@ -456,7 +455,7 @@ impl Segment {
|
||||
|
||||
// TrackRunBox / trun (8.8.8).
|
||||
fn truns(&self, playback: &db::RecordingPlayback, initial_pos: u64, len: usize)
|
||||
-> Result<Vec<u8>, Error> {
|
||||
-> Result<Vec<u8>, failure::Error> {
|
||||
let mut v = Vec::with_capacity(len);
|
||||
|
||||
struct RunInfo {
|
||||
@ -521,7 +520,7 @@ impl Segment {
|
||||
v.write_u32::<BigEndian>(it.bytes as u32)?;
|
||||
data_pos += it.bytes as u64;
|
||||
Ok(())
|
||||
})?;
|
||||
}).err_kind(ErrorKind::Internal)?;
|
||||
if let Some(r) = run_info.take() {
|
||||
// Finish the run as in the non-terminal case above.
|
||||
let p = v.len();
|
||||
@ -600,7 +599,7 @@ enum SliceType {
|
||||
impl Slice {
|
||||
fn new(end: u64, t: SliceType, p: usize) -> Result<Self, Error> {
|
||||
if end >= (1<<40) || p >= (1<<20) {
|
||||
bail!("end={} p={} too large for Slice", end, p);
|
||||
bail_t!(InvalidArgument, "end={} p={} too large for {:?} Slice", end, p, t);
|
||||
}
|
||||
|
||||
Ok(Slice(end | ((t as u64) << 40) | ((p as u64) << 44)))
|
||||
@ -630,7 +629,8 @@ impl Slice {
|
||||
}
|
||||
let truns =
|
||||
mp4.0.db.lock()
|
||||
.with_recording_playback(s.s.id, &mut |playback| s.truns(playback, pos, len))?;
|
||||
.with_recording_playback(s.s.id, &mut |playback| s.truns(playback, pos, len))
|
||||
.err_kind(ErrorKind::Unknown)?;
|
||||
let truns = ARefs::new(truns);
|
||||
Ok(truns.map(|t| &t[r.start as usize .. r.end as usize]).into())
|
||||
}
|
||||
@ -672,7 +672,8 @@ impl slices::Slice for Slice {
|
||||
.map_err(|e| wrap_error(e))
|
||||
.and_then(move |c| {
|
||||
if c.remaining() != (range.end - range.start) as usize {
|
||||
return Err(wrap_error(format_err!(
|
||||
return Err(wrap_error(format_err_t!(
|
||||
Internal,
|
||||
"Error producing {:?}: range {:?} produced incorrect len {}.",
|
||||
self, range, c.remaining())));
|
||||
}
|
||||
@ -757,8 +758,9 @@ impl FileBuilder {
|
||||
rel_range_90k: Range<i32>) -> Result<(), Error> {
|
||||
if let Some(prev) = self.segments.last() {
|
||||
if prev.s.have_trailing_zero() {
|
||||
bail!("unable to append recording {} after recording {} with trailing zero",
|
||||
row.id, prev.s.id);
|
||||
bail_t!(InvalidArgument,
|
||||
"unable to append recording {} after recording {} with trailing zero",
|
||||
row.id, prev.s.id);
|
||||
}
|
||||
}
|
||||
let s = Segment::new(db, &row, rel_range_90k, self.next_frame_num)?;
|
||||
@ -777,15 +779,16 @@ impl FileBuilder {
|
||||
dirs_by_stream_id: Arc<::fnv::FnvHashMap<i32, Arc<dir::SampleFileDir>>>)
|
||||
-> Result<File, Error> {
|
||||
let mut max_end = None;
|
||||
let mut etag = hash::Hasher::new(hash::MessageDigest::sha1())?;
|
||||
etag.update(&FORMAT_VERSION[..])?;
|
||||
let mut etag = hash::Hasher::new(hash::MessageDigest::sha1())
|
||||
.err_kind(ErrorKind::Internal)?;
|
||||
etag.update(&FORMAT_VERSION[..]).err_kind(ErrorKind::Internal)?;
|
||||
if self.include_timestamp_subtitle_track {
|
||||
etag.update(b":ts:")?;
|
||||
etag.update(b":ts:").err_kind(ErrorKind::Internal)?;
|
||||
}
|
||||
match self.type_ {
|
||||
Type::Normal => {},
|
||||
Type::InitSegment => etag.update(b":init:")?,
|
||||
Type::MediaSegment => etag.update(b":media:")?,
|
||||
Type::InitSegment => etag.update(b":init:").err_kind(ErrorKind::Internal)?,
|
||||
Type::MediaSegment => etag.update(b":media:").err_kind(ErrorKind::Internal)?,
|
||||
};
|
||||
for s in &mut self.segments {
|
||||
let d = &s.s.desired_range_90k;
|
||||
@ -809,12 +812,12 @@ impl FileBuilder {
|
||||
// Update the etag to reflect this segment.
|
||||
let mut data = [0_u8; 28];
|
||||
let mut cursor = io::Cursor::new(&mut data[..]);
|
||||
cursor.write_i64::<BigEndian>(s.s.id.0)?;
|
||||
cursor.write_i64::<BigEndian>(s.s.start.0)?;
|
||||
cursor.write_u32::<BigEndian>(s.s.open_id)?;
|
||||
cursor.write_i32::<BigEndian>(d.start)?;
|
||||
cursor.write_i32::<BigEndian>(d.end)?;
|
||||
etag.update(cursor.into_inner())?;
|
||||
cursor.write_i64::<BigEndian>(s.s.id.0).err_kind(ErrorKind::Internal)?;
|
||||
cursor.write_i64::<BigEndian>(s.s.start.0).err_kind(ErrorKind::Internal)?;
|
||||
cursor.write_u32::<BigEndian>(s.s.open_id).err_kind(ErrorKind::Internal)?;
|
||||
cursor.write_i32::<BigEndian>(d.start).err_kind(ErrorKind::Internal)?;
|
||||
cursor.write_i32::<BigEndian>(d.end).err_kind(ErrorKind::Internal)?;
|
||||
etag.update(cursor.into_inner()).err_kind(ErrorKind::Internal)?;
|
||||
}
|
||||
let max_end = match max_end {
|
||||
None => 0,
|
||||
@ -836,8 +839,9 @@ impl FileBuilder {
|
||||
// If the segment is > 4 GiB, the 32-bit trun data offsets are untrustworthy.
|
||||
// We'd need multiple moof+mdat sequences to support large media segments properly.
|
||||
if self.body.slices.len() > u32::max_value() as u64 {
|
||||
bail!("media segment has length {}, greater than allowed 4 GiB",
|
||||
self.body.slices.len());
|
||||
bail_t!(InvalidArgument,
|
||||
"media segment has length {}, greater than allowed 4 GiB",
|
||||
self.body.slices.len());
|
||||
}
|
||||
|
||||
p
|
||||
@ -871,6 +875,7 @@ impl FileBuilder {
|
||||
debug!("slices: {:?}", self.body.slices);
|
||||
let last_modified = ::std::time::UNIX_EPOCH +
|
||||
::std::time::Duration::from_secs(max_end as u64);
|
||||
let etag = etag.finish().err_kind(ErrorKind::Internal)?;
|
||||
Ok(File(Arc::new(FileInner {
|
||||
db,
|
||||
dirs_by_stream_id,
|
||||
@ -880,7 +885,7 @@ impl FileBuilder {
|
||||
video_sample_entries: self.video_sample_entries,
|
||||
initial_sample_byte_pos,
|
||||
last_modified,
|
||||
etag: HeaderValue::from_str(&format!("\"{}\"", &strutil::hex(&etag.finish()?)))
|
||||
etag: HeaderValue::from_str(&format!("\"{}\"", &strutil::hex(&etag)))
|
||||
.expect("hex string should be valid UTF-8"),
|
||||
})))
|
||||
}
|
||||
@ -1051,7 +1056,7 @@ impl FileBuilder {
|
||||
None => Some((e.width, e.height)),
|
||||
Some((w, h)) => Some((cmp::max(w, e.width), cmp::max(h, e.height))),
|
||||
}
|
||||
}).ok_or_else(|| format_err!("No video_sample_entries"))?;
|
||||
}).ok_or_else(|| format_err_t!(InvalidArgument, "no video_sample_entries"))?;
|
||||
self.body.append_u32((width as u32) << 16);
|
||||
self.body.append_u32((height as u32) << 16);
|
||||
})
|
||||
@ -1091,7 +1096,7 @@ impl FileBuilder {
|
||||
let skip = s.s.desired_range_90k.start - actual_start_90k;
|
||||
let keep = s.s.desired_range_90k.end - s.s.desired_range_90k.start;
|
||||
if skip < 0 || keep < 0 {
|
||||
bail!("skip={} keep={} on segment {:#?}", skip, keep, s);
|
||||
bail_t!(Internal, "skip={} keep={} on segment {:#?}", skip, keep, s);
|
||||
}
|
||||
cur_media_time += skip as u64;
|
||||
if unflushed.segment_duration + unflushed.media_time == cur_media_time {
|
||||
@ -1408,7 +1413,7 @@ impl BodyState {
|
||||
|
||||
fn append_slice(&mut self, len: u64, t: SliceType, p: usize) -> Result<(), Error> {
|
||||
let l = self.slices.len();
|
||||
self.slices.append(Slice::new(l + len, t, p)?)
|
||||
self.slices.append(Slice::new(l + len, t, p)?).err_kind(ErrorKind::Internal)
|
||||
}
|
||||
|
||||
/// Appends a static bytestring, flushing the buffer if necessary.
|
||||
@ -1436,7 +1441,7 @@ impl FileInner {
|
||||
let mut v = Vec::with_capacity(l as usize);
|
||||
let mut pos = self.initial_sample_byte_pos;
|
||||
for s in &self.segments {
|
||||
v.write_u64::<BigEndian>(pos)?;
|
||||
v.write_u64::<BigEndian>(pos).err_kind(ErrorKind::Internal)?;
|
||||
let r = s.s.sample_file_range();
|
||||
pos += r.end - r.start;
|
||||
}
|
||||
@ -1456,14 +1461,14 @@ impl FileInner {
|
||||
let s = &self.segments[i];
|
||||
let f = self.dirs_by_stream_id
|
||||
.get(&s.s.id.stream())
|
||||
.ok_or_else(|| format_err!("{}: stream not found", s.s.id))?
|
||||
.open_file(s.s.id)?;
|
||||
.ok_or_else(|| format_err_t!(NotFound, "{}: stream not found", s.s.id))?
|
||||
.open_file(s.s.id).err_kind(ErrorKind::Unknown)?;
|
||||
let start = s.s.sample_file_range().start + r.start;
|
||||
let mmap = Box::new(unsafe {
|
||||
memmap::MmapOptions::new()
|
||||
.offset(start)
|
||||
.len((r.end - r.start) as usize)
|
||||
.map(&f)?
|
||||
.map(&f).err_kind(ErrorKind::Internal)?
|
||||
});
|
||||
use core::ops::Deref;
|
||||
Ok(ARefs::new(mmap).map(|m| m.deref()).into())
|
||||
@ -1477,10 +1482,11 @@ impl FileInner {
|
||||
.unix_seconds();
|
||||
let mut v = Vec::with_capacity(l as usize);
|
||||
for ts in start_sec .. end_sec {
|
||||
v.write_u16::<BigEndian>(SUBTITLE_LENGTH as u16)?;
|
||||
v.write_u16::<BigEndian>(SUBTITLE_LENGTH as u16).expect("Vec write shouldn't fail");
|
||||
let tm = time::at(time::Timespec{sec: ts, nsec: 0});
|
||||
use std::io::Write;
|
||||
write!(v, "{}", tm.strftime(SUBTITLE_TEMPLATE)?)?;
|
||||
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())
|
||||
}
|
||||
@ -2013,7 +2019,7 @@ mod tests {
|
||||
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(), "No video_sample_entries");
|
||||
assert_eq!(e.to_string(), "Invalid argument: no video_sample_entries");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -30,6 +30,7 @@
|
||||
|
||||
//! Tools for implementing a `http_serve::Entity` body composed from many "slices".
|
||||
|
||||
use crate::base::format_err_t;
|
||||
use crate::body::{BoxedError, wrap_error};
|
||||
use failure::Error;
|
||||
use futures::stream;
|
||||
@ -113,8 +114,8 @@ impl<S> Slices<S> where S: Slice {
|
||||
pub fn get_range(&self, ctx: &S::Ctx, range: Range<u64>)
|
||||
-> Box<Stream<Item = S::Chunk, Error = BoxedError> + Send> {
|
||||
if range.start > range.end || range.end > self.len {
|
||||
return Box::new(stream::once(Err(wrap_error(format_err!(
|
||||
"Bad range {:?} for slice of length {}", range, self.len)))));
|
||||
return Box::new(stream::once(Err(wrap_error(format_err_t!(
|
||||
Internal, "Bad range {:?} for slice of length {}", range, self.len)))));
|
||||
}
|
||||
|
||||
// Binary search for the first slice of the range to write, determining its index and
|
||||
|
35
src/web.rs
35
src/web.rs
@ -31,7 +31,7 @@
|
||||
extern crate hyper;
|
||||
|
||||
use crate::base::clock::Clocks;
|
||||
use crate::base::strutil;
|
||||
use crate::base::{ErrorKind, strutil};
|
||||
use crate::body::{Body, BoxedError};
|
||||
use base64;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
@ -164,6 +164,15 @@ fn internal_server_err<E: Into<Error>>(err: E) -> Response<Body> {
|
||||
plain_response(StatusCode::INTERNAL_SERVER_ERROR, err.into().to_string())
|
||||
}
|
||||
|
||||
fn from_base_error(err: base::Error) -> Response<Body> {
|
||||
let status_code = match err.kind() {
|
||||
ErrorKind::InvalidArgument => StatusCode::BAD_REQUEST,
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
plain_response(status_code, err.to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct Segments {
|
||||
ids: Range<i32>,
|
||||
@ -347,7 +356,7 @@ impl ServiceInner {
|
||||
if ent.sha1 == sha1 {
|
||||
builder.append_video_sample_entry(ent.clone());
|
||||
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())
|
||||
.map_err(internal_server_err)?;
|
||||
.map_err(from_base_error)?;
|
||||
return Ok(http_serve::serve(mp4, req));
|
||||
}
|
||||
}
|
||||
@ -458,7 +467,7 @@ impl ServiceInner {
|
||||
};
|
||||
}
|
||||
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())
|
||||
.map_err(internal_server_err)?;
|
||||
.map_err(from_base_error)?;
|
||||
Ok(http_serve::serve(mp4, req))
|
||||
}
|
||||
|
||||
@ -859,11 +868,10 @@ mod tests {
|
||||
}
|
||||
|
||||
impl Server {
|
||||
fn new() -> Server {
|
||||
fn new(require_auth: bool) -> Server {
|
||||
let db = TestDb::new(crate::base::clock::RealClocks {});
|
||||
let (shutdown_tx, shutdown_rx) = futures::sync::oneshot::channel::<()>();
|
||||
let addr = "127.0.0.1:0".parse().unwrap();
|
||||
let require_auth = true;
|
||||
let service = super::Service::new(super::Config {
|
||||
db: db.db.clone(),
|
||||
ui_dir: None,
|
||||
@ -961,7 +969,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unauthorized_without_cookie() {
|
||||
testutil::init();
|
||||
let s = Server::new();
|
||||
let s = Server::new(true);
|
||||
let cli = reqwest::Client::new();
|
||||
let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
||||
@ -970,7 +978,7 @@ mod tests {
|
||||
#[test]
|
||||
fn login() {
|
||||
testutil::init();
|
||||
let s = Server::new();
|
||||
let s = Server::new(true);
|
||||
let cli = reqwest::Client::new();
|
||||
let login_url = format!("{}/api/login", &s.base_url);
|
||||
|
||||
@ -1003,7 +1011,7 @@ mod tests {
|
||||
#[test]
|
||||
fn logout() {
|
||||
testutil::init();
|
||||
let s = Server::new();
|
||||
let s = Server::new(true);
|
||||
let cli = reqwest::Client::new();
|
||||
let mut p = HashMap::new();
|
||||
p.insert("username", "slamb");
|
||||
@ -1054,6 +1062,17 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_without_segments() {
|
||||
testutil::init();
|
||||
let s = Server::new(false);
|
||||
let cli = reqwest::Client::new();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature="nightly"))]
|
||||
|
Loading…
Reference in New Issue
Block a user