rustfmt everything

I want to make the project more accessible by not expecting folks to
match my idiosyncratic style. Now almost [1] everything is written
in the "standard" style. CI enforces this.

[1] "Almost": I used #[rustfmt::skip] in a few sections where I felt
aligning things in columns significantly improves readability.
This commit is contained in:
Scott Lamb 2021-02-16 22:15:54 -08:00
parent 64f8d38e01
commit 97678f42e4
47 changed files with 6541 additions and 3453 deletions

View File

@ -17,6 +17,8 @@ jobs:
include:
- rust: nightly
extra_args: "--features nightly --benches"
- rust: stable
extra_components: rustfmt
runs-on: ubuntu-20.04
steps:
- name: Checkout
@ -37,8 +39,12 @@ jobs:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
components: ${{ matrix.extra_components }}
- name: Test
run: cd server && cargo test ${{ matrix.extra_args }} --all
- name: Check formatting
if: matrix.rust == 'stable'
run: cd server && cargo fmt --all -- --check
js:
name: Build and lint Javascript frontend
runs-on: ubuntu-20.04

View File

@ -35,13 +35,13 @@ use libc;
use log::warn;
use parking_lot::Mutex;
use std::mem;
use std::sync::{Arc, mpsc};
use std::sync::{mpsc, Arc};
use std::thread;
use std::time::Duration as StdDuration;
use time::{Duration, Timespec};
/// Abstract interface to the system clocks. This is for testability.
pub trait Clocks : Send + Sync + 'static {
pub trait Clocks: Send + Sync + 'static {
/// Gets the current time from `CLOCK_REALTIME`.
fn realtime(&self) -> Timespec;
@ -52,19 +52,29 @@ pub trait Clocks : Send + Sync + 'static {
fn sleep(&self, how_long: Duration);
/// Calls `rcv.recv_timeout` or substitutes a test implementation.
fn recv_timeout<T>(&self, rcv: &mpsc::Receiver<T>,
timeout: StdDuration) -> Result<T, mpsc::RecvTimeoutError>;
fn recv_timeout<T>(
&self,
rcv: &mpsc::Receiver<T>,
timeout: StdDuration,
) -> Result<T, mpsc::RecvTimeoutError>;
}
pub fn retry_forever<C, T, E>(clocks: &C, f: &mut dyn FnMut() -> Result<T, E>) -> T
where C: Clocks, E: Into<Error> {
where
C: Clocks,
E: Into<Error>,
{
loop {
let e = match f() {
Ok(t) => return t,
Err(e) => e.into(),
};
let sleep_time = Duration::seconds(1);
warn!("sleeping for {:?} after error: {}", sleep_time, crate::error::prettify_failure(&e));
warn!(
"sleeping for {:?} after error: {}",
sleep_time,
crate::error::prettify_failure(&e)
);
clocks.sleep(sleep_time);
}
}
@ -84,8 +94,12 @@ impl RealClocks {
}
impl Clocks for RealClocks {
fn realtime(&self) -> Timespec { self.get(libc::CLOCK_REALTIME) }
fn monotonic(&self) -> Timespec { self.get(libc::CLOCK_MONOTONIC) }
fn realtime(&self) -> Timespec {
self.get(libc::CLOCK_REALTIME)
}
fn monotonic(&self) -> Timespec {
self.get(libc::CLOCK_MONOTONIC)
}
fn sleep(&self, how_long: Duration) {
match how_long.to_std() {
@ -94,8 +108,11 @@ impl Clocks for RealClocks {
};
}
fn recv_timeout<T>(&self, rcv: &mpsc::Receiver<T>,
timeout: StdDuration) -> Result<T, mpsc::RecvTimeoutError> {
fn recv_timeout<T>(
&self,
rcv: &mpsc::Receiver<T>,
timeout: StdDuration,
) -> Result<T, mpsc::RecvTimeoutError> {
rcv.recv_timeout(timeout)
}
}
@ -119,7 +136,11 @@ impl<'a, C: Clocks + ?Sized, S: AsRef<str>, F: FnOnce() -> S + 'a> TimerGuard<'a
}
impl<'a, C, S, F> Drop for TimerGuard<'a, C, S, F>
where C: Clocks + ?Sized, S: AsRef<str>, F: FnOnce() -> S + 'a {
where
C: Clocks + ?Sized,
S: AsRef<str>,
F: FnOnce() -> S + 'a,
{
fn drop(&mut self) {
let elapsed = self.clocks.monotonic() - self.start;
if elapsed.num_seconds() >= 1 {
@ -148,8 +169,12 @@ impl SimulatedClocks {
}
impl Clocks for SimulatedClocks {
fn realtime(&self) -> Timespec { self.0.boot + *self.0.uptime.lock() }
fn monotonic(&self) -> Timespec { Timespec::new(0, 0) + *self.0.uptime.lock() }
fn realtime(&self) -> Timespec {
self.0.boot + *self.0.uptime.lock()
}
fn monotonic(&self) -> Timespec {
Timespec::new(0, 0) + *self.0.uptime.lock()
}
/// Advances the clock by the specified amount without actually sleeping.
fn sleep(&self, how_long: Duration) {
@ -158,8 +183,11 @@ impl Clocks for SimulatedClocks {
}
/// Advances the clock by the specified amount if data is not immediately available.
fn recv_timeout<T>(&self, rcv: &mpsc::Receiver<T>,
timeout: StdDuration) -> Result<T, mpsc::RecvTimeoutError> {
fn recv_timeout<T>(
&self,
rcv: &mpsc::Receiver<T>,
timeout: StdDuration,
) -> Result<T, mpsc::RecvTimeoutError> {
let r = rcv.recv_timeout(StdDuration::new(0, 0));
if let Err(_) = r {
self.sleep(Duration::from_std(timeout).unwrap());

View File

@ -38,8 +38,11 @@ pub fn prettify_failure(e: &failure::Error) -> String {
write!(&mut msg, "\ncaused by: {}", cause).unwrap();
}
if e.backtrace().is_empty() {
write!(&mut msg, "\n\n(set environment variable RUST_BACKTRACE=1 to see backtraces)")
.unwrap();
write!(
&mut msg,
"\n\n(set environment variable RUST_BACKTRACE=1 to see backtraces)"
)
.unwrap();
} else {
write!(&mut msg, "\n\nBacktrace:\n{}", e.backtrace()).unwrap();
}
@ -73,7 +76,9 @@ impl Fail for Error {
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Error {
Error { inner: Context::new(kind) }
Error {
inner: Context::new(kind),
}
}
}
@ -112,6 +117,8 @@ impl fmt::Display for Error {
/// which is a nice general-purpose classification of errors. See that link for descriptions of
/// each error.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
#[non_exhaustive]
#[rustfmt::skip]
pub enum ErrorKind {
#[fail(display = "Cancelled")] Cancelled,
#[fail(display = "Unknown")] Unknown,
@ -129,7 +136,6 @@ pub enum ErrorKind {
#[fail(display = "Internal")] Internal,
#[fail(display = "Unavailable")] Unavailable,
#[fail(display = "Data loss")] DataLoss,
#[doc(hidden)] #[fail(display = "__Nonexhaustive")] __Nonexhaustive,
}
/// Extension methods for `Result`.
@ -146,7 +152,10 @@ pub trait ResultExt<T, E> {
fn err_kind(self, k: ErrorKind) -> Result<T, Error>;
}
impl<T, E> ResultExt<T, E> for Result<T, E> where E: Into<failure::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())
}

View File

@ -29,8 +29,8 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
pub mod clock;
pub mod time;
mod error;
pub mod strutil;
pub mod time;
pub use crate::error::{Error, ErrorKind, ResultExt, prettify_failure};
pub use crate::error::{prettify_failure, Error, ErrorKind, ResultExt};

View File

@ -28,12 +28,12 @@
// 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 nom::IResult;
use nom::branch::alt;
use nom::bytes::complete::{tag, take_while1};
use nom::character::complete::space0;
use nom::combinator::{map, map_res, opt};
use nom::sequence::{delimited, tuple};
use nom::IResult;
use std::fmt::Write as _;
static MULTIPLIERS: [(char, u64); 4] = [
@ -48,7 +48,7 @@ static MULTIPLIERS: [(char, u64); 4] = [
pub fn encode_size(mut raw: i64) -> String {
let mut encoded = String::new();
for &(c, n) in &MULTIPLIERS {
if raw >= 1i64<<n {
if raw >= 1i64 << n {
write!(&mut encoded, "{}{} ", raw >> n, c).unwrap();
raw &= (1i64 << n) - 1;
}
@ -56,7 +56,7 @@ pub fn encode_size(mut raw: i64) -> String {
if raw > 0 || encoded.len() == 0 {
write!(&mut encoded, "{}", raw).unwrap();
} else {
encoded.pop(); // remove trailing space.
encoded.pop(); // remove trailing space.
}
encoded
}
@ -64,24 +64,24 @@ pub fn encode_size(mut raw: i64) -> String {
fn decode_sizepart(input: &str) -> IResult<&str, i64> {
map(
tuple((
map_res(take_while1(|c: char| c.is_ascii_digit()),
|input: &str| i64::from_str_radix(input, 10)),
map_res(take_while1(|c: char| c.is_ascii_digit()), |input: &str| {
i64::from_str_radix(input, 10)
}),
opt(alt((
nom::combinator::value(1<<40, tag("T")),
nom::combinator::value(1<<30, tag("G")),
nom::combinator::value(1<<20, tag("M")),
nom::combinator::value(1<<10, tag("K"))
)))
nom::combinator::value(1 << 40, tag("T")),
nom::combinator::value(1 << 30, tag("G")),
nom::combinator::value(1 << 20, tag("M")),
nom::combinator::value(1 << 10, tag("K")),
))),
)),
|(n, opt_unit)| n * opt_unit.unwrap_or(1)
|(n, opt_unit)| n * opt_unit.unwrap_or(1),
)(input)
}
fn decode_size_internal(input: &str) -> IResult<&str, i64> {
nom::multi::fold_many1(
delimited(space0, decode_sizepart, space0),
0,
|sum, i| sum + i)(input)
nom::multi::fold_many1(delimited(space0, decode_sizepart, space0), 0, |sum, i| {
sum + i
})(input)
}
/// Decodes a human-readable size as output by encode_size.
@ -95,12 +95,15 @@ pub fn decode_size(encoded: &str) -> Result<i64, ()> {
/// Returns a hex-encoded version of the input.
pub fn hex(raw: &[u8]) -> String {
const HEX_CHARS: [u8; 16] = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7',
b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f'];
#[rustfmt::skip]
const HEX_CHARS: [u8; 16] = [
b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7',
b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f',
];
let mut hex = Vec::with_capacity(2 * raw.len());
for b in raw {
hex.push(HEX_CHARS[((b & 0xf0) >> 4) as usize]);
hex.push(HEX_CHARS[( b & 0x0f ) as usize]);
hex.push(HEX_CHARS[(b & 0x0f) as usize]);
}
unsafe { String::from_utf8_unchecked(hex) }
}
@ -108,8 +111,8 @@ pub fn hex(raw: &[u8]) -> String {
/// Returns [0, 16) or error.
fn dehex_byte(hex_byte: u8) -> Result<u8, ()> {
match hex_byte {
b'0' ..= b'9' => Ok(hex_byte - b'0'),
b'a' ..= b'f' => Ok(hex_byte - b'a' + 10),
b'0'..=b'9' => Ok(hex_byte - b'0'),
b'a'..=b'f' => Ok(hex_byte - b'a' + 10),
_ => Err(()),
}
}
@ -122,7 +125,7 @@ pub fn dehex(hexed: &[u8]) -> Result<[u8; 20], ()> {
}
let mut out = [0; 20];
for i in 0..20 {
out[i] = (dehex_byte(hexed[i<<1])? << 4) + dehex_byte(hexed[(i<<1) + 1])?;
out[i] = (dehex_byte(hexed[i << 1])? << 4) + dehex_byte(hexed[(i << 1) + 1])?;
}
Ok(out)
}

View File

@ -30,13 +30,13 @@
//! Time and durations for Moonfire NVR's internal format.
use failure::{Error, bail, format_err};
use failure::{bail, format_err, Error};
use nom::branch::alt;
use nom::bytes::complete::{tag, take_while_m_n};
use nom::combinator::{map, map_res, opt};
use nom::sequence::{preceded, tuple};
use std::ops;
use std::fmt;
use std::ops;
use std::str::FromStr;
use time;
@ -50,8 +50,10 @@ pub struct Time(pub i64);
/// Returns a parser for a `len`-digit non-negative number which fits into an i32.
fn fixed_len_num<'a>(len: usize) -> impl FnMut(&'a str) -> IResult<&'a str, i32> {
map_res(take_while_m_n(len, len, |c: char| c.is_ascii_digit()),
|input: &str| i32::from_str_radix(input, 10))
map_res(
take_while_m_n(len, len, |c: char| c.is_ascii_digit()),
|input: &str| i32::from_str_radix(input, 10),
)
}
/// Parses `YYYY-mm-dd` into pieces.
@ -59,7 +61,7 @@ fn parse_datepart(input: &str) -> IResult<&str, (i32, i32, i32)> {
tuple((
fixed_len_num(4),
preceded(tag("-"), fixed_len_num(2)),
preceded(tag("-"), fixed_len_num(2))
preceded(tag("-"), fixed_len_num(2)),
))(input)
}
@ -67,9 +69,9 @@ fn parse_datepart(input: &str) -> IResult<&str, (i32, i32, i32)> {
fn parse_timepart(input: &str) -> IResult<&str, (i32, i32, i32, i32)> {
let (input, (hr, _, min)) = tuple((fixed_len_num(2), tag(":"), fixed_len_num(2)))(input)?;
let (input, stuff) = opt(tuple((
preceded(tag(":"), fixed_len_num(2)),
opt(preceded(tag(":"), fixed_len_num(5)))
)))(input)?;
preceded(tag(":"), fixed_len_num(2)),
opt(preceded(tag(":"), fixed_len_num(5))),
)))(input)?;
let (sec, opt_subsec) = stuff.unwrap_or((0, None));
Ok((input, (hr, min, sec, opt_subsec.unwrap_or(0))))
}
@ -77,18 +79,23 @@ fn parse_timepart(input: &str) -> IResult<&str, (i32, i32, i32, i32)> {
/// Parses `Z` (UTC) or `{+,-,}HH:MM` into a time zone offset in seconds.
fn parse_zone(input: &str) -> IResult<&str, i32> {
alt((
nom::combinator::value(0, tag("Z")),
map(
tuple((
opt(nom::character::complete::one_of(&b"+-"[..])),
fixed_len_num(2),
tag(":"),
fixed_len_num(2)
)),
|(sign, hr, _, min)| {
let off = hr * 3600 + min * 60;
if sign == Some('-') { off } else { -off }
})
nom::combinator::value(0, tag("Z")),
map(
tuple((
opt(nom::character::complete::one_of(&b"+-"[..])),
fixed_len_num(2),
tag(":"),
fixed_len_num(2),
)),
|(sign, hr, _, min)| {
let off = hr * 3600 + min * 60;
if sign == Some('-') {
off
} else {
-off
}
},
),
))(input)
}
@ -97,8 +104,12 @@ impl Time {
Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000)
}
pub const fn min_value() -> Self { Time(i64::min_value()) }
pub const fn max_value() -> Self { Time(i64::max_value()) }
pub const fn min_value() -> Self {
Time(i64::min_value())
}
pub const fn max_value() -> Self {
Time(i64::max_value())
}
/// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string.
///
@ -112,20 +123,21 @@ impl Time {
// First try parsing as 90,000ths of a second since epoch.
match i64::from_str(input) {
Ok(i) => return Ok(Time(i)),
Err(_) => {},
Err(_) => {}
}
// If that failed, parse as a time string or bust.
let (remaining, ((tm_year, tm_mon, tm_mday), opt_time, opt_zone)) =
tuple((parse_datepart,
opt(preceded(tag("T"), parse_timepart)),
opt(parse_zone)))(input)
.map_err(|e| match e {
nom::Err::Incomplete(_) => format_err!("incomplete"),
nom::Err::Error(e) | nom::Err::Failure(e) => {
format_err!("{}", nom::error::convert_error(input, e))
}
})?;
let (remaining, ((tm_year, tm_mon, tm_mday), opt_time, opt_zone)) = tuple((
parse_datepart,
opt(preceded(tag("T"), parse_timepart)),
opt(parse_zone),
))(input)
.map_err(|e| match e {
nom::Err::Incomplete(_) => format_err!("incomplete"),
nom::Err::Error(e) | nom::Err::Failure(e) => {
format_err!("{}", nom::error::convert_error(input, e))
}
})?;
if remaining != "" {
bail!("unexpected suffix {:?} following time string", remaining);
}
@ -166,32 +178,44 @@ impl Time {
}
/// Convert to unix seconds by floor method (rounding down).
pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC }
pub fn unix_seconds(&self) -> i64 {
self.0 / TIME_UNITS_PER_SEC
}
}
impl std::str::FromStr for Time {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { Self::parse(s) }
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl ops::Sub for Time {
type Output = Duration;
fn sub(self, rhs: Time) -> Duration { Duration(self.0 - rhs.0) }
fn sub(self, rhs: Time) -> Duration {
Duration(self.0 - rhs.0)
}
}
impl ops::AddAssign<Duration> for Time {
fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
fn add_assign(&mut self, rhs: Duration) {
self.0 += rhs.0
}
}
impl ops::Add<Duration> for Time {
type Output = Time;
fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
fn add(self, rhs: Duration) -> Time {
Time(self.0 + rhs.0)
}
}
impl ops::Sub<Duration> for Time {
type Output = Time;
fn sub(self, rhs: Duration) -> Time { Time(self.0 - rhs.0) }
fn sub(self, rhs: Duration) -> Time {
Time(self.0 - rhs.0)
}
}
impl fmt::Debug for Time {
@ -203,11 +227,20 @@ impl fmt::Debug for Time {
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let tm = time::at(time::Timespec{sec: self.0 / TIME_UNITS_PER_SEC, nsec: 0});
let tm = time::at(time::Timespec {
sec: self.0 / TIME_UNITS_PER_SEC,
nsec: 0,
});
let zone_minutes = tm.tm_utcoff.abs() / 60;
write!(f, "{}:{:05}{}{:02}:{:02}", tm.strftime("%FT%T").or_else(|_| Err(fmt::Error))?,
self.0 % TIME_UNITS_PER_SEC,
if tm.tm_utcoff > 0 { '+' } else { '-' }, zone_minutes / 60, zone_minutes % 60)
write!(
f,
"{}:{:05}{}{:02}:{:02}",
tm.strftime("%FT%T").or_else(|_| Err(fmt::Error))?,
self.0 % TIME_UNITS_PER_SEC,
if tm.tm_utcoff > 0 { '+' } else { '-' },
zone_minutes / 60,
zone_minutes % 60
)
}
}
@ -242,18 +275,33 @@ impl fmt::Display for Duration {
false
};
if hours > 0 {
write!(f, "{}{} hour{}", if have_written { " " } else { "" },
hours, if hours == 1 { "" } else { "s" })?;
write!(
f,
"{}{} hour{}",
if have_written { " " } else { "" },
hours,
if hours == 1 { "" } else { "s" }
)?;
have_written = true;
}
if minutes > 0 {
write!(f, "{}{} minute{}", if have_written { " " } else { "" },
minutes, if minutes == 1 { "" } else { "s" })?;
write!(
f,
"{}{} minute{}",
if have_written { " " } else { "" },
minutes,
if minutes == 1 { "" } else { "s" }
)?;
have_written = true;
}
if seconds > 0 || !have_written {
write!(f, "{}{} second{}", if have_written { " " } else { "" },
seconds, if seconds == 1 { "" } else { "s" })?;
write!(
f,
"{}{} second{}",
if have_written { " " } else { "" },
seconds,
if seconds == 1 { "" } else { "s" }
)?;
}
Ok(())
}
@ -261,15 +309,21 @@ impl fmt::Display for Duration {
impl ops::Add for Duration {
type Output = Duration;
fn add(self, rhs: Duration) -> Duration { Duration(self.0 + rhs.0) }
fn add(self, rhs: Duration) -> Duration {
Duration(self.0 + rhs.0)
}
}
impl ops::AddAssign for Duration {
fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
fn add_assign(&mut self, rhs: Duration) {
self.0 += rhs.0
}
}
impl ops::SubAssign for Duration {
fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 }
fn sub_assign(&mut self, rhs: Duration) {
self.0 -= rhs.0
}
}
#[cfg(test)]
@ -280,17 +334,18 @@ mod tests {
fn test_parse_time() {
std::env::set_var("TZ", "America/Los_Angeles");
time::tzset();
#[rustfmt::skip]
let tests = &[
("2006-01-02T15:04:05-07:00", 102261550050000),
("2006-01-02T15:04:05:00001-07:00", 102261550050001),
("2006-01-02T15:04:05-08:00", 102261874050000),
("2006-01-02T15:04:05", 102261874050000), // implied -08:00
("2006-01-02T15:04", 102261873600000), // implied -08:00
("2006-01-02T15:04:05:00001", 102261874050001), // implied -08:00
("2006-01-02T15:04:05", 102261874050000), // implied -08:00
("2006-01-02T15:04", 102261873600000), // implied -08:00
("2006-01-02T15:04:05:00001", 102261874050001), // implied -08:00
("2006-01-02T15:04:05-00:00", 102259282050000),
("2006-01-02T15:04:05Z", 102259282050000),
("2006-01-02-08:00", 102256992000000), // implied -08:00
("2006-01-02", 102256992000000), // implied -08:00
("2006-01-02-08:00", 102256992000000), // implied -08:00
("2006-01-02", 102256992000000), // implied -08:00
("2006-01-02Z", 102254400000000),
("102261550050000", 102261550050000),
];
@ -303,7 +358,10 @@ mod tests {
fn test_format_time() {
std::env::set_var("TZ", "America/Los_Angeles");
time::tzset();
assert_eq!("2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000)));
assert_eq!(
"2006-01-02T15:04:05:00000-08:00",
format!("{}", Time(102261874050000))
);
}
#[test]

View File

@ -28,17 +28,17 @@
// 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 log::info;
use base::strutil;
use crate::schema::Permissions;
use failure::{Error, bail, format_err};
use base::strutil;
use failure::{bail, format_err, Error};
use fnv::FnvHashMap;
use lazy_static::lazy_static;
use libpasta;
use log::info;
use parking_lot::Mutex;
use protobuf::Message;
use ring::rand::{SecureRandom, SystemRandom};
use rusqlite::{Connection, Transaction, params};
use rusqlite::{params, Connection, Transaction};
use std::collections::BTreeMap;
use std::fmt;
use std::net::IpAddr;
@ -54,8 +54,9 @@ lazy_static! {
/// See also <https://github.com/libpasta/libpasta/issues/9>.
/// Call via `testutil::init()`.
pub(crate) fn set_test_config() {
*PASTA_CONFIG.lock() =
Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2)));
*PASTA_CONFIG.lock() = Arc::new(libpasta::Config::with_primitive(
libpasta::primitives::Bcrypt::new(2),
));
}
enum UserFlag {
@ -91,8 +92,12 @@ impl User {
}
}
pub fn has_password(&self) -> bool { self.password_hash.is_some() }
fn disabled(&self) -> bool { (self.flags & UserFlag::Disabled as i32) != 0 }
pub fn has_password(&self) -> bool {
self.password_hash.is_some()
}
fn disabled(&self) -> bool {
(self.flags & UserFlag::Disabled as i32) != 0
}
}
/// A change to a user.
@ -175,20 +180,18 @@ impl rusqlite::types::FromSql for FromSqlIpAddr {
use rusqlite::types::ValueRef;
match value {
ValueRef::Null => Ok(FromSqlIpAddr(None)),
ValueRef::Blob(ref b) => {
match b.len() {
4 => {
let mut buf = [0u8; 4];
buf.copy_from_slice(b);
Ok(FromSqlIpAddr(Some(buf.into())))
},
16 => {
let mut buf = [0u8; 16];
buf.copy_from_slice(b);
Ok(FromSqlIpAddr(Some(buf.into())))
},
_ => Err(rusqlite::types::FromSqlError::InvalidType),
ValueRef::Blob(ref b) => match b.len() {
4 => {
let mut buf = [0u8; 4];
buf.copy_from_slice(b);
Ok(FromSqlIpAddr(Some(buf.into())))
}
16 => {
let mut buf = [0u8; 16];
buf.copy_from_slice(b);
Ok(FromSqlIpAddr(Some(buf.into())))
}
_ => Err(rusqlite::types::FromSqlError::InvalidType),
},
_ => Err(rusqlite::types::FromSqlError::InvalidType),
}
@ -227,7 +230,7 @@ pub enum RevocationReason {
#[derive(Debug, Default)]
pub struct Session {
user_id: i32,
flags: i32, // bitmask of SessionFlag enum values
flags: i32, // bitmask of SessionFlag enum values
domain: Option<Vec<u8>>,
description: Option<String>,
seed: Seed,
@ -236,7 +239,7 @@ pub struct Session {
creation: Request,
revocation: Request,
revocation_reason: Option<i32>, // see RevocationReason enum
revocation_reason: Option<i32>, // see RevocationReason enum
revocation_reason_detail: Option<String>,
pub permissions: Permissions,
@ -259,7 +262,9 @@ impl Session {
pub struct RawSessionId([u8; 48]);
impl RawSessionId {
pub fn new() -> Self { RawSessionId([0u8; 48]) }
pub fn new() -> Self {
RawSessionId([0u8; 48])
}
pub fn decode_base64(input: &[u8]) -> Result<Self, Error> {
let mut s = RawSessionId::new();
@ -279,11 +284,15 @@ impl RawSessionId {
}
impl AsRef<[u8]> for RawSessionId {
fn as_ref(&self) -> &[u8] { &self.0[..] }
fn as_ref(&self) -> &[u8] {
&self.0[..]
}
}
impl AsMut<[u8]> for RawSessionId {
fn as_mut(&mut self) -> &mut [u8] { &mut self.0[..] }
fn as_mut(&mut self) -> &mut [u8] {
&mut self.0[..]
}
}
impl fmt::Debug for RawSessionId {
@ -319,7 +328,11 @@ impl fmt::Debug for SessionHash {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
let mut buf = [0; 32];
self.encode_base64(&mut buf);
write!(f, "SessionHash(\"{}\")", ::std::str::from_utf8(&buf[..]).expect("base64 is UTF-8"))
write!(
f,
"SessionHash(\"{}\")",
::std::str::from_utf8(&buf[..]).expect("base64 is UTF-8")
)
}
}
@ -330,8 +343,9 @@ impl rusqlite::types::FromSql for Seed {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let b = value.as_blob()?;
if b.len() != 32 {
return Err(rusqlite::types::FromSqlError::Other(
Box::new(format_err!("expected a 32-byte seed").compat())));
return Err(rusqlite::types::FromSqlError::Other(Box::new(
format_err!("expected a 32-byte seed").compat(),
)));
}
let mut s = Seed::default();
s.0.copy_from_slice(b);
@ -363,7 +377,8 @@ impl State {
sessions: FnvHashMap::default(),
rand: ring::rand::SystemRandom::new(),
};
let mut stmt = conn.prepare(r#"
let mut stmt = conn.prepare(
r#"
select
id,
username,
@ -375,24 +390,28 @@ impl State {
permissions
from
user
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let id = row.get(0)?;
let name: String = row.get(1)?;
let mut permissions = Permissions::new();
permissions.merge_from_bytes(row.get_raw_checked(7)?.as_blob()?)?;
state.users_by_id.insert(id, User {
state.users_by_id.insert(
id,
username: name.clone(),
flags: row.get(2)?,
password_hash: row.get(3)?,
password_id: row.get(4)?,
password_failure_count: row.get(5)?,
unix_uid: row.get(6)?,
dirty: false,
permissions,
});
User {
id,
username: name.clone(),
flags: row.get(2)?,
password_hash: row.get(3)?,
password_id: row.get(4)?,
password_failure_count: row.get(5)?,
unix_uid: row.get(6)?,
dirty: false,
permissions,
},
);
state.users_by_name.insert(name, id);
}
Ok(state)
@ -406,11 +425,18 @@ impl State {
}
}
pub fn users_by_id(&self) -> &BTreeMap<i32, User> { &self.users_by_id }
pub fn users_by_id(&self) -> &BTreeMap<i32, User> {
&self.users_by_id
}
fn update_user(&mut self, conn: &Connection, id: i32, change: UserChange)
-> Result<&User, Error> {
let mut stmt = conn.prepare_cached(r#"
fn update_user(
&mut self,
conn: &Connection,
id: i32,
change: UserChange,
) -> Result<&User, Error> {
let mut stmt = conn.prepare_cached(
r#"
update user
set
username = :username,
@ -422,7 +448,8 @@ impl State {
permissions = :permissions
where
id = :id
"#)?;
"#,
)?;
let e = self.users_by_id.entry(id);
let e = match e {
::std::collections::btree_map::Entry::Vacant(_) => panic!("missing uid {}!", id),
@ -433,10 +460,13 @@ impl State {
None => {
let u = e.get();
(&u.password_hash, u.password_id, u.password_failure_count)
},
}
Some(h) => (h, e.get().password_id + 1, 0),
};
let permissions = change.permissions.write_to_bytes().expect("proto3->vec is infallible");
let permissions = change
.permissions
.write_to_bytes()
.expect("proto3->vec is infallible");
stmt.execute_named(&[
(":username", &&change.username[..]),
(":password_hash", phash),
@ -462,12 +492,17 @@ impl State {
}
fn add_user(&mut self, conn: &Connection, change: UserChange) -> Result<&User, Error> {
let mut stmt = conn.prepare_cached(r#"
let mut stmt = conn.prepare_cached(
r#"
insert into user (username, password_hash, flags, unix_uid, permissions)
values (:username, :password_hash, :flags, :unix_uid, :permissions)
"#)?;
"#,
)?;
let password_hash = change.set_password_hash.unwrap_or(None);
let permissions = change.permissions.write_to_bytes().expect("proto3->vec is infallible");
let permissions = change
.permissions
.write_to_bytes()
.expect("proto3->vec is infallible");
stmt.execute_named(&[
(":username", &&change.username[..]),
(":password_hash", &password_hash),
@ -512,17 +547,30 @@ impl State {
}
pub fn get_user(&self, username: &str) -> Option<&User> {
self.users_by_name
.get(username)
.map(|id| self.users_by_id.get(id).expect("users_by_name implies users_by_id"))
self.users_by_name.get(username).map(|id| {
self.users_by_id
.get(id)
.expect("users_by_name implies users_by_id")
})
}
pub fn login_by_password(&mut self, conn: &Connection, req: Request, username: &str,
password: String, domain: Option<Vec<u8>>, session_flags: i32)
-> Result<(RawSessionId, &Session), Error> {
let id = self.users_by_name.get(username)
pub fn login_by_password(
&mut self,
conn: &Connection,
req: Request,
username: &str,
password: String,
domain: Option<Vec<u8>>,
session_flags: i32,
) -> Result<(RawSessionId, &Session), Error> {
let id = self
.users_by_name
.get(username)
.ok_or_else(|| format_err!("no such user {:?}", username))?;
let u = self.users_by_id.get_mut(id).expect("users_by_name implies users_by_id");
let u = self
.users_by_id
.get_mut(id)
.expect("users_by_name implies users_by_id");
if u.disabled() {
bail!("user {:?} is disabled", username);
}
@ -537,7 +585,7 @@ impl State {
u.dirty = true;
u.password_failure_count += 1;
bail!("incorrect password for user {:?}", username);
},
}
libpasta::HashUpdate::Verified(new_pwd) => new_pwd,
}
};
@ -546,34 +594,67 @@ impl State {
u.dirty = true;
}
let password_id = u.password_id;
State::make_session_int(&self.rand, conn, req, u, domain, Some(password_id), session_flags,
&mut self.sessions, u.permissions.clone())
State::make_session_int(
&self.rand,
conn,
req,
u,
domain,
Some(password_id),
session_flags,
&mut self.sessions,
u.permissions.clone(),
)
}
/// Makes a session directly (no password required).
pub fn make_session<'s>(&'s mut self, conn: &Connection, creation: Request, uid: i32,
domain: Option<Vec<u8>>, flags: i32, permissions: Permissions)
-> Result<(RawSessionId, &'s Session), Error> {
let u = self.users_by_id.get_mut(&uid).ok_or_else(|| format_err!("no such uid {:?}", uid))?;
pub fn make_session<'s>(
&'s mut self,
conn: &Connection,
creation: Request,
uid: i32,
domain: Option<Vec<u8>>,
flags: i32,
permissions: Permissions,
) -> Result<(RawSessionId, &'s Session), Error> {
let u = self
.users_by_id
.get_mut(&uid)
.ok_or_else(|| format_err!("no such uid {:?}", uid))?;
if u.disabled() {
bail!("user is disabled");
}
State::make_session_int(&self.rand, conn, creation, u, domain, None, flags,
&mut self.sessions, permissions)
State::make_session_int(
&self.rand,
conn,
creation,
u,
domain,
None,
flags,
&mut self.sessions,
permissions,
)
}
fn make_session_int<'s>(rand: &SystemRandom, conn: &Connection, creation: Request,
user: &mut User, domain: Option<Vec<u8>>,
creation_password_id: Option<i32>, flags: i32,
sessions: &'s mut FnvHashMap<SessionHash, Session>,
permissions: Permissions)
-> Result<(RawSessionId, &'s Session), Error> {
fn make_session_int<'s>(
rand: &SystemRandom,
conn: &Connection,
creation: Request,
user: &mut User,
domain: Option<Vec<u8>>,
creation_password_id: Option<i32>,
flags: i32,
sessions: &'s mut FnvHashMap<SessionHash, Session>,
permissions: Permissions,
) -> Result<(RawSessionId, &'s Session), Error> {
let mut session_id = RawSessionId::new();
rand.fill(&mut session_id.0).unwrap();
let mut seed = [0u8; 32];
rand.fill(&mut seed).unwrap();
let hash = session_id.hash();
let mut stmt = conn.prepare_cached(r#"
let mut stmt = conn.prepare_cached(
r#"
insert into user_session (session_id_hash, user_id, seed, flags, domain,
creation_password_id, creation_time_sec,
creation_user_agent, creation_peer_addr,
@ -582,10 +663,13 @@ impl State {
:creation_password_id, :creation_time_sec,
:creation_user_agent, :creation_peer_addr,
:permissions)
"#)?;
"#,
)?;
let addr = creation.addr_buf();
let addr: Option<&[u8]> = addr.as_ref().map(|a| a.as_ref());
let permissions_blob = permissions.write_to_bytes().expect("proto3->vec is infallible");
let permissions_blob = permissions
.write_to_bytes()
.expect("proto3->vec is infallible");
stmt.execute_named(&[
(":session_id_hash", &&hash.0[..]),
(":user_id", &user.id),
@ -615,8 +699,12 @@ impl State {
Ok((session_id, session))
}
pub fn authenticate_session(&mut self, conn: &Connection, req: Request, hash: &SessionHash)
-> Result<(&Session, &User), Error> {
pub fn authenticate_session(
&mut self,
conn: &Connection,
req: Request,
hash: &SessionHash,
) -> Result<(&Session, &User), Error> {
let s = match self.sessions.entry(*hash) {
::std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
::std::collections::hash_map::Entry::Vacant(e) => e.insert(lookup_session(conn, hash)?),
@ -637,15 +725,21 @@ impl State {
Ok((s, u))
}
pub fn revoke_session(&mut self, conn: &Connection, reason: RevocationReason,
detail: Option<String>, req: Request, hash: &SessionHash)
-> Result<(), Error> {
pub fn revoke_session(
&mut self,
conn: &Connection,
reason: RevocationReason,
detail: Option<String>,
req: Request,
hash: &SessionHash,
) -> Result<(), Error> {
let s = match self.sessions.entry(*hash) {
::std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
::std::collections::hash_map::Entry::Vacant(e) => e.insert(lookup_session(conn, hash)?),
};
if s.revocation_reason.is_none() {
let mut stmt = conn.prepare(r#"
let mut stmt = conn.prepare(
r#"
update user_session
set
revocation_time_sec = ?,
@ -655,7 +749,8 @@ impl State {
revocation_reason_detail = ?
where
session_id_hash = ?
"#)?;
"#,
)?;
let addr = req.addr_buf();
let addr: Option<&[u8]> = addr.as_ref().map(|a| a.as_ref());
stmt.execute(params![
@ -677,15 +772,18 @@ impl State {
/// The caller is expected to call `post_flush` afterward if the transaction is
/// successfully committed.
pub fn flush(&self, tx: &Transaction) -> Result<(), Error> {
let mut u_stmt = tx.prepare(r#"
let mut u_stmt = tx.prepare(
r#"
update user
set
password_failure_count = :password_failure_count,
password_hash = :password_hash
where
id = :id
"#)?;
let mut s_stmt = tx.prepare(r#"
"#,
)?;
let mut s_stmt = tx.prepare(
r#"
update user_session
set
last_use_time_sec = :last_use_time_sec,
@ -694,12 +792,16 @@ impl State {
use_count = :use_count
where
session_id_hash = :hash
"#)?;
"#,
)?;
for (&id, u) in &self.users_by_id {
if !u.dirty {
continue;
}
info!("flushing user with hash: {}", u.password_hash.as_ref().unwrap());
info!(
"flushing user with hash: {}",
u.password_hash.as_ref().unwrap()
);
u_stmt.execute_named(&[
(":password_failure_count", &u.password_failure_count),
(":password_hash", &u.password_hash),
@ -736,7 +838,8 @@ impl State {
}
fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Error> {
let mut stmt = conn.prepare_cached(r#"
let mut stmt = conn.prepare_cached(
r#"
select
user_id,
seed,
@ -761,7 +864,8 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
user_session
where
session_id_hash = ?
"#)?;
"#,
)?;
let mut rows = stmt.query(params![&hash.0[..]])?;
let row = rows.next()?.ok_or_else(|| format_err!("no such session"))?;
let creation_addr: FromSqlIpAddr = row.get(8)?;
@ -801,10 +905,10 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
#[cfg(test)]
mod tests {
use crate::db;
use rusqlite::Connection;
use super::*;
use crate::db;
use crate::testutil;
use rusqlite::Connection;
#[test]
fn open_empty_db() {
@ -823,43 +927,82 @@ mod tests {
let mut state = State::init(&conn).unwrap();
let req = Request {
when_sec: Some(42),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(127, 0, 0, 1))),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(
127, 0, 0, 1,
))),
user_agent: Some(b"some ua".to_vec()),
};
let (uid, mut c) = {
let u = state.apply(&conn, UserChange::add_user("slamb".to_owned())).unwrap();
let u = state
.apply(&conn, UserChange::add_user("slamb".to_owned()))
.unwrap();
(u.id, u.change())
};
let e = state.login_by_password(&conn, req.clone(), "slamb", "hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap_err();
let e = state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap_err();
assert_eq!(format!("{}", e), "no password set for user \"slamb\"");
c.set_password("hunter2".to_owned());
state.apply(&conn, c).unwrap();
let e = state.login_by_password(&conn, req.clone(), "slamb",
"hunter3".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap_err();
let e = state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter3".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap_err();
assert_eq!(format!("{}", e), "incorrect password for user \"slamb\"");
let sid = {
let (sid, s) = state.login_by_password(&conn, req.clone(), "slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap();
let (sid, s) = state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap();
assert_eq!(s.user_id, uid);
sid
};
{
let (_, u) = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap();
let (_, u) = state
.authenticate_session(&conn, req.clone(), &sid.hash())
.unwrap();
assert_eq!(u.id, uid);
}
state.revoke_session(&conn, RevocationReason::LoggedOut, None, req.clone(),
&sid.hash()).unwrap();
let e = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap_err();
state
.revoke_session(
&conn,
RevocationReason::LoggedOut,
None,
req.clone(),
&sid.hash(),
)
.unwrap();
let e = state
.authenticate_session(&conn, req.clone(), &sid.hash())
.unwrap_err();
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
// Everything should persist across reload.
drop(state);
let mut state = State::init(&conn).unwrap();
let e = state.authenticate_session(&conn, req, &sid.hash()).unwrap_err();
let e = state
.authenticate_session(&conn, req, &sid.hash())
.unwrap_err();
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
}
@ -871,7 +1014,9 @@ mod tests {
let mut state = State::init(&conn).unwrap();
let req = Request {
when_sec: Some(42),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(127, 0, 0, 1))),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(
127, 0, 0, 1,
))),
user_agent: Some(b"some ua".to_vec()),
};
{
@ -879,25 +1024,43 @@ mod tests {
c.set_password("hunter2".to_owned());
state.apply(&conn, c).unwrap();
};
let sid = state.login_by_password(&conn, req.clone(), "slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap().0;
state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap();
let sid = state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap()
.0;
state
.authenticate_session(&conn, req.clone(), &sid.hash())
.unwrap();
// Reload.
drop(state);
let mut state = State::init(&conn).unwrap();
state.revoke_session(&conn, RevocationReason::LoggedOut, None, req.clone(),
&sid.hash()).unwrap();
let e = state.authenticate_session(&conn, req, &sid.hash()).unwrap_err();
state
.revoke_session(
&conn,
RevocationReason::LoggedOut,
None,
req.clone(),
&sid.hash(),
)
.unwrap();
let e = state
.authenticate_session(&conn, req, &sid.hash())
.unwrap_err();
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
}
#[test]
fn upgrade_hash() {
// This hash is generated with cost=1 vs the cost=2 of PASTA_CONFIG.
let insecure_hash =
libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(1))
let insecure_hash = libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(1))
.hash_password("hunter2");
testutil::init();
let mut conn = Connection::open_in_memory().unwrap();
@ -915,11 +1078,21 @@ mod tests {
let req = Request {
when_sec: Some(42),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(127, 0, 0, 1))),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(
127, 0, 0, 1,
))),
user_agent: Some(b"some ua".to_vec()),
};
state.login_by_password(&conn, req.clone(), "slamb", "hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap();
state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap();
let new_hash = {
// Password should have been automatically upgraded.
let u = state.users_by_id().get(&uid).unwrap();
@ -944,8 +1117,16 @@ mod tests {
}
// Login should still work.
state.login_by_password(&conn, req.clone(), "slamb", "hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap();
state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap();
}
#[test]
@ -956,7 +1137,9 @@ mod tests {
let mut state = State::init(&conn).unwrap();
let req = Request {
when_sec: Some(42),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(127, 0, 0, 1))),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(
127, 0, 0, 1,
))),
user_agent: Some(b"some ua".to_vec()),
};
let uid = {
@ -966,9 +1149,17 @@ mod tests {
};
// Get a session for later.
let sid = state.login_by_password(&conn, req.clone(), "slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap().0;
let sid = state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap()
.0;
// Disable the user.
{
@ -978,19 +1169,30 @@ mod tests {
}
// Fresh logins shouldn't work.
let e = state.login_by_password(&conn, req.clone(), "slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap_err();
let e = state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap_err();
assert_eq!(format!("{}", e), "user \"slamb\" is disabled");
// Authenticating existing sessions shouldn't work either.
let e = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap_err();
let e = state
.authenticate_session(&conn, req.clone(), &sid.hash())
.unwrap_err();
assert_eq!(format!("{}", e), "user \"slamb\" is disabled");
// The user should still be disabled after reload.
drop(state);
let mut state = State::init(&conn).unwrap();
let e = state.authenticate_session(&conn, req, &sid.hash()).unwrap_err();
let e = state
.authenticate_session(&conn, req, &sid.hash())
.unwrap_err();
assert_eq!(format!("{}", e), "user \"slamb\" is disabled");
}
@ -1002,7 +1204,9 @@ mod tests {
let mut state = State::init(&conn).unwrap();
let req = Request {
when_sec: Some(42),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(127, 0, 0, 1))),
addr: Some(::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(
127, 0, 0, 1,
))),
user_agent: Some(b"some ua".to_vec()),
};
let uid = {
@ -1012,20 +1216,31 @@ mod tests {
};
// Get a session for later.
let (sid, _) = state.login_by_password(&conn, req.clone(), "slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()), 0).unwrap();
let (sid, _) = state
.login_by_password(
&conn,
req.clone(),
"slamb",
"hunter2".to_owned(),
Some(b"nvr.example.com".to_vec()),
0,
)
.unwrap();
state.delete_user(&mut conn, uid).unwrap();
assert!(state.users_by_id().get(&uid).is_none());
let e = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap_err();
let e = state
.authenticate_session(&conn, req.clone(), &sid.hash())
.unwrap_err();
assert_eq!(format!("{}", e), "no such session");
// The user should still be deleted after reload.
drop(state);
let mut state = State::init(&conn).unwrap();
assert!(state.users_by_id().get(&uid).is_none());
let e = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap_err();
let e = state
.authenticate_session(&conn, req.clone(), &sid.hash())
.unwrap_err();
assert_eq!(format!("{}", e), "no such session");
}

View File

@ -35,12 +35,12 @@ use crate::db::{self, CompositeId, FromSqlUuid};
use crate::dir;
use crate::raw;
use crate::recording;
use crate::schema;
use failure::Error;
use fnv::{FnvHashMap, FnvHashSet};
use log::{info, error, warn};
use log::{error, info, warn};
use nix::fcntl::AtFlags;
use rusqlite::params;
use crate::schema;
use std::os::unix::io::AsRawFd;
pub struct Options {
@ -53,7 +53,7 @@ pub struct Options {
#[derive(Default)]
pub struct Context {
rows_to_delete: FnvHashSet<CompositeId>,
files_to_trash: FnvHashSet<(i32, CompositeId)>, // (dir_id, composite_id)
files_to_trash: FnvHashSet<(i32, CompositeId)>, // (dir_id, composite_id)
}
pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error> {
@ -65,7 +65,9 @@ pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let e: String = row.get(0)?;
if e == "ok" { continue; }
if e == "ok" {
continue;
}
error!("{}", e);
printed_error = true;
}
@ -101,12 +103,14 @@ pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error
// Scan directories.
let mut dirs_by_id: FnvHashMap<i32, Dir> = FnvHashMap::default();
{
let mut dir_stmt = conn.prepare(r#"
let mut dir_stmt = conn.prepare(
r#"
select d.id, d.path, d.uuid, d.last_complete_open_id, o.uuid
from sample_file_dir d left join open o on (d.last_complete_open_id = o.id)
"#)?;
let mut garbage_stmt = conn.prepare_cached(
"select composite_id from garbage where sample_file_dir_id = ?")?;
"#,
)?;
let mut garbage_stmt =
conn.prepare_cached("select composite_id from garbage where sample_file_dir_id = ?")?;
let mut rows = dir_stmt.query(params![])?;
while let Some(row) = rows.next()? {
let mut meta = schema::DirMeta::default();
@ -131,8 +135,10 @@ pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error
while let Some(row) = rows.next()? {
let id = CompositeId(row.get(0)?);
let s = streams.entry(id.stream()).or_insert_with(Stream::default);
s.recordings.entry(id.recording()).or_insert_with(Recording::default).garbage_row =
true;
s.recordings
.entry(id.recording())
.or_insert_with(Recording::default)
.garbage_row = true;
}
dirs_by_id.insert(dir_id, streams);
}
@ -141,7 +147,8 @@ pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error
// Scan known streams.
let mut ctx = Context::default();
{
let mut stmt = conn.prepare(r#"
let mut stmt = conn.prepare(
r#"
select
id,
sample_file_dir_id,
@ -150,7 +157,8 @@ pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error
stream
where
sample_file_dir_id is not null
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let stream_id = row.get(0)?;
@ -170,9 +178,15 @@ pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error
for (&stream_id, stream) in streams {
for (&recording_id, r) in &stream.recordings {
let id = CompositeId::new(stream_id, recording_id);
if r.recording_row.is_some() || r.playback_row.is_some() ||
r.integrity_row || !r.garbage_row {
error!("dir {} recording {} for unknown stream: {:#?}", dir_id, id, r);
if r.recording_row.is_some()
|| r.playback_row.is_some()
|| r.integrity_row
|| !r.garbage_row
{
error!(
"dir {} recording {} for unknown stream: {:#?}",
dir_id, id, r
);
printed_error = true;
}
}
@ -195,7 +209,8 @@ pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error
if !ctx.files_to_trash.is_empty() {
info!("Trashing {} recording files", ctx.files_to_trash.len());
let mut g = tx.prepare(
"insert or ignore into garbage (sample_file_dir_id, composite_id) values (?, ?)")?;
"insert or ignore into garbage (sample_file_dir_id, composite_id) values (?, ?)",
)?;
for (dir_id, composite_id) in &ctx.files_to_trash {
g.execute(params![dir_id, composite_id.0])?;
}
@ -259,7 +274,11 @@ fn summarize_index(video_index: &[u8]) -> Result<RecordingSummary, Error> {
video_samples,
video_sync_samples,
media_duration,
flags: if it.duration_90k == 0 { db::RecordingFlags::TrailingZero as i32 } else { 0 },
flags: if it.duration_90k == 0 {
db::RecordingFlags::TrailingZero as i32
} else {
0
},
})
}
@ -275,35 +294,53 @@ fn read_dir(d: &dir::SampleFileDir, opts: &Options) -> Result<Dir, Error> {
let f = e.file_name();
match f.to_bytes() {
b"." | b".." | b"meta" => continue,
_ => {},
_ => {}
};
let id = match dir::parse_id(f.to_bytes()) {
Ok(id) => id,
Err(_) => {
error!("sample file directory contains file {:?} which isn't an id", f);
error!(
"sample file directory contains file {:?} which isn't an id",
f
);
continue;
}
};
let len = if opts.compare_lens {
nix::sys::stat::fstatat(fd, f, AtFlags::empty())?.st_size as u64
} else { 0 };
} else {
0
};
let stream = dir.entry(id.stream()).or_insert_with(Stream::default);
stream.recordings.entry(id.recording()).or_insert_with(Recording::default).file = Some(len);
stream
.recordings
.entry(id.recording())
.or_insert_with(Recording::default)
.file = Some(len);
}
Ok(dir)
}
/// Looks through a known stream for errors.
fn compare_stream(conn: &rusqlite::Connection, dir_id: i32, stream_id: i32, opts: &Options,
mut stream: Stream, ctx: &mut Context) -> Result<bool, Error> {
fn compare_stream(
conn: &rusqlite::Connection,
dir_id: i32,
stream_id: i32,
opts: &Options,
mut stream: Stream,
ctx: &mut Context,
) -> Result<bool, Error> {
let start = CompositeId::new(stream_id, 0);
let end = CompositeId::new(stream_id, i32::max_value());
let mut printed_error = false;
let cum_recordings = stream.cum_recordings.expect("cum_recordings must be set on known stream");
let cum_recordings = stream
.cum_recordings
.expect("cum_recordings must be set on known stream");
// recording row.
{
let mut stmt = conn.prepare_cached(r#"
let mut stmt = conn.prepare_cached(
r#"
select
composite_id,
flags,
@ -315,7 +352,8 @@ fn compare_stream(conn: &rusqlite::Connection, dir_id: i32, stream_id: i32, opts
recording
where
composite_id between ? and ?
"#)?;
"#,
)?;
let mut rows = stmt.query(params![start.0, end.0])?;
while let Some(row) = rows.next()? {
let id = CompositeId(row.get(0)?);
@ -326,15 +364,18 @@ fn compare_stream(conn: &rusqlite::Connection, dir_id: i32, stream_id: i32, opts
video_samples: row.get(4)?,
video_sync_samples: row.get(5)?,
};
stream.recordings.entry(id.recording())
.or_insert_with(Recording::default)
.recording_row = Some(s);
stream
.recordings
.entry(id.recording())
.or_insert_with(Recording::default)
.recording_row = Some(s);
}
}
// recording_playback row.
{
let mut stmt = conn.prepare_cached(r#"
let mut stmt = conn.prepare_cached(
r#"
select
composite_id,
video_index
@ -342,7 +383,8 @@ fn compare_stream(conn: &rusqlite::Connection, dir_id: i32, stream_id: i32, opts
recording_playback
where
composite_id between ? and ?
"#)?;
"#,
)?;
let mut rows = stmt.query(params![start.0, end.0])?;
while let Some(row) = rows.next()? {
let id = CompositeId(row.get(0)?);
@ -357,30 +399,36 @@ fn compare_stream(conn: &rusqlite::Connection, dir_id: i32, stream_id: i32, opts
ctx.files_to_trash.insert((dir_id, id));
}
continue;
},
}
};
stream.recordings.entry(id.recording())
.or_insert_with(Recording::default)
.playback_row = Some(s);
stream
.recordings
.entry(id.recording())
.or_insert_with(Recording::default)
.playback_row = Some(s);
}
}
// recording_integrity row.
{
let mut stmt = conn.prepare_cached(r#"
let mut stmt = conn.prepare_cached(
r#"
select
composite_id
from
recording_integrity
where
composite_id between ? and ?
"#)?;
"#,
)?;
let mut rows = stmt.query(params![start.0, end.0])?;
while let Some(row) = rows.next()? {
let id = CompositeId(row.get(0)?);
stream.recordings.entry(id.recording())
.or_insert_with(Recording::default)
.integrity_row = true;
stream
.recordings
.entry(id.recording())
.or_insert_with(Recording::default)
.integrity_row = true;
}
}
@ -400,14 +448,15 @@ fn compare_stream(conn: &rusqlite::Connection, dir_id: i32, stream_id: i32, opts
continue;
}
r
},
}
None => {
if db_rows_expected {
error!("Missing recording row for {}: {:#?}", id, recording);
if opts.trash_orphan_sample_files {
ctx.files_to_trash.insert((dir_id, id));
}
if opts.delete_orphan_rows { // also delete playback/integrity rows, if any.
if opts.delete_orphan_rows {
// also delete playback/integrity rows, if any.
ctx.rows_to_delete.insert(id);
}
printed_error = true;
@ -419,38 +468,44 @@ fn compare_stream(conn: &rusqlite::Connection, dir_id: i32, stream_id: i32, opts
printed_error = true;
}
continue;
},
}
};
match recording.playback_row {
Some(ref p) => {
if r != p {
error!("Recording {} summary doesn't match video_index: {:#?}", id, recording);
error!(
"Recording {} summary doesn't match video_index: {:#?}",
id, recording
);
printed_error = true;
}
},
}
None => {
error!("Recording {} missing playback row: {:#?}", id, recording);
printed_error = true;
if opts.trash_orphan_sample_files {
ctx.files_to_trash.insert((dir_id, id));
}
if opts.delete_orphan_rows { // also delete recording/integrity rows, if any.
if opts.delete_orphan_rows {
// also delete recording/integrity rows, if any.
ctx.rows_to_delete.insert(id);
}
},
}
}
match recording.file {
Some(len) => if opts.compare_lens && r.bytes != len {
error!("Recording {} length mismatch: {:#?}", id, recording);
printed_error = true;
},
Some(len) => {
if opts.compare_lens && r.bytes != len {
error!("Recording {} length mismatch: {:#?}", id, recording);
printed_error = true;
}
}
None => {
error!("Recording {} missing file: {:#?}", id, recording);
if opts.delete_orphan_rows {
ctx.rows_to_delete.insert(id);
}
printed_error = true;
},
}
}
}

View File

@ -34,28 +34,35 @@
/// encoding](https://developers.google.com/protocol-buffers/docs/encoding#types). Uses the low bit
/// to indicate signedness (1 = negative, 0 = non-negative).
#[inline(always)]
pub fn zigzag32(i: i32) -> u32 { ((i << 1) as u32) ^ ((i >> 31) as u32) }
pub fn zigzag32(i: i32) -> u32 {
((i << 1) as u32) ^ ((i >> 31) as u32)
}
/// Zigzag-decodes to a signed integer.
/// See `zigzag`.
#[inline(always)]
pub fn unzigzag32(i: u32) -> i32 { ((i >> 1) as i32) ^ -((i & 1) as i32) }
pub fn unzigzag32(i: u32) -> i32 {
((i >> 1) as i32) ^ -((i & 1) as i32)
}
#[inline(always)]
pub fn decode_varint32(data: &[u8], i: usize) -> Result<(u32, usize), ()> {
// Unroll a few likely possibilities before going into the robust out-of-line loop.
// This aids branch prediction.
if data.len() > i && (data[i] & 0x80) == 0 {
return Ok((data[i] as u32, i+1))
} else if data.len() > i + 1 && (data[i+1] & 0x80) == 0 {
return Ok((( (data[i] & 0x7f) as u32) |
(( data[i+1] as u32) << 7),
i+2))
} else if data.len() > i + 2 && (data[i+2] & 0x80) == 0 {
return Ok((( (data[i] & 0x7f) as u32) |
(((data[i+1] & 0x7f) as u32) << 7) |
(( data[i+2] as u32) << 14),
i+3))
return Ok((data[i] as u32, i + 1));
} else if data.len() > i + 1 && (data[i + 1] & 0x80) == 0 {
return Ok((
((data[i] & 0x7f) as u32) | ((data[i + 1] as u32) << 7),
i + 2,
));
} else if data.len() > i + 2 && (data[i + 2] & 0x80) == 0 {
return Ok((
((data[i] & 0x7f) as u32)
| (((data[i + 1] & 0x7f) as u32) << 7)
| ((data[i + 2] as u32) << 14),
i + 3,
));
}
decode_varint32_slow(data, i)
}
@ -67,11 +74,11 @@ fn decode_varint32_slow(data: &[u8], mut i: usize) -> Result<(u32, usize), ()> {
let mut shift = 0;
loop {
if i == l {
return Err(())
return Err(());
}
let b = data[i];
if shift == 28 && (b & 0xf0) != 0 {
return Err(())
return Err(());
}
out |= ((b & 0x7f) as u32) << shift;
shift += 7;
@ -87,27 +94,31 @@ pub fn append_varint32(i: u32, data: &mut Vec<u8>) {
if i < 1u32 << 7 {
data.push(i as u8);
} else if i < 1u32 << 14 {
data.extend_from_slice(&[(( i & 0x7F) | 0x80) as u8,
(i >> 7) as u8]);
data.extend_from_slice(&[((i & 0x7F) | 0x80) as u8, (i >> 7) as u8]);
} else if i < 1u32 << 21 {
data.extend_from_slice(&[(( i & 0x7F) | 0x80) as u8,
(((i >> 7) & 0x7F) | 0x80) as u8,
(i >> 14) as u8]);
data.extend_from_slice(&[
((i & 0x7F) | 0x80) as u8,
(((i >> 7) & 0x7F) | 0x80) as u8,
(i >> 14) as u8,
]);
} else if i < 1u32 << 28 {
data.extend_from_slice(&[(( i & 0x7F) | 0x80) as u8,
(((i >> 7) & 0x7F) | 0x80) as u8,
(((i >> 14) & 0x7F) | 0x80) as u8,
(i >> 21) as u8]);
data.extend_from_slice(&[
((i & 0x7F) | 0x80) as u8,
(((i >> 7) & 0x7F) | 0x80) as u8,
(((i >> 14) & 0x7F) | 0x80) as u8,
(i >> 21) as u8,
]);
} else {
data.extend_from_slice(&[(( i & 0x7F) | 0x80) as u8,
(((i >> 7) & 0x7F) | 0x80) as u8,
(((i >> 14) & 0x7F) | 0x80) as u8,
(((i >> 21) & 0x7F) | 0x80) as u8,
(i >> 28) as u8]);
data.extend_from_slice(&[
((i & 0x7F) | 0x80) as u8,
(((i >> 7) & 0x7F) | 0x80) as u8,
(((i >> 14) & 0x7F) | 0x80) as u8,
(((i >> 21) & 0x7F) | 0x80) as u8,
(i >> 28) as u8,
]);
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -119,12 +130,30 @@ mod tests {
encoded: u32,
}
let tests = [
Test{decoded: 0, encoded: 0},
Test{decoded: -1, encoded: 1},
Test{decoded: 1, encoded: 2},
Test{decoded: -2, encoded: 3},
Test{decoded: 2147483647, encoded: 4294967294},
Test{decoded: -2147483648, encoded: 4294967295},
Test {
decoded: 0,
encoded: 0,
},
Test {
decoded: -1,
encoded: 1,
},
Test {
decoded: 1,
encoded: 2,
},
Test {
decoded: -2,
encoded: 3,
},
Test {
decoded: 2147483647,
encoded: 4294967294,
},
Test {
decoded: -2147483648,
encoded: 4294967295,
},
];
for test in &tests {
assert_eq!(test.encoded, zigzag32(test.decoded));
@ -139,11 +168,26 @@ mod tests {
encoded: &'static [u8],
}
let tests = [
Test{decoded: 1, encoded: b"\x01"},
Test{decoded: 257, encoded: b"\x81\x02"},
Test{decoded: 49409, encoded: b"\x81\x82\x03"},
Test{decoded: 8438017, encoded: b"\x81\x82\x83\x04"},
Test{decoded: 1350615297, encoded: b"\x81\x82\x83\x84\x05"},
Test {
decoded: 1,
encoded: b"\x01",
},
Test {
decoded: 257,
encoded: b"\x81\x02",
},
Test {
decoded: 49409,
encoded: b"\x81\x82\x03",
},
Test {
decoded: 8438017,
encoded: b"\x81\x82\x83\x04",
},
Test {
decoded: 1350615297,
encoded: b"\x81\x82\x83\x84\x05",
},
];
for test in &tests {
// Test encoding to an empty buffer.
@ -161,13 +205,17 @@ mod tests {
assert_eq!(out, buf);
// Test decoding from the beginning of the string.
assert_eq!((test.decoded, test.encoded.len()),
decode_varint32(test.encoded, 0).unwrap());
assert_eq!(
(test.decoded, test.encoded.len()),
decode_varint32(test.encoded, 0).unwrap()
);
// ...and from the middle of a buffer.
buf.push(b'x');
assert_eq!((test.decoded, test.encoded.len() + 1),
decode_varint32(&buf, 1).unwrap());
assert_eq!(
(test.decoded, test.encoded.len() + 1),
decode_varint32(&buf, 1).unwrap()
);
}
}
@ -180,7 +228,6 @@ mod tests {
b"\x80\x80",
b"\x80\x80\x80",
b"\x80\x80\x80\x80",
// int32 overflows
b"\x80\x80\x80\x80\x80",
b"\x80\x80\x80\x80\x80\x00",

View File

@ -79,7 +79,8 @@ impl std::fmt::Display for IndexColumn {
/// Returns a sorted vec of table names in the given connection.
fn get_tables(c: &rusqlite::Connection) -> Result<Vec<String>, rusqlite::Error> {
c.prepare(r#"
c.prepare(
r#"
select
name
from
@ -88,66 +89,86 @@ fn get_tables(c: &rusqlite::Connection) -> Result<Vec<String>, rusqlite::Error>
type = 'table' and
name not like 'sqlite_%'
order by name
"#)?
.query_map(params![], |r| r.get(0))?
.collect()
"#,
)?
.query_map(params![], |r| r.get(0))?
.collect()
}
/// Returns a vec of columns in the given table.
fn get_table_columns(c: &rusqlite::Connection, table: &str)
-> Result<Vec<Column>, rusqlite::Error> {
fn get_table_columns(
c: &rusqlite::Connection,
table: &str,
) -> Result<Vec<Column>, rusqlite::Error> {
// Note that placeholders aren't allowed for these pragmas. Just assume sane table names
// (no escaping). "select * from pragma_..." syntax would be nicer but requires SQLite
// 3.16.0 (2017-01-02). Ubuntu 16.04 Xenial (still used on Travis CI) has an older SQLite.
c.prepare(&format!("pragma table_info(\"{}\")", table))?
.query_map(params![], |r| Ok(Column {
cid: r.get(0)?,
name: r.get(1)?,
type_: r.get(2)?,
notnull: r.get(3)?,
dflt_value: r.get(4)?,
pk: r.get(5)?,
}))?
.collect()
.query_map(params![], |r| {
Ok(Column {
cid: r.get(0)?,
name: r.get(1)?,
type_: r.get(2)?,
notnull: r.get(3)?,
dflt_value: r.get(4)?,
pk: r.get(5)?,
})
})?
.collect()
}
/// Returns a vec of indices associated with the given table.
fn get_indices(c: &rusqlite::Connection, table: &str) -> Result<Vec<Index>, rusqlite::Error> {
// See note at get_tables_columns about placeholders.
c.prepare(&format!("pragma index_list(\"{}\")", table))?
.query_map(params![], |r| Ok(Index {
seq: r.get(0)?,
name: r.get(1)?,
unique: r.get(2)?,
origin: r.get(3)?,
partial: r.get(4)?,
}))?
.collect()
.query_map(params![], |r| {
Ok(Index {
seq: r.get(0)?,
name: r.get(1)?,
unique: r.get(2)?,
origin: r.get(3)?,
partial: r.get(4)?,
})
})?
.collect()
}
/// Returns a vec of all the columns in the given index.
fn get_index_columns(c: &rusqlite::Connection, index: &str)
-> Result<Vec<IndexColumn>, rusqlite::Error> {
fn get_index_columns(
c: &rusqlite::Connection,
index: &str,
) -> Result<Vec<IndexColumn>, rusqlite::Error> {
// See note at get_tables_columns about placeholders.
c.prepare(&format!("pragma index_info(\"{}\")", index))?
.query_map(params![], |r| Ok(IndexColumn {
seqno: r.get(0)?,
cid: r.get(1)?,
name: r.get(2)?,
}))?
.collect()
.query_map(params![], |r| {
Ok(IndexColumn {
seqno: r.get(0)?,
cid: r.get(1)?,
name: r.get(2)?,
})
})?
.collect()
}
pub fn get_diffs(n1: &str, c1: &rusqlite::Connection, n2: &str, c2: &rusqlite::Connection)
-> Result<Option<String>, Error> {
pub fn get_diffs(
n1: &str,
c1: &rusqlite::Connection,
n2: &str,
c2: &rusqlite::Connection,
) -> Result<Option<String>, Error> {
let mut diffs = String::new();
// Compare table list.
let tables1 = get_tables(c1)?;
let tables2 = get_tables(c2)?;
if tables1 != tables2 {
write!(&mut diffs, "table list mismatch, {} vs {}:\n{}",
n1, n2, diff_slice(&tables1, &tables2))?;
write!(
&mut diffs,
"table list mismatch, {} vs {}:\n{}",
n1,
n2,
diff_slice(&tables1, &tables2)
)?;
}
// Compare columns and indices for each table.
@ -155,8 +176,14 @@ pub fn get_diffs(n1: &str, c1: &rusqlite::Connection, n2: &str, c2: &rusqlite::C
let columns1 = get_table_columns(c1, &t)?;
let columns2 = get_table_columns(c2, &t)?;
if columns1 != columns2 {
write!(&mut diffs, "table {:?} column, {} vs {}:\n{}",
t, n1, n2, diff_slice(&columns1, &columns2))?;
write!(
&mut diffs,
"table {:?} column, {} vs {}:\n{}",
t,
n1,
n2,
diff_slice(&columns1, &columns2)
)?;
}
let mut indices1 = get_indices(c1, &t)?;
@ -164,16 +191,29 @@ pub fn get_diffs(n1: &str, c1: &rusqlite::Connection, n2: &str, c2: &rusqlite::C
indices1.sort_by(|a, b| a.name.cmp(&b.name));
indices2.sort_by(|a, b| a.name.cmp(&b.name));
if indices1 != indices2 {
write!(&mut diffs, "table {:?} indices, {} vs {}:\n{}",
t, n1, n2, diff_slice(&indices1, &indices2))?;
write!(
&mut diffs,
"table {:?} indices, {} vs {}:\n{}",
t,
n1,
n2,
diff_slice(&indices1, &indices2)
)?;
}
for i in &indices1 {
let ic1 = get_index_columns(c1, &i.name)?;
let ic2 = get_index_columns(c2, &i.name)?;
if ic1 != ic2 {
write!(&mut diffs, "table {:?} index {:?} columns {} vs {}:\n{}",
t, i, n1, n2, diff_slice(&ic1, &ic2))?;
write!(
&mut diffs,
"table {:?} index {:?} columns {} vs {}:\n{}",
t,
i,
n1,
n2,
diff_slice(&ic1, &ic2)
)?;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -36,11 +36,15 @@ use crate::coding;
use crate::db::CompositeId;
use crate::schema;
use cstr::cstr;
use failure::{Error, Fail, bail, format_err};
use failure::{bail, format_err, Error, Fail};
use log::warn;
use protobuf::Message;
use nix::{NixPath, fcntl::{FlockArg, OFlag}, sys::stat::Mode};
use nix::sys::statvfs::Statvfs;
use nix::{
fcntl::{FlockArg, OFlag},
sys::stat::Mode,
NixPath,
};
use protobuf::Message;
use std::ffi::CStr;
use std::fs;
use std::io::{Read, Write};
@ -76,11 +80,17 @@ impl CompositeIdPath {
}
impl NixPath for CompositeIdPath {
fn is_empty(&self) -> bool { false }
fn len(&self) -> usize { 16 }
fn is_empty(&self) -> bool {
false
}
fn len(&self) -> usize {
16
}
fn with_nix_path<T, F>(&self, f: F) -> Result<T, nix::Error>
where F: FnOnce(&CStr) -> T {
where
F: FnOnce(&CStr) -> T,
{
let p = CStr::from_bytes_with_nul(&self.0[..]).expect("no interior nuls");
Ok(f(p))
}
@ -91,7 +101,9 @@ impl NixPath for CompositeIdPath {
pub struct Fd(std::os::unix::io::RawFd);
impl std::os::unix::io::AsRawFd for Fd {
fn as_raw_fd(&self) -> std::os::unix::io::RawFd { self.0 }
fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
self.0
}
}
impl Drop for Fd {
@ -107,7 +119,7 @@ impl Fd {
pub fn open<P: ?Sized + NixPath>(path: &P, mkdir: bool) -> Result<Fd, nix::Error> {
if mkdir {
match nix::unistd::mkdir(path, nix::sys::stat::Mode::S_IRWXU) {
Ok(()) | Err(nix::Error::Sys(nix::errno::Errno::EEXIST)) => {},
Ok(()) | Err(nix::Error::Sys(nix::errno::Errno::EEXIST)) => {}
Err(e) => return Err(e),
}
}
@ -138,7 +150,7 @@ pub(crate) fn read_meta(dir: &Fd) -> Result<schema::DirMeta, Error> {
return Ok(meta);
}
return Err(e.into());
},
}
Ok(f) => f,
};
let mut data = Vec::new();
@ -146,38 +158,63 @@ pub(crate) fn read_meta(dir: &Fd) -> Result<schema::DirMeta, Error> {
let (len, pos) = coding::decode_varint32(&data, 0)
.map_err(|_| format_err!("Unable to decode varint length in meta file"))?;
if data.len() != FIXED_DIR_META_LEN || len as usize + pos > FIXED_DIR_META_LEN {
bail!("Expected a {}-byte file with a varint length of a DirMeta message; got \
a {}-byte file with length {}", FIXED_DIR_META_LEN, data.len(), len);
bail!(
"Expected a {}-byte file with a varint length of a DirMeta message; got \
a {}-byte file with length {}",
FIXED_DIR_META_LEN,
data.len(),
len
);
}
let data = &data[pos..pos+len as usize];
let data = &data[pos..pos + len as usize];
let mut s = protobuf::CodedInputStream::from_bytes(&data);
meta.merge_from(&mut s).map_err(|e| e.context("Unable to parse metadata proto"))?;
meta.merge_from(&mut s)
.map_err(|e| e.context("Unable to parse metadata proto"))?;
Ok(meta)
}
/// Write `dir`'s metadata, clobbering existing data.
pub(crate) fn write_meta(dirfd: RawFd, meta: &schema::DirMeta) -> Result<(), Error> {
let mut data = meta.write_length_delimited_to_bytes().expect("proto3->vec is infallible");
let mut data = meta
.write_length_delimited_to_bytes()
.expect("proto3->vec is infallible");
if data.len() > FIXED_DIR_META_LEN {
bail!("Length-delimited DirMeta message requires {} bytes, over limit of {}",
data.len(), FIXED_DIR_META_LEN);
bail!(
"Length-delimited DirMeta message requires {} bytes, over limit of {}",
data.len(),
FIXED_DIR_META_LEN
);
}
data.resize(FIXED_DIR_META_LEN, 0); // pad to required length.
let mut f = crate::fs::openat(dirfd, cstr!("meta"), OFlag::O_CREAT | OFlag::O_WRONLY,
Mode::S_IRUSR | Mode::S_IWUSR)
.map_err(|e| e.context("Unable to open meta file"))?;
let stat = f.metadata().map_err(|e| e.context("Unable to stat meta file"))?;
data.resize(FIXED_DIR_META_LEN, 0); // pad to required length.
let mut f = crate::fs::openat(
dirfd,
cstr!("meta"),
OFlag::O_CREAT | OFlag::O_WRONLY,
Mode::S_IRUSR | Mode::S_IWUSR,
)
.map_err(|e| e.context("Unable to open meta file"))?;
let stat = f
.metadata()
.map_err(|e| e.context("Unable to stat meta file"))?;
if stat.len() == 0 {
// Need to sync not only the data but also the file metadata and dirent.
f.write_all(&data).map_err(|e| e.context("Unable to write to meta file"))?;
f.sync_all().map_err(|e| e.context("Unable to sync meta file"))?;
f.write_all(&data)
.map_err(|e| e.context("Unable to write to meta file"))?;
f.sync_all()
.map_err(|e| e.context("Unable to sync meta file"))?;
nix::unistd::fsync(dirfd).map_err(|e| e.context("Unable to sync dir"))?;
} else if stat.len() == FIXED_DIR_META_LEN as u64 {
// Just syncing the data will suffice; existing metadata and dirent are fine.
f.write_all(&data).map_err(|e| e.context("Unable to write to meta file"))?;
f.sync_data().map_err(|e| e.context("Unable to sync meta file"))?;
f.write_all(&data)
.map_err(|e| e.context("Unable to write to meta file"))?;
f.sync_data()
.map_err(|e| e.context("Unable to sync meta file"))?;
} else {
bail!("Existing meta file is {}-byte; expected {}", stat.len(), FIXED_DIR_META_LEN);
bail!(
"Existing meta file is {}-byte; expected {}",
stat.len(),
FIXED_DIR_META_LEN
);
}
Ok(())
}
@ -187,21 +224,26 @@ impl SampleFileDir {
///
/// `db_meta.in_progress_open` should be filled if the directory should be opened in read/write
/// mode; absent in read-only mode.
pub fn open(path: &str, db_meta: &schema::DirMeta)
-> Result<Arc<SampleFileDir>, Error> {
pub fn open(path: &str, db_meta: &schema::DirMeta) -> Result<Arc<SampleFileDir>, Error> {
let read_write = db_meta.in_progress_open.is_some();
let s = SampleFileDir::open_self(path, false)?;
s.fd.lock(if read_write {
FlockArg::LockExclusiveNonblock
} else {
FlockArg::LockSharedNonblock
}).map_err(|e| e.context(format!("unable to lock dir {}", path)))?;
FlockArg::LockExclusiveNonblock
} else {
FlockArg::LockSharedNonblock
})
.map_err(|e| e.context(format!("unable to lock dir {}", path)))?;
let dir_meta = read_meta(&s.fd).map_err(|e| e.context("unable to read meta file"))?;
if !SampleFileDir::consistent(db_meta, &dir_meta) {
let serialized =
db_meta.write_length_delimited_to_bytes().expect("proto3->vec is infallible");
bail!("metadata mismatch.\ndb: {:#?}\ndir: {:#?}\nserialized db: {:#?}",
db_meta, &dir_meta, &serialized);
let serialized = db_meta
.write_length_delimited_to_bytes()
.expect("proto3->vec is infallible");
bail!(
"metadata mismatch.\ndb: {:#?}\ndir: {:#?}\nserialized db: {:#?}",
db_meta,
&dir_meta,
&serialized
);
}
if db_meta.in_progress_open.is_some() {
s.write_meta(db_meta)?;
@ -212,12 +254,17 @@ impl SampleFileDir {
/// Returns true if the existing directory and database metadata are consistent; the directory
/// is then openable.
pub(crate) fn consistent(db_meta: &schema::DirMeta, dir_meta: &schema::DirMeta) -> bool {
if dir_meta.db_uuid != db_meta.db_uuid { return false; }
if dir_meta.dir_uuid != db_meta.dir_uuid { return false; }
if dir_meta.db_uuid != db_meta.db_uuid {
return false;
}
if dir_meta.dir_uuid != db_meta.dir_uuid {
return false;
}
if db_meta.last_complete_open.is_some() &&
(db_meta.last_complete_open != dir_meta.last_complete_open &&
db_meta.last_complete_open != dir_meta.in_progress_open) {
if db_meta.last_complete_open.is_some()
&& (db_meta.last_complete_open != dir_meta.last_complete_open
&& db_meta.last_complete_open != dir_meta.in_progress_open)
{
return false;
}
@ -228,8 +275,10 @@ impl SampleFileDir {
true
}
pub(crate) fn create(path: &str, db_meta: &schema::DirMeta)
-> Result<Arc<SampleFileDir>, Error> {
pub(crate) fn create(
path: &str,
db_meta: &schema::DirMeta,
) -> Result<Arc<SampleFileDir>, Error> {
let s = SampleFileDir::open_self(path, true)?;
s.fd.lock(FlockArg::LockExclusiveNonblock)
.map_err(|e| e.context(format!("unable to lock dir {}", path)))?;
@ -238,7 +287,11 @@ impl SampleFileDir {
// Verify metadata. We only care that it hasn't been completely opened.
// Partial opening by this or another database is fine; we won't overwrite anything.
if old_meta.last_complete_open.is_some() {
bail!("Can't create dir at path {}: is already in use:\n{:?}", path, old_meta);
bail!(
"Can't create dir at path {}: is already in use:\n{:?}",
path,
old_meta
);
}
if !s.is_empty()? {
bail!("Can't create dir at path {} with existing files", path);
@ -248,8 +301,12 @@ impl SampleFileDir {
}
pub(crate) fn opendir(&self) -> Result<nix::dir::Dir, nix::Error> {
nix::dir::Dir::openat(self.fd.as_raw_fd(), ".", OFlag::O_DIRECTORY | OFlag::O_RDONLY,
Mode::empty())
nix::dir::Dir::openat(
self.fd.as_raw_fd(),
".",
OFlag::O_DIRECTORY | OFlag::O_RDONLY,
Mode::empty(),
)
}
/// Determines if the directory is empty, aside form metadata.
@ -259,7 +316,7 @@ impl SampleFileDir {
let e = e?;
match e.file_name().to_bytes() {
b"." | b".." => continue,
b"meta" => continue, // existing metadata is fine.
b"meta" => continue, // existing metadata is fine.
_ => return Ok(false),
}
}
@ -268,9 +325,7 @@ impl SampleFileDir {
fn open_self(path: &str, create: bool) -> Result<Arc<SampleFileDir>, Error> {
let fd = Fd::open(path, create)?;
Ok(Arc::new(SampleFileDir {
fd,
}))
Ok(Arc::new(SampleFileDir { fd }))
}
/// Opens the given sample file for reading.
@ -281,15 +336,21 @@ impl SampleFileDir {
pub fn create_file(&self, composite_id: CompositeId) -> Result<fs::File, nix::Error> {
let p = CompositeIdPath::from(composite_id);
crate::fs::openat(self.fd.0, &p, OFlag::O_WRONLY | OFlag::O_EXCL | OFlag::O_CREAT,
Mode::S_IRUSR | Mode::S_IWUSR)
crate::fs::openat(
self.fd.0,
&p,
OFlag::O_WRONLY | OFlag::O_EXCL | OFlag::O_CREAT,
Mode::S_IRUSR | Mode::S_IWUSR,
)
}
pub(crate) fn write_meta(&self, meta: &schema::DirMeta) -> Result<(), Error> {
write_meta(self.fd.0, meta)
}
pub fn statfs(&self) -> Result<Statvfs, nix::Error> { self.fd.statfs() }
pub fn statfs(&self) -> Result<Statvfs, nix::Error> {
self.fd.statfs()
}
/// Unlinks the given sample file within this directory.
pub(crate) fn unlink_file(&self, id: CompositeId) -> Result<(), nix::Error> {
@ -312,11 +373,12 @@ pub(crate) fn parse_id(id: &[u8]) -> Result<CompositeId, ()> {
}
let mut v: u64 = 0;
for i in 0..16 {
v = (v << 4) | match id[i] {
b @ b'0'..=b'9' => b - b'0',
b @ b'a'..=b'f' => b - b'a' + 10,
_ => return Err(()),
} as u64;
v = (v << 4)
| match id[i] {
b @ b'0'..=b'9' => b - b'0',
b @ b'a'..=b'f' => b - b'a' + 10,
_ => return Err(()),
} as u64;
}
Ok(CompositeId(v as i64))
}
@ -353,7 +415,14 @@ mod tests {
o.id = u32::max_value();
o.uuid.extend_from_slice(fake_uuid);
}
let data = meta.write_length_delimited_to_bytes().expect("proto3->vec is infallible");
assert!(data.len() <= FIXED_DIR_META_LEN, "{} vs {}", data.len(), FIXED_DIR_META_LEN);
let data = meta
.write_length_delimited_to_bytes()
.expect("proto3->vec is infallible");
assert!(
data.len() <= FIXED_DIR_META_LEN,
"{} vs {}",
data.len(),
FIXED_DIR_META_LEN
);
}
}

View File

@ -28,13 +28,17 @@
// 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 std::os::unix::io::{FromRawFd, RawFd};
use nix::NixPath;
use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
use nix::NixPath;
use std::os::unix::io::{FromRawFd, RawFd};
pub fn openat<P: ?Sized + NixPath>(dirfd: RawFd, path: &P, oflag: OFlag, mode: Mode)
-> Result<std::fs::File, nix::Error> {
pub fn openat<P: ?Sized + NixPath>(
dirfd: RawFd,
path: &P,
oflag: OFlag,
mode: Mode,
) -> Result<std::fs::File, nix::Error> {
let fd = nix::fcntl::openat(dirfd, path, oflag, mode)?;
Ok(unsafe { std::fs::File::from_raw_fd(fd) })
}

View File

@ -28,7 +28,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#![cfg_attr(all(feature="nightly", test), feature(test))]
#![cfg_attr(all(feature = "nightly", test), feature(test))]
pub mod auth;
pub mod check;

View File

@ -31,9 +31,9 @@
//! Raw database access: SQLite statements which do not touch any cached state.
use crate::db::{self, CompositeId, FromSqlUuid};
use failure::{Error, ResultExt, bail};
use fnv::FnvHashSet;
use crate::recording;
use failure::{bail, Error, ResultExt};
use fnv::FnvHashSet;
use rusqlite::{named_params, params};
use std::ops::Range;
use uuid::Uuid;
@ -126,10 +126,13 @@ const LIST_OLDEST_RECORDINGS_SQL: &'static str = r#"
/// Lists the specified recordings in ascending order by start time, passing them to a supplied
/// function. Given that the function is called with the database lock held, it should be quick.
pub(crate) fn list_recordings_by_time(
conn: &rusqlite::Connection, stream_id: i32, desired_time: Range<recording::Time>,
f: &mut dyn FnMut(db::ListRecordingsRow) -> Result<(), Error>) -> Result<(), Error> {
conn: &rusqlite::Connection,
stream_id: i32,
desired_time: Range<recording::Time>,
f: &mut dyn FnMut(db::ListRecordingsRow) -> Result<(), Error>,
) -> Result<(), Error> {
let mut stmt = conn.prepare_cached(LIST_RECORDINGS_BY_TIME_SQL)?;
let rows = stmt.query_named(named_params!{
let rows = stmt.query_named(named_params! {
":stream_id": stream_id,
":start_time_90k": desired_time.start.0,
":end_time_90k": desired_time.end.0,
@ -139,19 +142,24 @@ pub(crate) fn list_recordings_by_time(
/// Lists the specified recordings in ascending order by id.
pub(crate) fn list_recordings_by_id(
conn: &rusqlite::Connection, stream_id: i32, desired_ids: Range<i32>,
f: &mut dyn FnMut(db::ListRecordingsRow) -> Result<(), Error>) -> Result<(), Error> {
conn: &rusqlite::Connection,
stream_id: i32,
desired_ids: Range<i32>,
f: &mut dyn FnMut(db::ListRecordingsRow) -> Result<(), Error>,
) -> Result<(), Error> {
let mut stmt = conn.prepare_cached(LIST_RECORDINGS_BY_ID_SQL)?;
let rows = stmt.query_named(named_params!{
let rows = stmt.query_named(named_params! {
":start": CompositeId::new(stream_id, desired_ids.start).0,
":end": CompositeId::new(stream_id, desired_ids.end).0,
})?;
list_recordings_inner(rows, true, f)
}
fn list_recordings_inner(mut rows: rusqlite::Rows, include_prev: bool,
f: &mut dyn FnMut(db::ListRecordingsRow) -> Result<(), Error>)
-> Result<(), Error> {
fn list_recordings_inner(
mut rows: rusqlite::Rows,
include_prev: bool,
f: &mut dyn FnMut(db::ListRecordingsRow) -> Result<(), Error>,
) -> Result<(), Error> {
while let Some(row) = rows.next()? {
let wall_duration_90k = row.get(4)?;
let media_duration_delta_90k: i32 = row.get(5)?;
@ -177,17 +185,27 @@ fn list_recordings_inner(mut rows: rusqlite::Rows, include_prev: bool,
}
pub(crate) fn get_db_uuid(conn: &rusqlite::Connection) -> Result<Uuid, Error> {
Ok(conn.query_row("select uuid from meta", params![], |row| -> rusqlite::Result<Uuid> {
let uuid: FromSqlUuid = row.get(0)?;
Ok(uuid.0)
})?)
Ok(conn.query_row(
"select uuid from meta",
params![],
|row| -> rusqlite::Result<Uuid> {
let uuid: FromSqlUuid = row.get(0)?;
Ok(uuid.0)
},
)?)
}
/// Inserts the specified recording (for from `try_flush` only).
pub(crate) fn insert_recording(tx: &rusqlite::Transaction, o: &db::Open, id: CompositeId,
r: &db::RecordingToInsert) -> Result<(), Error> {
let mut stmt = tx.prepare_cached(r#"
insert into recording (composite_id, stream_id, open_id, run_offset, flags,
pub(crate) fn insert_recording(
tx: &rusqlite::Transaction,
o: &db::Open,
id: CompositeId,
r: &db::RecordingToInsert,
) -> Result<(), Error> {
let mut stmt = tx
.prepare_cached(
r#"
insert into recording (composite_id, stream_id, open_id, run_offset, flags,
sample_file_bytes, start_time_90k, prev_media_duration_90k,
prev_runs, wall_duration_90k, media_duration_delta_90k,
video_samples, video_sync_samples, video_sample_entry_id)
@ -195,8 +213,10 @@ pub(crate) fn insert_recording(tx: &rusqlite::Transaction, o: &db::Open, id: Com
:sample_file_bytes, :start_time_90k, :prev_media_duration_90k,
:prev_runs, :wall_duration_90k, :media_duration_delta_90k,
:video_samples, :video_sync_samples, :video_sample_entry_id)
"#).with_context(|e| format!("can't prepare recording insert: {}", e))?;
stmt.execute_named(named_params!{
"#,
)
.with_context(|e| format!("can't prepare recording insert: {}", e))?;
stmt.execute_named(named_params! {
":composite_id": id.0,
":stream_id": i64::from(id.stream()),
":open_id": o.id,
@ -211,32 +231,49 @@ pub(crate) fn insert_recording(tx: &rusqlite::Transaction, o: &db::Open, id: Com
":video_samples": r.video_samples,
":video_sync_samples": r.video_sync_samples,
":video_sample_entry_id": r.video_sample_entry_id,
}).with_context(|e| format!("unable to insert recording for recording {} {:#?}: {}",
id, r, e))?;
})
.with_context(|e| {
format!(
"unable to insert recording for recording {} {:#?}: {}",
id, r, e
)
})?;
let mut stmt = tx.prepare_cached(r#"
insert into recording_integrity (composite_id, local_time_delta_90k, sample_file_blake3)
values (:composite_id, :local_time_delta_90k, :sample_file_blake3)
"#).with_context(|e| format!("can't prepare recording_integrity insert: {}", e))?;
let mut stmt = tx
.prepare_cached(
r#"
insert into recording_integrity (composite_id, local_time_delta_90k,
sample_file_blake3)
values (:composite_id, :local_time_delta_90k,
:sample_file_blake3)
"#,
)
.with_context(|e| format!("can't prepare recording_integrity insert: {}", e))?;
let blake3 = r.sample_file_blake3.as_ref().map(|b| &b[..]);
let delta = match r.run_offset {
0 => None,
_ => Some(r.local_time_delta.0),
};
stmt.execute_named(named_params!{
stmt.execute_named(named_params! {
":composite_id": id.0,
":local_time_delta_90k": delta,
":sample_file_blake3": blake3,
}).with_context(|e| format!("unable to insert recording_integrity for {:#?}: {}", r, e))?;
})
.with_context(|e| format!("unable to insert recording_integrity for {:#?}: {}", r, e))?;
let mut stmt = tx.prepare_cached(r#"
insert into recording_playback (composite_id, video_index)
values (:composite_id, :video_index)
"#).with_context(|e| format!("can't prepare recording_playback insert: {}", e))?;
stmt.execute_named(named_params!{
let mut stmt = tx
.prepare_cached(
r#"
insert into recording_playback (composite_id, video_index)
values (:composite_id, :video_index)
"#,
)
.with_context(|e| format!("can't prepare recording_playback insert: {}", e))?;
stmt.execute_named(named_params! {
":composite_id": id.0,
":video_index": &r.video_index,
}).with_context(|e| format!("unable to insert recording_playback for {:#?}: {}", r, e))?;
})
.with_context(|e| format!("unable to insert recording_playback for {:#?}: {}", r, e))?;
Ok(())
}
@ -245,10 +282,13 @@ pub(crate) fn insert_recording(tx: &rusqlite::Transaction, o: &db::Open, id: Com
/// table. `sample_file_dir_id` is assumed to be correct.
///
/// Returns the number of recordings which were deleted.
pub(crate) fn delete_recordings(tx: &rusqlite::Transaction, sample_file_dir_id: i32,
ids: Range<CompositeId>)
-> Result<usize, Error> {
let mut insert = tx.prepare_cached(r#"
pub(crate) fn delete_recordings(
tx: &rusqlite::Transaction,
sample_file_dir_id: i32,
ids: Range<CompositeId>,
) -> Result<usize, Error> {
let mut insert = tx.prepare_cached(
r#"
insert into garbage (sample_file_dir_id, composite_id)
select
:sample_file_dir_id,
@ -258,54 +298,78 @@ pub(crate) fn delete_recordings(tx: &rusqlite::Transaction, sample_file_dir_id:
where
:start <= composite_id and
composite_id < :end
"#)?;
let mut del_playback = tx.prepare_cached(r#"
"#,
)?;
let mut del_playback = tx.prepare_cached(
r#"
delete from recording_playback
where
:start <= composite_id and
composite_id < :end
"#)?;
let mut del_integrity = tx.prepare_cached(r#"
"#,
)?;
let mut del_integrity = tx.prepare_cached(
r#"
delete from recording_integrity
where
:start <= composite_id and
composite_id < :end
"#)?;
let mut del_main = tx.prepare_cached(r#"
"#,
)?;
let mut del_main = tx.prepare_cached(
r#"
delete from recording
where
:start <= composite_id and
composite_id < :end
"#)?;
let n = insert.execute_named(named_params!{
"#,
)?;
let n = insert.execute_named(named_params! {
":sample_file_dir_id": sample_file_dir_id,
":start": ids.start.0,
":end": ids.end.0,
})?;
let p = named_params!{
let p = named_params! {
":start": ids.start.0,
":end": ids.end.0,
};
let n_playback = del_playback.execute_named(p)?;
if n_playback != n {
bail!("inserted {} garbage rows but deleted {} recording_playback rows!", n, n_playback);
bail!(
"inserted {} garbage rows but deleted {} recording_playback rows!",
n,
n_playback
);
}
let n_integrity = del_integrity.execute_named(p)?;
if n_integrity > n { // fewer is okay; recording_integrity is optional.
bail!("inserted {} garbage rows but deleted {} recording_integrity rows!", n, n_integrity);
if n_integrity > n {
// fewer is okay; recording_integrity is optional.
bail!(
"inserted {} garbage rows but deleted {} recording_integrity rows!",
n,
n_integrity
);
}
let n_main = del_main.execute_named(p)?;
if n_main != n {
bail!("inserted {} garbage rows but deleted {} recording rows!", n, n_main);
bail!(
"inserted {} garbage rows but deleted {} recording rows!",
n,
n_main
);
}
Ok(n)
}
/// Marks the given sample files as deleted. This shouldn't be called until the files have
/// been `unlink()`ed and the parent directory `fsync()`ed.
pub(crate) fn mark_sample_files_deleted(tx: &rusqlite::Transaction, ids: &[CompositeId])
-> Result<(), Error> {
if ids.is_empty() { return Ok(()); }
pub(crate) fn mark_sample_files_deleted(
tx: &rusqlite::Transaction,
ids: &[CompositeId],
) -> Result<(), Error> {
if ids.is_empty() {
return Ok(());
}
let mut stmt = tx.prepare_cached("delete from garbage where composite_id = ?")?;
for &id in ids {
let changes = stmt.execute(params![id.0])?;
@ -323,11 +387,13 @@ pub(crate) fn mark_sample_files_deleted(tx: &rusqlite::Transaction, ids: &[Compo
}
/// Gets the time range of recordings for the given stream.
pub(crate) fn get_range(conn: &rusqlite::Connection, stream_id: i32)
-> Result<Option<Range<recording::Time>>, Error> {
pub(crate) fn get_range(
conn: &rusqlite::Connection,
stream_id: i32,
) -> Result<Option<Range<recording::Time>>, Error> {
// The minimum is straightforward, taking advantage of the start_time_90k index.
let mut stmt = conn.prepare_cached(STREAM_MIN_START_SQL)?;
let mut rows = stmt.query_named(named_params!{":stream_id": stream_id})?;
let mut rows = stmt.query_named(named_params! {":stream_id": stream_id})?;
let min_start = match rows.next()? {
Some(row) => recording::Time(row.get(0)?),
None => return Ok(None),
@ -338,15 +404,15 @@ pub(crate) fn get_range(conn: &rusqlite::Connection, stream_id: i32)
// last MAX_RECORDING_DURATION must be examined in order to take advantage of the
// start_time_90k index.
let mut stmt = conn.prepare_cached(STREAM_MAX_START_SQL)?;
let mut rows = stmt.query_named(named_params!{":stream_id": stream_id})?;
let mut rows = stmt.query_named(named_params! {":stream_id": stream_id})?;
let mut maxes_opt = None;
while let Some(row) = rows.next()? {
let row_start = recording::Time(row.get(0)?);
let row_duration: i64 = row.get(1)?;
let row_end = recording::Time(row_start.0 + row_duration);
let maxes = match maxes_opt {
None => row_start .. row_end,
Some(Range{start: s, end: e}) => s .. ::std::cmp::max(e, row_end),
None => row_start..row_end,
Some(Range { start: s, end: e }) => s..::std::cmp::max(e, row_end),
};
if row_start.0 <= maxes.start.0 - recording::MAX_RECORDING_WALL_DURATION {
break;
@ -354,18 +420,24 @@ pub(crate) fn get_range(conn: &rusqlite::Connection, stream_id: i32)
maxes_opt = Some(maxes);
}
let max_end = match maxes_opt {
Some(Range{start: _, end: e}) => e,
None => bail!("missing max for stream {} which had min {}", stream_id, min_start),
Some(Range { start: _, end: e }) => e,
None => bail!(
"missing max for stream {} which had min {}",
stream_id,
min_start
),
};
Ok(Some(min_start .. max_end))
Ok(Some(min_start..max_end))
}
/// Lists all garbage ids for the given sample file directory.
pub(crate) fn list_garbage(conn: &rusqlite::Connection, dir_id: i32)
-> Result<FnvHashSet<CompositeId>, Error> {
pub(crate) fn list_garbage(
conn: &rusqlite::Connection,
dir_id: i32,
) -> Result<FnvHashSet<CompositeId>, Error> {
let mut garbage = FnvHashSet::default();
let mut stmt = conn.prepare_cached(
"select composite_id from garbage where sample_file_dir_id = ?")?;
let mut stmt =
conn.prepare_cached("select composite_id from garbage where sample_file_dir_id = ?")?;
let mut rows = stmt.query(&[&dir_id])?;
while let Some(row) = rows.next()? {
garbage.insert(CompositeId(row.get(0)?));
@ -375,11 +447,13 @@ pub(crate) fn list_garbage(conn: &rusqlite::Connection, dir_id: i32)
/// Lists the oldest recordings for a stream, starting with the given id.
/// `f` should return true as long as further rows are desired.
pub(crate) fn list_oldest_recordings(conn: &rusqlite::Connection, start: CompositeId,
f: &mut dyn FnMut(db::ListOldestRecordingsRow) -> bool)
-> Result<(), Error> {
pub(crate) fn list_oldest_recordings(
conn: &rusqlite::Connection,
start: CompositeId,
f: &mut dyn FnMut(db::ListOldestRecordingsRow) -> bool,
) -> Result<(), Error> {
let mut stmt = conn.prepare_cached(LIST_OLDEST_RECORDINGS_SQL)?;
let mut rows = stmt.query_named(named_params!{
let mut rows = stmt.query_named(named_params! {
":start": start.0,
":end": CompositeId::new(start.stream() + 1, 0).0,
})?;

View File

@ -30,7 +30,7 @@
use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32};
use crate::db;
use failure::{Error, bail};
use failure::{bail, Error};
use log::trace;
use std::convert::TryFrom;
use std::ops::Range;
@ -40,14 +40,18 @@ pub use base::time::TIME_UNITS_PER_SEC;
pub const DESIRED_RECORDING_WALL_DURATION: i64 = 60 * TIME_UNITS_PER_SEC;
pub const MAX_RECORDING_WALL_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC;
pub use base::time::Time;
pub use base::time::Duration;
pub use base::time::Time;
/// Converts from a wall time offset into a recording to a media time offset or vice versa.
pub fn rescale(from_off_90k: i32, from_duration_90k: i32, to_duration_90k: i32) -> i32 {
debug_assert!(from_off_90k <= from_duration_90k,
"from_off_90k={} from_duration_90k={} to_duration_90k={}",
from_off_90k, from_duration_90k, to_duration_90k);
debug_assert!(
from_off_90k <= from_duration_90k,
"from_off_90k={} from_duration_90k={} to_duration_90k={}",
from_off_90k,
from_duration_90k,
to_duration_90k
);
if from_duration_90k == 0 {
return 0; // avoid a divide by zero.
}
@ -56,12 +60,16 @@ pub fn rescale(from_off_90k: i32, from_duration_90k: i32, to_duration_90k: i32)
// time is recording::MAX_RECORDING_WALL_DURATION; the max media duration should be
// roughly the same (design limit of 500 ppm correction). The final result should fit
// within i32.
i32::try_from(i64::from(from_off_90k) *
i64::from(to_duration_90k) /
i64::from(from_duration_90k))
.map_err(|_| format!("rescale overflow: {} * {} / {} > i32::max_value()",
from_off_90k, to_duration_90k, from_duration_90k))
.unwrap()
i32::try_from(
i64::from(from_off_90k) * i64::from(to_duration_90k) / i64::from(from_duration_90k),
)
.map_err(|_| {
format!(
"rescale overflow: {} * {} / {} > i32::max_value()",
from_off_90k, to_duration_90k, from_duration_90k
)
})
.unwrap()
}
/// An iterator through a sample index.
@ -91,12 +99,14 @@ pub struct SampleIndexIterator {
impl SampleIndexIterator {
pub fn new() -> SampleIndexIterator {
SampleIndexIterator{i_and_is_key: 0,
pos: 0,
start_90k: 0,
duration_90k: 0,
bytes: 0,
bytes_other: 0}
SampleIndexIterator {
i_and_is_key: 0,
pos: 0,
start_90k: 0,
duration_90k: 0,
bytes: 0,
bytes_other: 0,
}
}
pub fn next(&mut self, data: &[u8]) -> Result<bool, Error> {
@ -104,7 +114,7 @@ impl SampleIndexIterator {
self.start_90k += self.duration_90k;
let i = (self.i_and_is_key & 0x7FFF_FFFF) as usize;
if i == data.len() {
return Ok(false)
return Ok(false);
}
let (raw1, i1) = match decode_varint32(data, i) {
Ok(tuple) => tuple,
@ -117,11 +127,17 @@ impl SampleIndexIterator {
let duration_90k_delta = unzigzag32(raw1 >> 1);
self.duration_90k += duration_90k_delta;
if self.duration_90k < 0 {
bail!("negative duration {} after applying delta {}",
self.duration_90k, duration_90k_delta);
bail!(
"negative duration {} after applying delta {}",
self.duration_90k,
duration_90k_delta
);
}
if self.duration_90k == 0 && data.len() > i2 {
bail!("zero duration only allowed at end; have {} bytes left", data.len() - i2);
bail!(
"zero duration only allowed at end; have {} bytes left",
data.len() - i2
);
}
let (prev_bytes_key, prev_bytes_nonkey) = match self.is_key() {
true => (self.bytes, self.bytes_other),
@ -137,14 +153,21 @@ impl SampleIndexIterator {
self.bytes_other = prev_bytes_key;
}
if self.bytes <= 0 {
bail!("non-positive bytes {} after applying delta {} to key={} frame at ts {}",
self.bytes, bytes_delta, self.is_key(), self.start_90k);
bail!(
"non-positive bytes {} after applying delta {} to key={} frame at ts {}",
self.bytes,
bytes_delta,
self.is_key(),
self.start_90k
);
}
Ok(true)
}
#[inline]
pub fn is_key(&self) -> bool { (self.i_and_is_key & 0x8000_0000) != 0 }
pub fn is_key(&self) -> bool {
(self.i_and_is_key & 0x8000_0000) != 0
}
}
#[derive(Debug)]
@ -163,24 +186,33 @@ impl SampleIndexEncoder {
}
}
pub fn add_sample(&mut self, duration_90k: i32, bytes: i32, is_key: bool,
r: &mut db::RecordingToInsert) {
pub fn add_sample(
&mut self,
duration_90k: i32,
bytes: i32,
is_key: bool,
r: &mut db::RecordingToInsert,
) {
let duration_delta = duration_90k - self.prev_duration_90k;
self.prev_duration_90k = duration_90k;
r.media_duration_90k += duration_90k;
r.sample_file_bytes += bytes;
r.video_samples += 1;
let bytes_delta = bytes - if is_key {
let prev = self.prev_bytes_key;
r.video_sync_samples += 1;
self.prev_bytes_key = bytes;
prev
} else {
let prev = self.prev_bytes_nonkey;
self.prev_bytes_nonkey = bytes;
prev
};
append_varint32((zigzag32(duration_delta) << 1) | (is_key as u32), &mut r.video_index);
let bytes_delta = bytes
- if is_key {
let prev = self.prev_bytes_key;
r.video_sync_samples += 1;
self.prev_bytes_key = bytes;
prev
} else {
let prev = self.prev_bytes_nonkey;
self.prev_bytes_nonkey = bytes;
prev
};
append_varint32(
(zigzag32(duration_delta) << 1) | (is_key as u32),
&mut r.video_index,
);
append_varint32(zigzag32(bytes_delta), &mut r.video_index);
}
}
@ -218,10 +250,12 @@ impl Segment {
/// The actual range will end at the first frame after the desired range (unless the desired
/// range extends beyond the recording). Likewise, the caller is responsible for trimming the
/// final frame's duration if desired.
pub fn new(db: &db::LockedDatabase,
recording: &db::ListRecordingsRow,
desired_media_range_90k: Range<i32>,
start_at_key: bool) -> Result<Segment, Error> {
pub fn new(
db: &db::LockedDatabase,
recording: &db::ListRecordingsRow,
desired_media_range_90k: Range<i32>,
start_at_key: bool,
) -> Result<Segment, Error> {
let mut self_ = Segment {
id: recording.id,
open_id: recording.open_id,
@ -229,28 +263,39 @@ impl Segment {
file_end: recording.sample_file_bytes,
frames: recording.video_samples as u16,
key_frames: recording.video_sync_samples as u16,
video_sample_entry_id_and_trailing_zero:
recording.video_sample_entry_id |
((((recording.flags & db::RecordingFlags::TrailingZero as i32) != 0) as i32) << 31),
video_sample_entry_id_and_trailing_zero: recording.video_sample_entry_id
| ((((recording.flags & db::RecordingFlags::TrailingZero as i32) != 0) as i32)
<< 31),
};
if desired_media_range_90k.start > desired_media_range_90k.end ||
desired_media_range_90k.end > recording.media_duration_90k {
bail!("desired media range [{}, {}) invalid for recording of length {}",
desired_media_range_90k.start, desired_media_range_90k.end,
recording.media_duration_90k);
if desired_media_range_90k.start > desired_media_range_90k.end
|| desired_media_range_90k.end > recording.media_duration_90k
{
bail!(
"desired media range [{}, {}) invalid for recording of length {}",
desired_media_range_90k.start,
desired_media_range_90k.end,
recording.media_duration_90k
);
}
if desired_media_range_90k.start == 0 &&
desired_media_range_90k.end == recording.media_duration_90k {
if desired_media_range_90k.start == 0
&& desired_media_range_90k.end == recording.media_duration_90k
{
// Fast path. Existing entry is fine.
trace!("recording::Segment::new fast path, recording={:#?}", recording);
return Ok(self_)
trace!(
"recording::Segment::new fast path, recording={:#?}",
recording
);
return Ok(self_);
}
// Slow path. Need to iterate through the index.
trace!("recording::Segment::new slow path, desired_media_range_90k={:?}, recording={:#?}",
desired_media_range_90k, recording);
trace!(
"recording::Segment::new slow path, desired_media_range_90k={:?}, recording={:#?}",
desired_media_range_90k,
recording
);
db.with_recording_playback(self_.id, &mut |playback| {
let mut begin = Box::new(SampleIndexIterator::new());
let data = &(&playback).video_index;
@ -274,8 +319,7 @@ impl Segment {
};
loop {
if it.start_90k <= desired_media_range_90k.start &&
(!start_at_key || it.is_key()) {
if it.start_90k <= desired_media_range_90k.start && (!start_at_key || it.is_key()) {
// new start candidate.
*begin = it;
self_.frames = 0;
@ -293,8 +337,7 @@ impl Segment {
self_.begin = Some(begin);
self_.file_end = it.pos;
self_.video_sample_entry_id_and_trailing_zero =
recording.video_sample_entry_id |
(((it.duration_90k == 0) as i32) << 31);
recording.video_sample_entry_id | (((it.duration_90k == 0) as i32) << 31);
Ok(())
})?;
Ok(self_)
@ -304,23 +347,33 @@ impl Segment {
self.video_sample_entry_id_and_trailing_zero & 0x7FFFFFFF
}
pub fn have_trailing_zero(&self) -> bool { self.video_sample_entry_id_and_trailing_zero < 0 }
pub fn have_trailing_zero(&self) -> bool {
self.video_sample_entry_id_and_trailing_zero < 0
}
/// Returns the byte range within the sample file of data associated with this segment.
pub fn sample_file_range(&self) -> Range<u64> {
self.begin.as_ref().map(|b| b.pos as u64).unwrap_or(0) .. self.file_end as u64
self.begin.as_ref().map(|b| b.pos as u64).unwrap_or(0)..self.file_end as u64
}
/// Returns the actual media start time. As described in `new`, this can be less than the
/// desired media start time if there is no key frame at the right position.
pub fn actual_start_90k(&self) -> i32 { self.begin.as_ref().map(|b| b.start_90k).unwrap_or(0) }
pub fn actual_start_90k(&self) -> i32 {
self.begin.as_ref().map(|b| b.start_90k).unwrap_or(0)
}
/// Iterates through each frame in the segment.
/// Must be called without the database lock held; retrieves video index from the cache.
pub fn foreach<F>(&self, playback: &db::RecordingPlayback, mut f: F) -> Result<(), Error>
where F: FnMut(&SampleIndexIterator) -> Result<(), Error> {
trace!("foreach on recording {}: {} frames, actual_start_90k: {}",
self.id, self.frames, self.actual_start_90k());
where
F: FnMut(&SampleIndexIterator) -> Result<(), Error>,
{
trace!(
"foreach on recording {}: {} frames, actual_start_90k: {}",
self.id,
self.frames,
self.actual_start_90k()
);
let data = &(&playback).video_index;
let mut it = match self.begin {
Some(ref b) => **b,
@ -338,15 +391,23 @@ impl Segment {
let mut have_frame = true;
let mut key_frame = 0;
for i in 0 .. self.frames {
for i in 0..self.frames {
if !have_frame {
bail!("recording {}: expected {} frames, found only {}", self.id, self.frames, i+1);
bail!(
"recording {}: expected {} frames, found only {}",
self.id,
self.frames,
i + 1
);
}
if it.is_key() {
key_frame += 1;
if key_frame > self.key_frames {
bail!("recording {}: more than expected {} key frames",
self.id, self.key_frames);
bail!(
"recording {}: more than expected {} key frames",
self.id,
self.key_frames
);
}
}
@ -362,8 +423,12 @@ impl Segment {
};
}
if key_frame < self.key_frames {
bail!("recording {}: expected {} key frames, found only {}",
self.id, self.key_frames, key_frame);
bail!(
"recording {}: expected {} key frames, found only {}",
self.id,
self.key_frames,
key_frame
);
}
Ok(())
}
@ -382,9 +447,9 @@ impl Segment {
#[cfg(test)]
mod tests {
use base::clock::RealClocks;
use super::*;
use crate::testutil::{self, TestDb};
use base::clock::RealClocks;
/// Tests encoding the example from design/schema.md.
#[test]
@ -397,7 +462,10 @@ mod tests {
e.add_sample(11, 15, false, &mut r);
e.add_sample(10, 12, false, &mut r);
e.add_sample(10, 1050, true, &mut r);
assert_eq!(r.video_index, b"\x29\xd0\x0f\x02\x14\x08\x0a\x02\x05\x01\x64");
assert_eq!(
r.video_index,
b"\x29\xd0\x0f\x02\x14\x08\x0a\x02\x05\x01\x64"
);
assert_eq!(10 + 9 + 11 + 10 + 10, r.media_duration_90k);
assert_eq!(5, r.video_samples);
assert_eq!(2, r.video_sync_samples);
@ -413,12 +481,13 @@ mod tests {
bytes: i32,
is_key: bool,
}
#[rustfmt::skip]
let samples = [
Sample{duration_90k: 10, bytes: 30000, is_key: true},
Sample{duration_90k: 9, bytes: 1000, is_key: false},
Sample{duration_90k: 11, bytes: 1100, is_key: false},
Sample{duration_90k: 18, bytes: 31000, is_key: true},
Sample{duration_90k: 0, bytes: 1000, is_key: false},
Sample { duration_90k: 10, bytes: 30000, is_key: true, },
Sample { duration_90k: 9, bytes: 1000, is_key: false, },
Sample { duration_90k: 11, bytes: 1100, is_key: false, },
Sample { duration_90k: 18, bytes: 31000, is_key: true, },
Sample { duration_90k: 0, bytes: 1000, is_key: false, },
];
let mut r = db::RecordingToInsert::default();
let mut e = SampleIndexEncoder::new();
@ -428,10 +497,14 @@ mod tests {
let mut it = SampleIndexIterator::new();
for sample in &samples {
assert!(it.next(&r.video_index).unwrap());
assert_eq!(sample,
&Sample{duration_90k: it.duration_90k,
bytes: it.bytes,
is_key: it.is_key()});
assert_eq!(
sample,
&Sample {
duration_90k: it.duration_90k,
bytes: it.bytes,
is_key: it.is_key()
}
);
}
assert!(!it.next(&r.video_index).unwrap());
}
@ -446,14 +519,26 @@ mod tests {
err: &'static str,
}
let tests = [
Test{encoded: b"\x80", err: "bad varint 1 at offset 0"},
Test{encoded: b"\x00\x80", err: "bad varint 2 at offset 1"},
Test{encoded: b"\x00\x02\x00\x00",
err: "zero duration only allowed at end; have 2 bytes left"},
Test{encoded: b"\x02\x02",
err: "negative duration -1 after applying delta -1"},
Test{encoded: b"\x04\x00",
err: "non-positive bytes 0 after applying delta 0 to key=false frame at ts 0"},
Test {
encoded: b"\x80",
err: "bad varint 1 at offset 0",
},
Test {
encoded: b"\x00\x80",
err: "bad varint 2 at offset 1",
},
Test {
encoded: b"\x00\x02\x00\x00",
err: "zero duration only allowed at end; have 2 bytes left",
},
Test {
encoded: b"\x02\x02",
err: "negative duration -1 after applying delta -1",
},
Test {
encoded: b"\x04\x00",
err: "non-positive bytes 0 after applying delta 0 to key=false frame at ts 0",
},
];
for test in &tests {
let mut it = SampleIndexIterator::new();
@ -462,11 +547,18 @@ mod tests {
}
fn get_frames<F, T>(db: &db::Database, segment: &Segment, f: F) -> Vec<T>
where F: Fn(&SampleIndexIterator) -> T {
where
F: Fn(&SampleIndexIterator) -> T,
{
let mut v = Vec::new();
db.lock().with_recording_playback(segment.id, &mut |playback| {
segment.foreach(playback, |it| { v.push(f(it)); Ok(()) })
}).unwrap();
db.lock()
.with_recording_playback(segment.id, &mut |playback| {
segment.foreach(playback, |it| {
v.push(f(it));
Ok(())
})
})
.unwrap();
v
}
@ -486,8 +578,11 @@ mod tests {
let row = db.insert_recording_from_encoder(r);
// Time range [2, 2 + 4 + 6 + 8) means the 2nd, 3rd, 4th samples should be
// included.
let segment = Segment::new(&db.db.lock(), &row, 2 .. 2+4+6+8, true).unwrap();
assert_eq!(&get_frames(&db.db, &segment, |it| it.duration_90k), &[4, 6, 8]);
let segment = Segment::new(&db.db.lock(), &row, 2..2 + 4 + 6 + 8, true).unwrap();
assert_eq!(
&get_frames(&db.db, &segment, |it| it.duration_90k),
&[4, 6, 8]
);
}
/// Half sync frames means starting from the last sync frame <= desired point.
@ -505,7 +600,7 @@ mod tests {
let row = db.insert_recording_from_encoder(r);
// Time range [2 + 4 + 6, 2 + 4 + 6 + 8) means the 4th sample should be included.
// The 3rd also gets pulled in because it is a sync frame and the 4th is not.
let segment = Segment::new(&db.db.lock(), &row, 2+4+6 .. 2+4+6+8, true).unwrap();
let segment = Segment::new(&db.db.lock(), &row, 2 + 4 + 6..2 + 4 + 6 + 8, true).unwrap();
assert_eq!(&get_frames(&db.db, &segment, |it| it.duration_90k), &[6, 8]);
}
@ -519,7 +614,7 @@ mod tests {
encoder.add_sample(0, 3, true, &mut r);
let db = TestDb::new(RealClocks {});
let row = db.insert_recording_from_encoder(r);
let segment = Segment::new(&db.db.lock(), &row, 1 .. 2, true).unwrap();
let segment = Segment::new(&db.db.lock(), &row, 1..2, true).unwrap();
assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[2, 3]);
}
@ -532,7 +627,7 @@ mod tests {
encoder.add_sample(1, 1, true, &mut r);
let db = TestDb::new(RealClocks {});
let row = db.insert_recording_from_encoder(r);
let segment = Segment::new(&db.db.lock(), &row, 0 .. 0, true).unwrap();
let segment = Segment::new(&db.db.lock(), &row, 0..0, true).unwrap();
assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[1]);
}
@ -550,8 +645,11 @@ mod tests {
}
let db = TestDb::new(RealClocks {});
let row = db.insert_recording_from_encoder(r);
let segment = Segment::new(&db.db.lock(), &row, 0 .. 2+4+6+8+10, true).unwrap();
assert_eq!(&get_frames(&db.db, &segment, |it| it.duration_90k), &[2, 4, 6, 8, 10]);
let segment = Segment::new(&db.db.lock(), &row, 0..2 + 4 + 6 + 8 + 10, true).unwrap();
assert_eq!(
&get_frames(&db.db, &segment, |it| it.duration_90k),
&[2, 4, 6, 8, 10]
);
}
#[test]
@ -564,14 +662,14 @@ mod tests {
encoder.add_sample(0, 3, true, &mut r);
let db = TestDb::new(RealClocks {});
let row = db.insert_recording_from_encoder(r);
let segment = Segment::new(&db.db.lock(), &row, 0 .. 2, true).unwrap();
let segment = Segment::new(&db.db.lock(), &row, 0..2, true).unwrap();
assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[1, 2, 3]);
}
// TODO: test segment error cases involving mismatch between row frames/key_frames and index.
}
#[cfg(all(test, feature="nightly"))]
#[cfg(all(test, feature = "nightly"))]
mod bench {
extern crate test;

View File

@ -28,16 +28,16 @@
// 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 base::bail_t;
use crate::coding;
use crate::db::FromSqlUuid;
use crate::recording;
use failure::{Error, bail, format_err};
use base::bail_t;
use failure::{bail, format_err, Error};
use fnv::FnvHashMap;
use log::debug;
use rusqlite::{Connection, Transaction, params};
use std::collections::{BTreeMap, BTreeSet};
use rusqlite::{params, Connection, Transaction};
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BTreeSet};
use std::ops::Range;
use uuid::Uuid;
@ -132,7 +132,9 @@ impl Point {
/// `from` must be an iterator of `(signal, state)` with signal numbers in monotonically increasing
/// order.
fn append_serialized<'a, I>(from: I, to: &mut Vec<u8>)
where I: IntoIterator<Item = (&'a u32, &'a u16)> {
where
I: IntoIterator<Item = (&'a u32, &'a u16)>,
{
let mut next_allowed = 0;
for (&signal, &state) in from.into_iter() {
assert!(signal >= next_allowed);
@ -170,15 +172,18 @@ impl<'a> PointDataIterator<'a> {
if self.cur_pos == self.data.len() {
return Ok(None);
}
let (signal_delta, p) = coding::decode_varint32(self.data, self.cur_pos)
.map_err(|()| format_err!("varint32 decode failure; data={:?} pos={}",
self.data, self.cur_pos))?;
let (signal_delta, p) = coding::decode_varint32(self.data, self.cur_pos).map_err(|()| {
format_err!(
"varint32 decode failure; data={:?} pos={}",
self.data,
self.cur_pos
)
})?;
let (state, p) = coding::decode_varint32(self.data, p)
.map_err(|()| format_err!("varint32 decode failure; data={:?} pos={}",
self.data, p))?;
let signal = self.cur_signal.checked_add(signal_delta)
.ok_or_else(|| format_err!("signal overflow: {} + {}",
self.cur_signal, signal_delta))?;
.map_err(|()| format_err!("varint32 decode failure; data={:?} pos={}", self.data, p))?;
let signal = self.cur_signal.checked_add(signal_delta).ok_or_else(|| {
format_err!("signal overflow: {} + {}", self.cur_signal, signal_delta)
})?;
if state > u16::max_value() as u32 {
bail!("state overflow: {}", state);
}
@ -221,7 +226,9 @@ pub struct ListStateChangesRow {
impl State {
pub fn init(conn: &Connection) -> Result<Self, Error> {
let max_signal_changes: Option<i64> =
conn.query_row("select max_signal_changes from meta", params![], |row| row.get(0))?;
conn.query_row("select max_signal_changes from meta", params![], |row| {
row.get(0)
})?;
let mut signals_by_id = State::init_signals(conn)?;
State::fill_signal_cameras(conn, &mut signals_by_id)?;
Ok(State {
@ -234,8 +241,10 @@ impl State {
}
pub fn list_changes_by_time(
&self, desired_time: Range<recording::Time>, f: &mut dyn FnMut(&ListStateChangesRow)) {
&self,
desired_time: Range<recording::Time>,
f: &mut dyn FnMut(&ListStateChangesRow),
) {
// First find the state immediately before. If it exists, include it.
if let Some((&when, p)) = self.points_by_time.range(..desired_time.start).next_back() {
for (&signal, &state) in &p.after() {
@ -261,8 +270,11 @@ impl State {
}
pub fn update_signals(
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
-> Result<(), base::Error> {
&mut self,
when: Range<recording::Time>,
signals: &[u32],
states: &[u16],
) -> Result<(), base::Error> {
// Do input validation before any mutation.
self.update_signals_validate(signals, states)?;
@ -294,11 +306,19 @@ impl State {
None => return,
Some(p) => p,
};
debug!("Performing signal GC: have {} points, want only {}, so removing {}",
self.points_by_time.len(), max, to_remove);
debug!(
"Performing signal GC: have {} points, want only {}, so removing {}",
self.points_by_time.len(),
max,
to_remove
);
let remove: smallvec::SmallVec<[recording::Time; 4]> =
self.points_by_time.keys().take(to_remove).map(|p| *p).collect();
let remove: smallvec::SmallVec<[recording::Time; 4]> = self
.points_by_time
.keys()
.take(to_remove)
.map(|p| *p)
.collect();
for p in &remove {
self.points_by_time.remove(p);
@ -320,14 +340,20 @@ impl State {
None => bail_t!(InvalidArgument, "unknown signal {}", signal),
Some(ref s) => {
let empty = Vec::new();
let states = self.types_by_uuid.get(&s.type_)
.map(|t| &t.states)
.unwrap_or(&empty);
let states = self
.types_by_uuid
.get(&s.type_)
.map(|t| &t.states)
.unwrap_or(&empty);
if state != 0 && states.binary_search_by_key(&state, |s| s.value).is_err() {
bail_t!(FailedPrecondition, "signal {} specifies unknown state {}",
signal, state);
bail_t!(
FailedPrecondition,
"signal {} specifies unknown state {}",
signal,
state
);
}
},
}
}
next_allowed = signal + 1;
}
@ -354,7 +380,10 @@ impl State {
// Any existing changes should still be applied. They win over reverting to prev.
let mut it = p.changes();
while let Some((signal, state)) = it.next().expect("in-mem changes is valid") {
changes.entry(signal).and_modify(|e| *e = state).or_insert(state);
changes
.entry(signal)
.and_modify(|e| *e = state)
.or_insert(state);
}
self.dirty_by_time.insert(t);
p.swap(&mut Point::new(&prev, &serialize(&changes)));
@ -374,20 +403,25 @@ impl State {
return;
}
self.dirty_by_time.insert(end);
self.points_by_time.insert(end, Point::new(&prev, &serialize(&changes)));
self.points_by_time
.insert(end, Point::new(&prev, &serialize(&changes)));
}
/// Helper for `update_signals_end`. Adjusts `prev` (the state prior to the end point) to
/// reflect the desired update (in `signals` and `states`). Adjusts `changes` (changes to
/// execute at the end point) to undo the change.
fn update_signals_end_maps(signals: &[u32], states: &[u16], prev: &mut BTreeMap<u32, u16>,
changes: &mut BTreeMap<u32, u16>) {
fn update_signals_end_maps(
signals: &[u32],
states: &[u16],
prev: &mut BTreeMap<u32, u16>,
changes: &mut BTreeMap<u32, u16>,
) {
for (&signal, &state) in signals.iter().zip(states) {
match prev.entry(signal) {
Entry::Vacant(e) => {
changes.insert(signal, 0);
e.insert(state);
},
}
Entry::Occupied(mut e) => {
if state == 0 {
changes.insert(signal, *e.get());
@ -396,7 +430,7 @@ impl State {
changes.insert(signal, *e.get());
*e.get_mut() = state;
}
},
}
}
}
}
@ -421,13 +455,13 @@ impl State {
*e.get_mut() = state;
}
}
},
}
Entry::Vacant(e) => {
if signal != 0 {
dirty = true;
e.insert(state);
}
},
}
}
}
if dirty {
@ -456,14 +490,19 @@ impl State {
}
self.dirty_by_time.insert(start);
self.points_by_time.insert(start, Point::new(&prev, &serialize(&changes)));
self.points_by_time
.insert(start, Point::new(&prev, &serialize(&changes)));
}
/// Helper for `update_signals` to apply all points in `(when.start, when.end)`.
fn update_signals_middle(&mut self, when: Range<recording::Time>, signals: &[u32],
states: &[u16]) {
fn update_signals_middle(
&mut self,
when: Range<recording::Time>,
signals: &[u32],
states: &[u16],
) {
let mut to_delete = Vec::new();
let after_start = recording::Time(when.start.0+1);
let after_start = recording::Time(when.start.0 + 1);
for (&t, ref mut p) in self.points_by_time.range_mut(after_start..when.end) {
let mut prev = p.prev().to_map().expect("in-mem prev is valid");
@ -476,7 +515,7 @@ impl State {
} else if *e.get() != state {
*e.get_mut() = state;
}
},
}
Entry::Vacant(e) => {
if state != 0 {
e.insert(state);
@ -486,14 +525,16 @@ impl State {
}
// Trim changes to omit any change to signals.
let mut changes = Vec::with_capacity(3*signals.len());
let mut changes = Vec::with_capacity(3 * signals.len());
let mut it = p.changes();
let mut next_allowed = 0;
let mut dirty = false;
while let Some((signal, state)) = it.next().expect("in-memory changes is valid") {
if signals.binary_search(&signal).is_ok() { // discard.
if signals.binary_search(&signal).is_ok() {
// discard.
dirty = true;
} else { // keep.
} else {
// keep.
assert!(signal >= next_allowed);
coding::append_varint32(signal - next_allowed, &mut changes);
coding::append_varint32(state as u32, &mut changes);
@ -521,24 +562,25 @@ impl State {
/// The caller is expected to call `post_flush` afterward if the transaction is
/// successfully committed. No mutations should happen between these calls.
pub fn flush(&mut self, tx: &Transaction) -> Result<(), Error> {
let mut i_stmt = tx.prepare(r#"
let mut i_stmt = tx.prepare(
r#"
insert or replace into signal_change (time_90k, changes) values (?, ?)
"#)?;
let mut d_stmt = tx.prepare(r#"
"#,
)?;
let mut d_stmt = tx.prepare(
r#"
delete from signal_change where time_90k = ?
"#)?;
"#,
)?;
for &t in &self.dirty_by_time {
match self.points_by_time.entry(t) {
Entry::Occupied(ref e) => {
let p = e.get();
i_stmt.execute(params![
t.0,
&p.data[p.changes_off..],
])?;
},
i_stmt.execute(params![t.0, &p.data[p.changes_off..],])?;
}
Entry::Vacant(_) => {
d_stmt.execute(params![t.0])?;
},
}
}
}
Ok(())
@ -553,7 +595,8 @@ impl State {
fn init_signals(conn: &Connection) -> Result<BTreeMap<u32, Signal>, Error> {
let mut signals = BTreeMap::new();
let mut stmt = conn.prepare(r#"
let mut stmt = conn.prepare(
r#"
select
id,
source_uuid,
@ -561,35 +604,41 @@ impl State {
short_name
from
signal
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let id = row.get(0)?;
let source: FromSqlUuid = row.get(1)?;
let type_: FromSqlUuid = row.get(2)?;
signals.insert(id, Signal {
signals.insert(
id,
source: source.0,
type_: type_.0,
short_name: row.get(3)?,
cameras: Vec::new(),
});
Signal {
id,
source: source.0,
type_: type_.0,
short_name: row.get(3)?,
cameras: Vec::new(),
},
);
}
Ok(signals)
}
fn init_points(conn: &Connection) -> Result<BTreeMap<recording::Time, Point>, Error> {
let mut stmt = conn.prepare(r#"
let mut stmt = conn.prepare(
r#"
select
time_90k,
changes
from
signal_change
order by time_90k
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
let mut points = BTreeMap::new();
let mut cur = BTreeMap::new(); // latest signal -> state, where state != 0
let mut cur = BTreeMap::new(); // latest signal -> state, where state != 0
while let Some(row) = rows.next()? {
let time_90k = recording::Time(row.get(0)?);
let changes = row.get_raw_checked(1)?.as_blob()?;
@ -607,9 +656,12 @@ impl State {
}
/// Fills the `cameras` field of the `Signal` structs within the supplied `signals`.
fn fill_signal_cameras(conn: &Connection, signals: &mut BTreeMap<u32, Signal>)
-> Result<(), Error> {
let mut stmt = conn.prepare(r#"
fn fill_signal_cameras(
conn: &Connection,
signals: &mut BTreeMap<u32, Signal>,
) -> Result<(), Error> {
let mut stmt = conn.prepare(
r#"
select
signal_id,
camera_id,
@ -617,13 +669,14 @@ impl State {
from
signal_camera
order by signal_id, camera_id
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let signal_id = row.get(0)?;
let s = signals.get_mut(&signal_id)
.ok_or_else(|| format_err!("signal_camera row for unknown signal id {}",
signal_id))?;
let s = signals.get_mut(&signal_id).ok_or_else(|| {
format_err!("signal_camera row for unknown signal id {}", signal_id)
})?;
let type_ = row.get(2)?;
s.cameras.push(SignalCamera {
camera_id: row.get(1)?,
@ -639,7 +692,8 @@ impl State {
fn init_types(conn: &Connection) -> Result<FnvHashMap<Uuid, Type>, Error> {
let mut types = FnvHashMap::default();
let mut stmt = conn.prepare(r#"
let mut stmt = conn.prepare(
r#"
select
type_uuid,
value,
@ -649,22 +703,31 @@ impl State {
from
signal_type_enum
order by type_uuid, value
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let type_: FromSqlUuid = row.get(0)?;
types.entry(type_.0).or_insert_with(Type::default).states.push(TypeState {
value: row.get(1)?,
name: row.get(2)?,
motion: row.get(3)?,
color: row.get(4)?,
});
types
.entry(type_.0)
.or_insert_with(Type::default)
.states
.push(TypeState {
value: row.get(1)?,
name: row.get(2)?,
motion: row.get(3)?,
color: row.get(4)?,
});
}
Ok(types)
}
pub fn signals_by_id(&self) -> &BTreeMap<u32, Signal> { &self.signals_by_id }
pub fn types_by_uuid(&self) -> &FnvHashMap<Uuid, Type> { & self.types_by_uuid }
pub fn signals_by_id(&self) -> &BTreeMap<u32, Signal> {
&self.signals_by_id
}
pub fn types_by_uuid(&self) -> &FnvHashMap<Uuid, Type> {
&self.types_by_uuid
}
}
/// Representation of a `signal` row.
@ -698,9 +761,9 @@ pub struct Type {
#[cfg(test)]
mod tests {
use super::*;
use crate::{db, testutil};
use rusqlite::Connection;
use super::*;
#[test]
fn test_point_data_it() {
@ -719,8 +782,10 @@ mod tests {
let mut conn = Connection::open_in_memory().unwrap();
db::init(&mut conn).unwrap();
let s = State::init(&conn).unwrap();
s.list_changes_by_time(recording::Time::min_value() .. recording::Time::max_value(),
&mut |_r| panic!("no changes expected"));
s.list_changes_by_time(
recording::Time::min_value()..recording::Time::max_value(),
&mut |_r| panic!("no changes expected"),
);
}
#[test]
@ -728,7 +793,8 @@ mod tests {
testutil::init();
let mut conn = Connection::open_in_memory().unwrap();
db::init(&mut conn).unwrap();
conn.execute_batch(r#"
conn.execute_batch(
r#"
update meta set max_signal_changes = 2;
insert into signal (id, source_uuid, type_uuid, short_name)
@ -740,12 +806,16 @@ mod tests {
insert into signal_type_enum (type_uuid, value, name, motion, color)
values (x'EE66270FD9C648198B339720D4CBCA6B', 1, 'still', 0, 'black'),
(x'EE66270FD9C648198B339720D4CBCA6B', 2, 'moving', 1, 'red');
"#).unwrap();
"#,
)
.unwrap();
let mut s = State::init(&conn).unwrap();
s.list_changes_by_time(recording::Time::min_value() .. recording::Time::max_value(),
&mut |_r| panic!("no changes expected"));
s.list_changes_by_time(
recording::Time::min_value()..recording::Time::max_value(),
&mut |_r| panic!("no changes expected"),
);
const START: recording::Time = recording::Time(140067462600000); // 2019-04-26T11:59:00
const NOW: recording::Time = recording::Time(140067468000000); // 2019-04-26T12:00:00
const NOW: recording::Time = recording::Time(140067468000000); // 2019-04-26T12:00:00
s.update_signals(START..NOW, &[1, 2], &[2, 1]).unwrap();
let mut rows = Vec::new();
@ -770,10 +840,12 @@ mod tests {
signal: 2,
state: 0,
},
];
];
s.list_changes_by_time(recording::Time::min_value() .. recording::Time::max_value(),
&mut |r| rows.push(*r));
s.list_changes_by_time(
recording::Time::min_value()..recording::Time::max_value(),
&mut |r| rows.push(*r),
);
assert_eq!(&rows[..], EXPECTED);
{
@ -785,8 +857,10 @@ mod tests {
drop(s);
let mut s = State::init(&conn).unwrap();
rows.clear();
s.list_changes_by_time(recording::Time::min_value() .. recording::Time::max_value(),
&mut |r| rows.push(*r));
s.list_changes_by_time(
recording::Time::min_value()..recording::Time::max_value(),
&mut |r| rows.push(*r),
);
assert_eq!(&rows[..], EXPECTED);
// Go through it again. This time, hit the max number of signals, forcing START to be
@ -815,9 +889,11 @@ mod tests {
signal: 2,
state: 0,
},
];
s.list_changes_by_time(recording::Time::min_value() .. recording::Time::max_value(),
&mut |r| rows.push(*r));
];
s.list_changes_by_time(
recording::Time::min_value()..recording::Time::max_value(),
&mut |r| rows.push(*r),
);
assert_eq!(&rows[..], EXPECTED2);
{
@ -828,8 +904,10 @@ mod tests {
drop(s);
let s = State::init(&conn).unwrap();
rows.clear();
s.list_changes_by_time(recording::Time::min_value() .. recording::Time::max_value(),
&mut |r| rows.push(*r));
s.list_changes_by_time(
recording::Time::min_value()..recording::Time::max_value(),
&mut |r| rows.push(*r),
);
assert_eq!(&rows[..], EXPECTED2);
}
}

View File

@ -28,9 +28,10 @@
// 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 base::clock::Clocks;
use crate::db;
use crate::dir;
use crate::writer;
use base::clock::Clocks;
use fnv::FnvHashMap;
use mylog;
use rusqlite;
@ -40,7 +41,6 @@ use std::thread;
use tempdir::TempDir;
use time;
use uuid::Uuid;
use crate::writer;
static INIT: parking_lot::Once = parking_lot::Once::new();
@ -101,29 +101,39 @@ impl<C: Clocks + Clone> TestDb<C> {
{
let mut l = db.lock();
sample_file_dir_id = l.add_sample_file_dir(path.to_owned()).unwrap();
assert_eq!(TEST_CAMERA_ID, l.add_camera(db::CameraChange {
short_name: "test camera".to_owned(),
description: "".to_owned(),
onvif_host: "test-camera".to_owned(),
username: "foo".to_owned(),
password: "bar".to_owned(),
streams: [
db::StreamChange {
sample_file_dir_id: Some(sample_file_dir_id),
rtsp_url: "rtsp://test-camera/main".to_owned(),
record: true,
flush_if_sec,
},
Default::default(),
],
}).unwrap());
assert_eq!(
TEST_CAMERA_ID,
l.add_camera(db::CameraChange {
short_name: "test camera".to_owned(),
description: "".to_owned(),
onvif_host: "test-camera".to_owned(),
username: "foo".to_owned(),
password: "bar".to_owned(),
streams: [
db::StreamChange {
sample_file_dir_id: Some(sample_file_dir_id),
rtsp_url: "rtsp://test-camera/main".to_owned(),
record: true,
flush_if_sec,
},
Default::default(),
],
})
.unwrap()
);
test_camera_uuid = l.cameras_by_id().get(&TEST_CAMERA_ID).unwrap().uuid;
l.update_retention(&[db::RetentionChange {
stream_id: TEST_STREAM_ID,
new_record: true,
new_limit: 1048576,
}]).unwrap();
dir = l.sample_file_dirs_by_id().get(&sample_file_dir_id).unwrap().get().unwrap();
}])
.unwrap();
dir = l
.sample_file_dirs_by_id()
.get(&sample_file_dir_id)
.unwrap()
.get()
.unwrap();
}
let mut dirs_by_stream_id = FnvHashMap::default();
dirs_by_stream_id.insert(TEST_STREAM_ID, dir.clone());
@ -142,48 +152,63 @@ impl<C: Clocks + Clone> TestDb<C> {
/// Creates a recording with a fresh `RecordingToInsert` row which has been touched only by
/// a `SampleIndexEncoder`. Fills in a video sample entry id and such to make it valid.
/// There will no backing sample file, so it won't be possible to generate a full `.mp4`.
pub fn insert_recording_from_encoder(&self, r: db::RecordingToInsert)
-> db::ListRecordingsRow {
pub fn insert_recording_from_encoder(&self, r: db::RecordingToInsert) -> db::ListRecordingsRow {
use crate::recording::{self, TIME_UNITS_PER_SEC};
let mut db = self.db.lock();
let video_sample_entry_id = db.insert_video_sample_entry(db::VideoSampleEntryToInsert {
let video_sample_entry_id = db
.insert_video_sample_entry(db::VideoSampleEntryToInsert {
width: 1920,
height: 1080,
pasp_h_spacing: 1,
pasp_v_spacing: 1,
data: [0u8; 100].to_vec(),
rfc6381_codec: "avc1.000000".to_owned(),
})
.unwrap();
let (id, _) = db
.add_recording(
TEST_STREAM_ID,
db::RecordingToInsert {
start: recording::Time(1430006400i64 * TIME_UNITS_PER_SEC),
video_sample_entry_id,
wall_duration_90k: r.media_duration_90k,
..r
},
)
.unwrap();
db.mark_synced(id).unwrap();
db.flush("create_recording_from_encoder").unwrap();
let mut row = None;
db.list_recordings_by_id(
TEST_STREAM_ID,
id.recording()..id.recording() + 1,
&mut |r| {
row = Some(r);
Ok(())
},
)
.unwrap();
row.unwrap()
}
}
// For benchmarking
#[cfg(feature = "nightly")]
pub fn add_dummy_recordings_to_db(db: &db::Database, num: usize) {
use crate::recording::{self, TIME_UNITS_PER_SEC};
let mut data = Vec::new();
data.extend_from_slice(include_bytes!("testdata/video_sample_index.bin"));
let mut db = db.lock();
let video_sample_entry_id = db
.insert_video_sample_entry(db::VideoSampleEntryToInsert {
width: 1920,
height: 1080,
pasp_h_spacing: 1,
pasp_v_spacing: 1,
data: [0u8; 100].to_vec(),
rfc6381_codec: "avc1.000000".to_owned(),
}).unwrap();
let (id, _) = db.add_recording(TEST_STREAM_ID, db::RecordingToInsert {
start: recording::Time(1430006400i64 * TIME_UNITS_PER_SEC),
video_sample_entry_id,
wall_duration_90k: r.media_duration_90k,
..r
}).unwrap();
db.mark_synced(id).unwrap();
db.flush("create_recording_from_encoder").unwrap();
let mut row = None;
db.list_recordings_by_id(TEST_STREAM_ID, id.recording() .. id.recording()+1,
&mut |r| { row = Some(r); Ok(()) }).unwrap();
row.unwrap()
}
}
// For benchmarking
#[cfg(feature="nightly")]
pub fn add_dummy_recordings_to_db(db: &db::Database, num: usize) {
use crate::recording::{self, TIME_UNITS_PER_SEC};
let mut data = Vec::new();
data.extend_from_slice(include_bytes!("testdata/video_sample_index.bin"));
let mut db = db.lock();
let video_sample_entry_id = db.insert_video_sample_entry(db::VideoSampleEntryToInsert {
width: 1920,
height: 1080,
pasp_h_spacing: 1,
pasp_v_spacing: 1,
data: [0u8; 100].to_vec(),
rfc6381_codec: "avc1.000000".to_owned(),
}).unwrap();
})
.unwrap();
let mut recording = db::RecordingToInsert {
sample_file_bytes: 30104460,
start: recording::Time(1430006400i64 * TIME_UNITS_PER_SEC),

View File

@ -31,14 +31,13 @@
/// Upgrades the database schema.
///
/// See `guide/schema.md` for more information.
use crate::db;
use failure::{Error, bail};
use failure::{bail, Error};
use log::info;
use std::ffi::CStr;
use std::io::Write;
use nix::NixPath;
use rusqlite::params;
use std::ffi::CStr;
use std::io::Write;
use uuid::Uuid;
mod v0_to_v1;
@ -59,10 +58,16 @@ pub struct Args<'a> {
}
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
assert!(!requested.contains(';')); // quick check for accidental sql injection.
let actual = conn.query_row(&format!("pragma journal_mode = {}", requested), params![],
|row| row.get::<_, String>(0))?;
info!("...database now in journal_mode {} (requested {}).", actual, requested);
assert!(!requested.contains(';')); // quick check for accidental sql injection.
let actual = conn.query_row(
&format!("pragma journal_mode = {}", requested),
params![],
|row| row.get::<_, String>(0),
)?;
info!(
"...database now in journal_mode {} (requested {}).",
actual, requested
);
Ok(())
}
@ -78,24 +83,31 @@ fn upgrade(args: &Args, target_ver: i32, conn: &mut rusqlite::Connection) -> Res
{
assert_eq!(upgraders.len(), db::EXPECTED_VERSION as usize);
let old_ver =
conn.query_row("select max(id) from version", params![],
|row| row.get(0))?;
let old_ver = conn.query_row("select max(id) from version", params![], |row| row.get(0))?;
if old_ver > db::EXPECTED_VERSION {
bail!("Database is at version {}, later than expected {}",
old_ver, db::EXPECTED_VERSION);
bail!(
"Database is at version {}, later than expected {}",
old_ver,
db::EXPECTED_VERSION
);
} else if old_ver < 0 {
bail!("Database is at negative version {}!", old_ver);
}
info!("Upgrading database from version {} to version {}...", old_ver, target_ver);
for ver in old_ver .. target_ver {
info!(
"Upgrading database from version {} to version {}...",
old_ver, target_ver
);
for ver in old_ver..target_ver {
info!("...from version {} to version {}", ver, ver + 1);
let tx = conn.transaction()?;
upgraders[ver as usize](&args, &tx)?;
tx.execute(r#"
tx.execute(
r#"
insert into version (id, unix_time, notes)
values (?, cast(strftime('%s', 'now') as int32), ?)
"#, params![ver + 1, UPGRADE_NOTES])?;
"#,
params![ver + 1, UPGRADE_NOTES],
)?;
tx.commit()?;
}
}
@ -117,10 +129,12 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> {
// non-WAL mode. https://www.sqlite.org/wal.html
if !args.no_vacuum {
info!("...vacuuming database after upgrade.");
conn.execute_batch(r#"
conn.execute_batch(
r#"
pragma page_size = 16384;
vacuum;
"#)?;
"#,
)?;
}
set_journal_mode(&conn, "wal")?;
@ -142,11 +156,17 @@ impl UuidPath {
}
impl NixPath for UuidPath {
fn is_empty(&self) -> bool { false }
fn len(&self) -> usize { 36 }
fn is_empty(&self) -> bool {
false
}
fn len(&self) -> usize {
36
}
fn with_nix_path<T, F>(&self, f: F) -> Result<T, nix::Error>
where F: FnOnce(&CStr) -> T {
where
F: FnOnce(&CStr) -> T,
{
let p = CStr::from_bytes_with_nul(&self.0[..]).expect("no interior nuls");
Ok(f(p))
}
@ -154,14 +174,13 @@ impl NixPath for UuidPath {
#[cfg(test)]
mod tests {
use super::*;
use crate::compare;
use crate::testutil;
use failure::ResultExt;
use fnv::FnvHashMap;
use super::*;
const BAD_ANAMORPHIC_VIDEO_SAMPLE_ENTRY: &[u8] =
b"\x00\x00\x00\x84\x61\x76\x63\x31\x00\x00\
const BAD_ANAMORPHIC_VIDEO_SAMPLE_ENTRY: &[u8] = b"\x00\x00\x00\x84\x61\x76\x63\x31\x00\x00\
\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x01\x40\x00\xf0\x00\x48\x00\x00\x00\x48\
\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\
@ -209,29 +228,44 @@ mod tests {
//let path = tmpdir.path().to_str().ok_or_else(|| format_err!("invalid UTF-8"))?.to_owned();
let mut upgraded = new_conn()?;
upgraded.execute_batch(include_str!("v0.sql"))?;
upgraded.execute_batch(r#"
upgraded.execute_batch(
r#"
insert into camera (id, uuid, short_name, description, host, username, password,
main_rtsp_path, sub_rtsp_path, retain_bytes)
values (1, zeroblob(16), 'test camera', 'desc', 'host', 'user', 'pass',
'main', 'sub', 42);
"#)?;
upgraded.execute(r#"
"#,
)?;
upgraded.execute(
r#"
insert into video_sample_entry (id, sha1, width, height, data)
values (1, X'0000000000000000000000000000000000000000', 1920, 1080, ?);
"#, params![testutil::TEST_VIDEO_SAMPLE_ENTRY_DATA])?;
upgraded.execute(r#"
"#,
params![testutil::TEST_VIDEO_SAMPLE_ENTRY_DATA],
)?;
upgraded.execute(
r#"
insert into video_sample_entry (id, sha1, width, height, data)
values (2, X'0000000000000000000000000000000000000001', 320, 240, ?);
"#, params![BAD_ANAMORPHIC_VIDEO_SAMPLE_ENTRY])?;
upgraded.execute(r#"
"#,
params![BAD_ANAMORPHIC_VIDEO_SAMPLE_ENTRY],
)?;
upgraded.execute(
r#"
insert into video_sample_entry (id, sha1, width, height, data)
values (3, X'0000000000000000000000000000000000000002', 704, 480, ?);
"#, params![GOOD_ANAMORPHIC_VIDEO_SAMPLE_ENTRY])?;
upgraded.execute(r#"
"#,
params![GOOD_ANAMORPHIC_VIDEO_SAMPLE_ENTRY],
)?;
upgraded.execute(
r#"
insert into video_sample_entry (id, sha1, width, height, data)
values (4, X'0000000000000000000000000000000000000003', 704, 480, ?);
"#, params![GOOD_ANAMORPHIC_VIDEO_SAMPLE_ENTRY])?;
upgraded.execute_batch(r#"
"#,
params![GOOD_ANAMORPHIC_VIDEO_SAMPLE_ENTRY],
)?;
upgraded.execute_batch(
r#"
insert into recording (id, camera_id, sample_file_bytes, start_time_90k, duration_90k,
local_time_delta_90k, video_samples, video_sync_samples,
video_sample_entry_id, sample_file_uuid, sample_file_sha1,
@ -244,7 +278,8 @@ mod tests {
X'C94D4D0B533746059CD40B29039E641E', zeroblob(20), X'00');
insert into reserved_sample_files values (X'51EF700C933E4197AAE4EE8161E94221', 0),
(X'E69D45E8CBA64DC1BA2ECB1585983A10', 1);
"#)?;
"#,
)?;
let rec1 = tmpdir.path().join("e69d45e8-cba6-4dc1-ba2e-cb1585983a10");
let rec2 = tmpdir.path().join("94de8484-ff87-4a52-95d4-88c8038a0312");
let rec3 = tmpdir.path().join("c94d4d0b-5337-4605-9cd4-0b29039e641e");
@ -254,17 +289,24 @@ mod tests {
std::fs::File::create(&rec3)?;
std::fs::File::create(&garbage)?;
for (ver, fresh_sql) in &[(1, Some(include_str!("v1.sql"))),
(2, None), // transitional; don't compare schemas.
(3, Some(include_str!("v3.sql"))),
(4, None), // transitional; don't compare schemas.
(5, Some(include_str!("v5.sql"))),
(6, Some(include_str!("../schema.sql")))] {
upgrade(&Args {
sample_file_dir: Some(&tmpdir.path()),
preset_journal: "delete",
no_vacuum: false,
}, *ver, &mut upgraded).context(format!("upgrading to version {}", ver))?;
for (ver, fresh_sql) in &[
(1, Some(include_str!("v1.sql"))),
(2, None), // transitional; don't compare schemas.
(3, Some(include_str!("v3.sql"))),
(4, None), // transitional; don't compare schemas.
(5, Some(include_str!("v5.sql"))),
(6, Some(include_str!("../schema.sql"))),
] {
upgrade(
&Args {
sample_file_dir: Some(&tmpdir.path()),
preset_journal: "delete",
no_vacuum: false,
},
*ver,
&mut upgraded,
)
.context(format!("upgrading to version {}", ver))?;
if let Some(f) = fresh_sql {
compare(&upgraded, *ver, f)?;
}
@ -277,14 +319,16 @@ mod tests {
}
if *ver == 6 {
// Check that the pasp was set properly.
let mut stmt = upgraded.prepare(r#"
let mut stmt = upgraded.prepare(
r#"
select
id,
pasp_h_spacing,
pasp_v_spacing
from
video_sample_entry
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
let mut pasp_by_id = FnvHashMap::default();
while let Some(row) = rows.next()? {

View File

@ -29,7 +29,6 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/// Upgrades a version 0 schema to a version 1 schema.
use crate::db;
use crate::recording;
use failure::Error;
@ -39,7 +38,8 @@ use std::collections::HashMap;
pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
// These create statements match the schema.sql when version 1 was the latest.
tx.execute_batch(r#"
tx.execute_batch(
r#"
alter table camera rename to old_camera;
create table camera (
id integer primary key,
@ -103,13 +103,16 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
1 as next_recording_id
from
old_camera;
"#)?;
"#,
)?;
let camera_state = fill_recording(tx)?;
update_camera(tx, camera_state)?;
tx.execute_batch(r#"
tx.execute_batch(
r#"
drop table old_recording;
drop table old_camera;
"#)?;
"#,
)?;
Ok(())
}
@ -130,7 +133,8 @@ fn has_trailing_zero(video_index: &[u8]) -> Result<bool, Error> {
/// Fills the `recording` and `recording_playback` tables from `old_recording`, returning
/// the `camera_state` map for use by a following call to `fill_cameras`.
fn fill_recording(tx: &rusqlite::Transaction) -> Result<HashMap<i32, CameraState>, Error> {
let mut select = tx.prepare(r#"
let mut select = tx.prepare(
r#"
select
camera_id,
sample_file_bytes,
@ -146,27 +150,32 @@ fn fill_recording(tx: &rusqlite::Transaction) -> Result<HashMap<i32, CameraState
id
from
old_recording
"#)?;
let mut insert1 = tx.prepare(r#"
"#,
)?;
let mut insert1 = tx.prepare(
r#"
insert into recording values (:composite_id, :camera_id, :run_offset, :flags,
:sample_file_bytes, :start_time_90k, :duration_90k,
:local_time_delta_90k, :video_samples, :video_sync_samples,
:video_sample_entry_id)
"#)?;
let mut insert2 = tx.prepare(r#"
"#,
)?;
let mut insert2 = tx.prepare(
r#"
insert into recording_playback values (:composite_id, :sample_file_uuid, :sample_file_sha1,
:video_index)
"#)?;
"#,
)?;
let mut rows = select.query(params![])?;
let mut camera_state: HashMap<i32, CameraState> = HashMap::new();
while let Some(row) = rows.next()? {
let camera_id: i32 = row.get(0)?;
let camera_state = camera_state.entry(camera_id).or_insert_with(|| {
CameraState{
let camera_state = camera_state
.entry(camera_id)
.or_insert_with(|| CameraState {
current_run: None,
next_recording_id: 1,
}
});
});
let composite_id = ((camera_id as i64) << 32) | (camera_state.next_recording_id as i64);
camera_state.next_recording_id += 1;
let sample_file_bytes: i32 = row.get(1)?;
@ -181,9 +190,15 @@ fn fill_recording(tx: &rusqlite::Transaction) -> Result<HashMap<i32, CameraState
let video_index: Vec<u8> = row.get(10)?;
let old_id: i32 = row.get(11)?;
let trailing_zero = has_trailing_zero(&video_index).unwrap_or_else(|e| {
warn!("recording {}/{} (sample file {}, formerly recording {}) has corrupt \
video_index: {}",
camera_id, composite_id & 0xFFFF, sample_file_uuid.0, old_id, e);
warn!(
"recording {}/{} (sample file {}, formerly recording {}) has corrupt \
video_index: {}",
camera_id,
composite_id & 0xFFFF,
sample_file_uuid.0,
old_id,
e
);
false
});
let run_id = match camera_state.current_run {
@ -194,7 +209,14 @@ fn fill_recording(tx: &rusqlite::Transaction) -> Result<HashMap<i32, CameraState
(":composite_id", &composite_id),
(":camera_id", &camera_id),
(":run_offset", &(composite_id - run_id)),
(":flags", &(if trailing_zero { db::RecordingFlags::TrailingZero as i32 } else { 0 })),
(
":flags",
&(if trailing_zero {
db::RecordingFlags::TrailingZero as i32
} else {
0
}),
),
(":sample_file_bytes", &sample_file_bytes),
(":start_time_90k", &start_time_90k),
(":duration_90k", &duration_90k),
@ -218,11 +240,15 @@ fn fill_recording(tx: &rusqlite::Transaction) -> Result<HashMap<i32, CameraState
Ok(camera_state)
}
fn update_camera(tx: &rusqlite::Transaction, camera_state: HashMap<i32, CameraState>)
-> Result<(), Error> {
let mut stmt = tx.prepare(r#"
fn update_camera(
tx: &rusqlite::Transaction,
camera_state: HashMap<i32, CameraState>,
) -> Result<(), Error> {
let mut stmt = tx.prepare(
r#"
update camera set next_recording_id = :next_recording_id where id = :id
"#)?;
"#,
)?;
for (ref id, ref state) in &camera_state {
stmt.execute_named(&[
(":id", &id),

View File

@ -29,29 +29,31 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/// Upgrades a version 1 schema to a version 2 schema.
use crate::dir;
use failure::{Error, bail, format_err};
use crate::schema::DirMeta;
use failure::{bail, format_err, Error};
use nix::fcntl::{FlockArg, OFlag};
use nix::sys::stat::Mode;
use rusqlite::params;
use crate::schema::DirMeta;
use std::os::unix::io::AsRawFd;
use uuid::Uuid;
pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
let sample_file_path =
args.sample_file_dir
.ok_or_else(|| format_err!("--sample-file-dir required when upgrading from \
schema version 1 to 2."))?;
let sample_file_path = args.sample_file_dir.ok_or_else(|| {
format_err!("--sample-file-dir required when upgrading from schema version 1 to 2.")
})?;
let mut d = nix::dir::Dir::open(sample_file_path, OFlag::O_DIRECTORY | OFlag::O_RDONLY,
Mode::empty())?;
let mut d = nix::dir::Dir::open(
sample_file_path,
OFlag::O_DIRECTORY | OFlag::O_RDONLY,
Mode::empty(),
)?;
nix::fcntl::flock(d.as_raw_fd(), FlockArg::LockExclusiveNonblock)?;
verify_dir_contents(sample_file_path, &mut d, tx)?;
// These create statements match the schema.sql when version 2 was the latest.
tx.execute_batch(r#"
tx.execute_batch(
r#"
create table meta (
uuid blob not null check (length(uuid) = 16)
);
@ -99,13 +101,17 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
use_count not null default 0
) without rowid;
create index user_session_uid on user_session (user_id);
"#)?;
"#,
)?;
let db_uuid = ::uuid::Uuid::new_v4();
let db_uuid_bytes = &db_uuid.as_bytes()[..];
tx.execute("insert into meta (uuid) values (?)", params![db_uuid_bytes])?;
let open_uuid = ::uuid::Uuid::new_v4();
let open_uuid_bytes = &open_uuid.as_bytes()[..];
tx.execute("insert into open (uuid) values (?)", params![open_uuid_bytes])?;
tx.execute(
"insert into open (uuid) values (?)",
params![open_uuid_bytes],
)?;
let open_id = tx.last_insert_rowid() as u32;
let dir_uuid = ::uuid::Uuid::new_v4();
let dir_uuid_bytes = &dir_uuid.as_bytes()[..];
@ -121,15 +127,22 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
}
dir::write_meta(d.as_raw_fd(), &meta)?;
let sample_file_path = sample_file_path.to_str()
.ok_or_else(|| format_err!("sample file dir {} is not a valid string",
sample_file_path.display()))?;
tx.execute(r#"
let sample_file_path = sample_file_path.to_str().ok_or_else(|| {
format_err!(
"sample file dir {} is not a valid string",
sample_file_path.display()
)
})?;
tx.execute(
r#"
insert into sample_file_dir (path, uuid, last_complete_open_id)
values (?, ?, ?)
"#, params![sample_file_path, dir_uuid_bytes, open_id])?;
"#,
params![sample_file_path, dir_uuid_bytes, open_id],
)?;
tx.execute_batch(r#"
tx.execute_batch(
r#"
drop table reserved_sample_files;
alter table camera rename to old_camera;
alter table recording rename to old_recording;
@ -253,12 +266,14 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
old_camera cross join sample_file_dir
where
old_camera.sub_rtsp_path != '';
"#)?;
"#,
)?;
// Add the new video_sample_entry rows, before inserting the recordings referencing them.
fix_video_sample_entry(tx)?;
tx.execute_batch(r#"
tx.execute_batch(
r#"
insert into recording
select
r.composite_id,
@ -282,7 +297,8 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
p.sample_file_sha1
from
old_recording r join recording_playback p on (r.composite_id = p.composite_id);
"#)?;
"#,
)?;
Ok(())
}
@ -295,16 +311,23 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
/// * optional: reserved sample file uuids.
/// * optional: meta and meta-tmp from half-completed update attempts.
/// * forbidden: anything else.
fn verify_dir_contents(sample_file_path: &std::path::Path, dir: &mut nix::dir::Dir,
tx: &rusqlite::Transaction) -> Result<(), Error> {
fn verify_dir_contents(
sample_file_path: &std::path::Path,
dir: &mut nix::dir::Dir,
tx: &rusqlite::Transaction,
) -> Result<(), Error> {
// Build a hash of the uuids found in the directory.
let n: i64 = tx.query_row(r#"
let n: i64 = tx.query_row(
r#"
select
a.c + b.c
from
(select count(*) as c from recording) a,
(select count(*) as c from reserved_sample_files) b;
"#, params![], |r| r.get(0))?;
"#,
params![],
|r| r.get(0),
)?;
let mut files = ::fnv::FnvHashSet::with_capacity_and_hasher(n as usize, Default::default());
for e in dir.iter() {
let e = e?;
@ -315,8 +338,8 @@ fn verify_dir_contents(sample_file_path: &std::path::Path, dir: &mut nix::dir::D
// Ignore metadata files. These might from a half-finished update attempt.
// If the directory is actually an in-use >v3 format, other contents won't match.
continue;
},
_ => {},
}
_ => {}
};
let s = match f.to_str() {
Ok(s) => s,
@ -326,7 +349,8 @@ fn verify_dir_contents(sample_file_path: &std::path::Path, dir: &mut nix::dir::D
Ok(u) => u,
Err(_) => bail!("unexpected file {:?} in {:?}", f, sample_file_path),
};
if s != uuid.to_hyphenated_ref().to_string() { // non-canonical form.
if s != uuid.to_hyphenated_ref().to_string() {
// non-canonical form.
bail!("unexpected file {:?} in {:?}", f, sample_file_path);
}
files.insert(uuid);
@ -339,7 +363,11 @@ fn verify_dir_contents(sample_file_path: &std::path::Path, dir: &mut nix::dir::D
while let Some(row) = rows.next()? {
let uuid: crate::db::FromSqlUuid = row.get(0)?;
if !files.remove(&uuid.0) {
bail!("{} is missing from dir {}!", uuid.0, sample_file_path.display());
bail!(
"{} is missing from dir {}!",
uuid.0,
sample_file_path.display()
);
}
}
}
@ -355,20 +383,28 @@ fn verify_dir_contents(sample_file_path: &std::path::Path, dir: &mut nix::dir::D
// a garbage file so if the upgrade transation fails this is still a valid and complete
// version 1 database.
let p = super::UuidPath::from(uuid.0);
nix::unistd::unlinkat(Some(dir.as_raw_fd()), &p,
nix::unistd::UnlinkatFlags::NoRemoveDir)?;
nix::unistd::unlinkat(
Some(dir.as_raw_fd()),
&p,
nix::unistd::UnlinkatFlags::NoRemoveDir,
)?;
}
}
if !files.is_empty() {
bail!("{} unexpected sample file uuids in dir {}: {:?}!",
files.len(), sample_file_path.display(), files);
bail!(
"{} unexpected sample file uuids in dir {}: {:?}!",
files.len(),
sample_file_path.display(),
files
);
}
Ok(())
}
fn fix_video_sample_entry(tx: &rusqlite::Transaction) -> Result<(), Error> {
let mut select = tx.prepare(r#"
let mut select = tx.prepare(
r#"
select
id,
sha1,
@ -377,10 +413,13 @@ fn fix_video_sample_entry(tx: &rusqlite::Transaction) -> Result<(), Error> {
data
from
old_video_sample_entry
"#)?;
let mut insert = tx.prepare(r#"
"#,
)?;
let mut insert = tx.prepare(
r#"
insert into video_sample_entry values (:id, :sha1, :width, :height, :rfc6381_codec, :data)
"#)?;
"#,
)?;
let mut rows = select.query(params![])?;
while let Some(row) = rows.next()? {
let data: Vec<u8> = row.get(4)?;
@ -398,12 +437,15 @@ fn fix_video_sample_entry(tx: &rusqlite::Transaction) -> Result<(), Error> {
// This previously lived in h264.rs. As of version 1, H.264 is the only supported codec.
fn rfc6381_codec_from_sample_entry(sample_entry: &[u8]) -> Result<String, Error> {
if sample_entry.len() < 99 || &sample_entry[4..8] != b"avc1" ||
&sample_entry[90..94] != b"avcC" {
if sample_entry.len() < 99 || &sample_entry[4..8] != b"avc1" || &sample_entry[90..94] != b"avcC"
{
bail!("not a valid AVCSampleEntry");
}
let profile_idc = sample_entry[103];
let constraint_flags_byte = sample_entry[104];
let level_idc = sample_entry[105];
Ok(format!("avc1.{:02x}{:02x}{:02x}", profile_idc, constraint_flags_byte, level_idc))
Ok(format!(
"avc1.{:02x}{:02x}{:02x}",
profile_idc, constraint_flags_byte, level_idc
))
}

View File

@ -31,11 +31,10 @@
/// Upgrades a version 2 schema to a version 3 schema.
/// Note that a version 2 schema is never actually used; so we know the upgrade from version 1 was
/// completed, and possibly an upgrade from 2 to 3 is half-finished.
use crate::db::{self, FromSqlUuid};
use crate::dir;
use failure::Error;
use crate::schema;
use failure::Error;
use rusqlite::params;
use std::os::unix::io::AsRawFd;
use std::sync::Arc;
@ -47,20 +46,26 @@ use std::sync::Arc;
/// * it has a last completed open.
fn open_sample_file_dir(tx: &rusqlite::Transaction) -> Result<Arc<dir::SampleFileDir>, Error> {
let (p, s_uuid, o_id, o_uuid, db_uuid): (String, FromSqlUuid, i32, FromSqlUuid, FromSqlUuid) =
tx.query_row(r#"
select
s.path, s.uuid, s.last_complete_open_id, o.uuid, m.uuid
from
sample_file_dir s
join open o on (s.last_complete_open_id = o.id)
cross join meta m
"#, params![], |row| {
Ok((row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?))
})?;
tx.query_row(
r#"
select
s.path, s.uuid, s.last_complete_open_id, o.uuid, m.uuid
from
sample_file_dir s
join open o on (s.last_complete_open_id = o.id)
cross join meta m
"#,
params![],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)?;
let mut meta = schema::DirMeta::default();
meta.db_uuid.extend_from_slice(&db_uuid.0.as_bytes()[..]);
meta.dir_uuid.extend_from_slice(&s_uuid.0.as_bytes()[..]);
@ -74,30 +79,37 @@ fn open_sample_file_dir(tx: &rusqlite::Transaction) -> Result<Arc<dir::SampleFil
pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
let d = open_sample_file_dir(&tx)?;
let mut stmt = tx.prepare(r#"
let mut stmt = tx.prepare(
r#"
select
composite_id,
sample_file_uuid
from
recording_playback
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let id = db::CompositeId(row.get(0)?);
let sample_file_uuid: FromSqlUuid = row.get(1)?;
let from_path = super::UuidPath::from(sample_file_uuid.0);
let to_path = crate::dir::CompositeIdPath::from(id);
if let Err(e) = nix::fcntl::renameat(Some(d.fd.as_raw_fd()), &from_path,
Some(d.fd.as_raw_fd()), &to_path) {
if let Err(e) = nix::fcntl::renameat(
Some(d.fd.as_raw_fd()),
&from_path,
Some(d.fd.as_raw_fd()),
&to_path,
) {
if e == nix::Error::Sys(nix::errno::Errno::ENOENT) {
continue; // assume it was already moved.
continue; // assume it was already moved.
}
Err(e)?;
}
}
// These create statements match the schema.sql when version 3 was the latest.
tx.execute_batch(r#"
tx.execute_batch(
r#"
alter table recording_playback rename to old_recording_playback;
create table recording_playback (
composite_id integer primary key references recording (composite_id),
@ -113,6 +125,7 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
drop table old_recording;
drop table old_camera;
drop table old_video_sample_entry;
"#)?;
"#,
)?;
Ok(())
}

View File

@ -29,12 +29,12 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/// Upgrades a version 3 schema to a version 4 schema.
use failure::Error;
pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
// These create statements match the schema.sql when version 4 was the latest.
tx.execute_batch(r#"
tx.execute_batch(
r#"
alter table meta add column max_signal_changes integer check (max_signal_changes >= 0);
create table signal (
@ -191,6 +191,7 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
-- This was supposed to be present in version 2, but the upgrade procedure used to miss it.
-- Catch up so we know a version 4 database is right.
create index if not exists user_session_uid on user_session (user_id);
"#)?;
"#,
)?;
Ok(())
}

View File

@ -32,11 +32,10 @@
///
/// This just handles the directory meta files. If they're already in the new format, great.
/// Otherwise, verify they are consistent with the database then upgrade them.
use crate::db::FromSqlUuid;
use crate::{dir, schema};
use cstr::cstr;
use failure::{Error, Fail, bail};
use failure::{bail, Error, Fail};
use log::info;
use nix::fcntl::{FlockArg, OFlag};
use nix::sys::stat::Mode;
@ -61,25 +60,42 @@ fn maybe_upgrade_meta(dir: &dir::Fd, db_meta: &schema::DirMeta) -> Result<bool,
let mut s = protobuf::CodedInputStream::from_bytes(&data);
let mut dir_meta = schema::DirMeta::new();
dir_meta.merge_from(&mut s)
dir_meta
.merge_from(&mut s)
.map_err(|e| e.context("Unable to parse metadata proto: {}"))?;
if !dir::SampleFileDir::consistent(&db_meta, &dir_meta) {
bail!("Inconsistent db_meta={:?} dir_meta={:?}", &db_meta, &dir_meta);
bail!(
"Inconsistent db_meta={:?} dir_meta={:?}",
&db_meta,
&dir_meta
);
}
let mut f = crate::fs::openat(dir.as_raw_fd(), tmp_path,
OFlag::O_CREAT | OFlag::O_TRUNC | OFlag::O_WRONLY,
Mode::S_IRUSR | Mode::S_IWUSR)?;
let mut data =
dir_meta.write_length_delimited_to_bytes().expect("proto3->vec is infallible");
let mut f = crate::fs::openat(
dir.as_raw_fd(),
tmp_path,
OFlag::O_CREAT | OFlag::O_TRUNC | OFlag::O_WRONLY,
Mode::S_IRUSR | Mode::S_IWUSR,
)?;
let mut data = dir_meta
.write_length_delimited_to_bytes()
.expect("proto3->vec is infallible");
if data.len() > FIXED_DIR_META_LEN {
bail!("Length-delimited DirMeta message requires {} bytes, over limit of {}",
data.len(), FIXED_DIR_META_LEN);
bail!(
"Length-delimited DirMeta message requires {} bytes, over limit of {}",
data.len(),
FIXED_DIR_META_LEN
);
}
data.resize(FIXED_DIR_META_LEN, 0); // pad to required length.
data.resize(FIXED_DIR_META_LEN, 0); // pad to required length.
f.write_all(&data)?;
f.sync_all()?;
nix::fcntl::renameat(Some(dir.as_raw_fd()), tmp_path, Some(dir.as_raw_fd()), meta_path)?;
nix::fcntl::renameat(
Some(dir.as_raw_fd()),
tmp_path,
Some(dir.as_raw_fd()),
meta_path,
)?;
Ok(true)
}
@ -91,8 +107,12 @@ fn maybe_upgrade_meta(dir: &dir::Fd, db_meta: &schema::DirMeta) -> Result<bool,
/// Returns true if something was done (and thus a sync is needed).
fn maybe_cleanup_garbage_uuids(dir: &dir::Fd) -> Result<bool, Error> {
let mut need_sync = false;
let mut dir2 = nix::dir::Dir::openat(dir.as_raw_fd(), ".",
OFlag::O_DIRECTORY | OFlag::O_RDONLY, Mode::empty())?;
let mut dir2 = nix::dir::Dir::openat(
dir.as_raw_fd(),
".",
OFlag::O_DIRECTORY | OFlag::O_RDONLY,
Mode::empty(),
)?;
for e in dir2.iter() {
let e = e?;
let f = e.file_name();
@ -103,8 +123,11 @@ fn maybe_cleanup_garbage_uuids(dir: &dir::Fd) -> Result<bool, Error> {
};
if Uuid::parse_str(f_str).is_ok() {
info!("removing leftover garbage file {}", f_str);
nix::unistd::unlinkat(Some(dir.as_raw_fd()), f,
nix::unistd::UnlinkatFlags::NoRemoveDir)?;
nix::unistd::unlinkat(
Some(dir.as_raw_fd()),
f,
nix::unistd::UnlinkatFlags::NoRemoveDir,
)?;
need_sync = true;
}
}
@ -115,7 +138,8 @@ fn maybe_cleanup_garbage_uuids(dir: &dir::Fd) -> Result<bool, Error> {
pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
let db_uuid: FromSqlUuid =
tx.query_row_and_then(r"select uuid from meta", params![], |row| row.get(0))?;
let mut stmt = tx.prepare(r#"
let mut stmt = tx.prepare(
r#"
select
d.path,
d.uuid,
@ -124,7 +148,8 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
from
sample_file_dir d
left join open o on (d.last_complete_open_id = o.id);
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let path = row.get_raw_checked(0)?.as_str()?;
@ -134,14 +159,16 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
let open_uuid: Option<FromSqlUuid> = row.get(3)?;
let mut db_meta = schema::DirMeta::new();
db_meta.db_uuid.extend_from_slice(&db_uuid.0.as_bytes()[..]);
db_meta.dir_uuid.extend_from_slice(&dir_uuid.0.as_bytes()[..]);
db_meta
.dir_uuid
.extend_from_slice(&dir_uuid.0.as_bytes()[..]);
match (open_id, open_uuid) {
(Some(id), Some(uuid)) => {
let mut o = db_meta.last_complete_open.set_default();
o.id = id;
o.uuid.extend_from_slice(&uuid.0.as_bytes()[..]);
},
(None, None) => {},
}
(None, None) => {}
_ => bail!("open table missing id"),
}

View File

@ -29,9 +29,8 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/// Upgrades a version 4 schema to a version 5 schema.
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use failure::{Error, ResultExt, bail, format_err};
use failure::{bail, format_err, Error, ResultExt};
use h264_reader::avcc::AvcDecoderConfigurationRecord;
use rusqlite::{named_params, params};
use std::convert::{TryFrom, TryInto};
@ -39,9 +38,9 @@ use std::convert::{TryFrom, TryInto};
// Copied from src/h264.rs. h264 stuff really doesn't belong in the db crate, but we do what we
// must for schema upgrades.
const PIXEL_ASPECT_RATIOS: [((u16, u16), (u16, u16)); 4] = [
((320, 240), ( 4, 3)),
((320, 240), (4, 3)),
((352, 240), (40, 33)),
((640, 480), ( 4, 3)),
((640, 480), (4, 3)),
((704, 480), (40, 33)),
];
fn default_pixel_aspect_ratio(width: u16, height: u16) -> (u16, u16) {
@ -59,13 +58,16 @@ fn parse(data: &[u8]) -> Result<AvcDecoderConfigurationRecord, Error> {
bail!("data of len {} doesn't have an avcC", data.len());
}
let avcc_len = BigEndian::read_u32(&data[86..90]);
if avcc_len < 8 { // length and type.
if avcc_len < 8 {
// length and type.
bail!("invalid avcc len {}", avcc_len);
}
let end_pos = 86 + usize::try_from(avcc_len)?;
if end_pos != data.len() {
bail!("expected avcC to be end of extradata; there are {} more bytes.",
data.len() - end_pos);
bail!(
"expected avcC to be end of extradata; there are {} more bytes.",
data.len() - end_pos
);
}
AvcDecoderConfigurationRecord::try_from(&data[94..end_pos])
.map_err(|e| format_err!("Bad AvcDecoderConfigurationRecord: {:?}", e))
@ -73,7 +75,8 @@ fn parse(data: &[u8]) -> Result<AvcDecoderConfigurationRecord, Error> {
pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
// These create statements match the schema.sql when version 5 was the latest.
tx.execute_batch(r#"
tx.execute_batch(
r#"
alter table video_sample_entry rename to old_video_sample_entry;
create table video_sample_entry (
@ -85,19 +88,23 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
pasp_h_spacing integer not null default 1 check (pasp_h_spacing > 0),
pasp_v_spacing integer not null default 1 check (pasp_v_spacing > 0)
);
"#)?;
"#,
)?;
let mut insert = tx.prepare(r#"
let mut insert = tx.prepare(
r#"
insert into video_sample_entry (id, width, height, rfc6381_codec, data,
pasp_h_spacing, pasp_v_spacing)
values (:id, :width, :height, :rfc6381_codec, :data,
:pasp_h_spacing, :pasp_v_spacing)
"#)?;
"#,
)?;
// Only insert still-referenced video sample entries. I've had problems with
// no-longer-referenced ones (perhaps from some ancient, buggy version of Moonfire NVR) for
// which avcc.create_context(()) fails.
let mut stmt = tx.prepare(r#"
let mut stmt = tx.prepare(
r#"
select
id,
width,
@ -114,7 +121,8 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
recording r
where
r.video_sample_entry_id = v.id)
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let id: i32 = row.get(0)?;
@ -126,24 +134,31 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
if avcc.num_of_sequence_parameter_sets() != 1 {
bail!("Multiple SPSs!");
}
let ctx = avcc.create_context(())
.map_err(|e| format_err!("Can't load SPS+PPS for video_sample_entry_id {}: {:?}",
id, e))?;
let sps = ctx.sps_by_id(h264_reader::nal::pps::ParamSetId::from_u32(0).unwrap())
let ctx = avcc.create_context(()).map_err(|e| {
format_err!(
"Can't load SPS+PPS for video_sample_entry_id {}: {:?}",
id,
e
)
})?;
let sps = ctx
.sps_by_id(h264_reader::nal::pps::ParamSetId::from_u32(0).unwrap())
.ok_or_else(|| format_err!("No SPS 0 for video_sample_entry_id {}", id))?;
let pasp = sps.vui_parameters.as_ref()
.and_then(|v| v.aspect_ratio_info.as_ref())
.and_then(|a| a.clone().get())
.unwrap_or_else(|| default_pixel_aspect_ratio(width, height));
let pasp = sps
.vui_parameters
.as_ref()
.and_then(|v| v.aspect_ratio_info.as_ref())
.and_then(|a| a.clone().get())
.unwrap_or_else(|| default_pixel_aspect_ratio(width, height));
if pasp != (1, 1) {
data.extend_from_slice(b"\x00\x00\x00\x10pasp"); // length + box name
data.extend_from_slice(b"\x00\x00\x00\x10pasp"); // length + box name
data.write_u32::<BigEndian>(pasp.0.into())?;
data.write_u32::<BigEndian>(pasp.1.into())?;
let len = data.len();
BigEndian::write_u32(&mut data[0..4], u32::try_from(len)?);
}
insert.execute_named(named_params!{
insert.execute_named(named_params! {
":id": id,
":width": width,
":height": height,
@ -153,7 +168,8 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
":pasp_v_spacing": pasp.1,
})?;
}
tx.execute_batch(r#"
tx.execute_batch(
r#"
alter table stream rename to old_stream;
create table stream (
id integer primary key,
@ -205,14 +221,16 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
video_sample_entry_id integer references video_sample_entry (id),
check (composite_id >> 32 = stream_id)
);
"#)?;
"#,
)?;
// SQLite added window functions in 3.25.0. macOS still ships SQLite 3.24.0 (no support).
// Compute cumulative columns by hand.
let mut cur_stream_id = None;
let mut cum_duration_90k = 0;
let mut cum_runs = 0;
let mut stmt = tx.prepare(r#"
let mut stmt = tx.prepare(
r#"
select
composite_id,
open_id,
@ -228,8 +246,10 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
from
old_recording
order by composite_id
"#)?;
let mut insert = tx.prepare(r#"
"#,
)?;
let mut insert = tx.prepare(
r#"
insert into recording (composite_id, open_id, stream_id, run_offset, flags,
sample_file_bytes, start_time_90k, prev_media_duration_90k,
prev_runs, wall_duration_90k, media_duration_delta_90k,
@ -238,7 +258,8 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
:sample_file_bytes, :start_time_90k, :prev_media_duration_90k,
:prev_runs, :wall_duration_90k, 0, :video_samples,
:video_sync_samples, :video_sample_entry_id)
"#)?;
"#,
)?;
let mut rows = stmt.query(params![])?;
while let Some(row) = rows.next()? {
let composite_id: i64 = row.get(0)?;
@ -257,25 +278,28 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
cum_runs = 0;
cur_stream_id = Some(stream_id);
}
insert.execute_named(named_params!{
":composite_id": composite_id,
":open_id": open_id,
":stream_id": stream_id,
":run_offset": run_offset,
":flags": flags,
":sample_file_bytes": sample_file_bytes,
":start_time_90k": start_time_90k,
":prev_media_duration_90k": cum_duration_90k,
":prev_runs": cum_runs,
":wall_duration_90k": wall_duration_90k,
":video_samples": video_samples,
":video_sync_samples": video_sync_samples,
":video_sample_entry_id": video_sample_entry_id,
}).with_context(|_| format!("Unable to insert composite_id {}", composite_id))?;
insert
.execute_named(named_params! {
":composite_id": composite_id,
":open_id": open_id,
":stream_id": stream_id,
":run_offset": run_offset,
":flags": flags,
":sample_file_bytes": sample_file_bytes,
":start_time_90k": start_time_90k,
":prev_media_duration_90k": cum_duration_90k,
":prev_runs": cum_runs,
":wall_duration_90k": wall_duration_90k,
":video_samples": video_samples,
":video_sync_samples": video_sync_samples,
":video_sample_entry_id": video_sample_entry_id,
})
.with_context(|_| format!("Unable to insert composite_id {}", composite_id))?;
cum_duration_90k += i64::from(wall_duration_90k);
cum_runs += if run_offset == 0 { 1 } else { 0 };
}
tx.execute_batch(r#"
tx.execute_batch(
r#"
drop index recording_cover;
create index recording_cover on recording (
stream_id,
@ -328,6 +352,7 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
revocation_reason_detail = 'Blake2b->Blake3 upgrade'
where
revocation_reason is null;
"#)?;
"#,
)?;
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -50,7 +50,7 @@
//! interval to cut down on expense.
use cstr::cstr;
use failure::{Error, format_err};
use failure::{format_err, Error};
use ffmpeg;
use log::info;
use std::sync::Arc;
@ -163,17 +163,28 @@ impl ObjectDetector {
let model = moonfire_tflite::Model::from_static(MODEL)
.map_err(|()| format_err!("TensorFlow Lite model initialization failed"))?;
let devices = moonfire_tflite::edgetpu::Devices::list();
let device = devices.first().ok_or_else(|| format_err!("No Edge TPU device available"))?;
info!("Using device {:?}/{:?} for object detection", device.type_(), device.path());
let device = devices
.first()
.ok_or_else(|| format_err!("No Edge TPU device available"))?;
info!(
"Using device {:?}/{:?} for object detection",
device.type_(),
device.path()
);
let mut builder = moonfire_tflite::Interpreter::builder();
builder.add_owned_delegate(device.create_delegate()
.map_err(|()| format_err!("Unable to create delegate for {:?}/{:?}",
device.type_(), device.path()))?);
let interpreter = builder.build(&model)
builder.add_owned_delegate(device.create_delegate().map_err(|()| {
format_err!(
"Unable to create delegate for {:?}/{:?}",
device.type_(),
device.path()
)
})?);
let interpreter = builder
.build(&model)
.map_err(|()| format_err!("TensorFlow Lite initialization failed"))?;
Ok(Arc::new(Self {
interpreter: parking_lot::Mutex::new(interpreter),
width: 300, // TODO
width: 300, // TODO
height: 300,
}))
}
@ -194,17 +205,19 @@ fn copy(from: &ffmpeg::avutil::VideoFrame, to: &mut moonfire_tflite::Tensor) {
let mut from_i = 0;
let mut to_i = 0;
for _y in 0..h {
to[to_i..to_i+3*w].copy_from_slice(&from.data[from_i..from_i+3*w]);
to[to_i..to_i + 3 * w].copy_from_slice(&from.data[from_i..from_i + 3 * w]);
from_i += from.linesize;
to_i += 3*w;
to_i += 3 * w;
}
}
const SCORE_THRESHOLD: f32 = 0.5;
impl ObjectDetectorStream {
pub fn new(par: ffmpeg::avcodec::InputCodecParameters<'_>,
detector: &ObjectDetector) -> Result<Self, Error> {
pub fn new(
par: ffmpeg::avcodec::InputCodecParameters<'_>,
detector: &ObjectDetector,
) -> Result<Self, Error> {
let mut dopt = ffmpeg::avutil::Dictionary::new();
dopt.set(cstr!("refcounted_frames"), cstr!("0"))?;
let decoder = par.new_decoder(&mut dopt)?;
@ -223,15 +236,20 @@ impl ObjectDetectorStream {
})
}
pub fn process_frame(&mut self, pkt: &ffmpeg::avcodec::Packet<'_>,
detector: &ObjectDetector) -> Result<(), Error> {
pub fn process_frame(
&mut self,
pkt: &ffmpeg::avcodec::Packet<'_>,
detector: &ObjectDetector,
) -> Result<(), Error> {
if !self.decoder.decode_video(pkt, &mut self.frame)? {
return Ok(());
}
self.scaler.scale(&self.frame, &mut self.scaled);
let mut interpreter = detector.interpreter.lock();
copy(&self.scaled, &mut interpreter.inputs()[0]);
interpreter.invoke().map_err(|()| format_err!("TFLite interpreter invocation failed"))?;
interpreter
.invoke()
.map_err(|()| format_err!("TFLite interpreter invocation failed"))?;
let outputs = interpreter.outputs();
let classes = outputs[1].f32s();
let scores = outputs[2].f32s();

View File

@ -31,7 +31,7 @@
//! Tools for implementing a `http_serve::Entity` body composed from many "slices".
use base::Error;
use futures::{Stream, stream};
use futures::{stream, Stream};
use reffers::ARefss;
use std::error::Error as StdError;
use std::pin::Pin;
@ -47,28 +47,42 @@ pub fn wrap_error(e: Error) -> BoxedError {
}
impl From<ARefss<'static, [u8]>> for Chunk {
fn from(r: ARefss<'static, [u8]>) -> Self { Chunk(r) }
fn from(r: ARefss<'static, [u8]>) -> Self {
Chunk(r)
}
}
impl From<&'static [u8]> for Chunk {
fn from(r: &'static [u8]) -> Self { Chunk(ARefss::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(ARefss::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(ARefss::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(ARefss::new(r).map(|v| &v[..])) }
fn from(r: Vec<u8>) -> Self {
Chunk(ARefss::new(r).map(|v| &v[..]))
}
}
impl hyper::body::Buf for Chunk {
fn remaining(&self) -> usize { self.0.len() }
fn chunk(&self) -> &[u8] { &*self.0 }
fn remaining(&self) -> usize {
self.0.len()
}
fn chunk(&self) -> &[u8] {
&*self.0
}
fn advance(&mut self, cnt: usize) {
self.0 = ::std::mem::replace(&mut self.0, ARefss::new(&[][..])).map(|b| &b[cnt..]);
}
@ -83,32 +97,46 @@ impl hyper::body::HttpBody for Body {
type Data = Chunk;
type Error = BoxedError;
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.get_mut().as_mut().poll_next(cx)
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
.get_mut()
.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))
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(SyncWrapper::new(Pin::from(b))) }
fn from(b: BodyStream) -> Self {
Body(SyncWrapper::new(Pin::from(b)))
}
}
impl<C: Into<Chunk>> From<C> for Body {
fn from(c: C) -> Self {
Body(SyncWrapper::new(Box::pin(stream::once(futures::future::ok(c.into())))))
Body(SyncWrapper::new(Box::pin(stream::once(
futures::future::ok(c.into()),
))))
}
}
impl From<Error> for Body {
fn from(e: Error) -> Self {
Body(SyncWrapper::new(Box::pin(stream::once(futures::future::err(wrap_error(e))))))
Body(SyncWrapper::new(Box::pin(stream::once(
futures::future::err(wrap_error(e)),
))))
}
}

View File

@ -38,8 +38,12 @@ use structopt::StructOpt;
#[derive(StructOpt)]
pub struct Args {
/// Directory holding the SQLite3 index database.
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
#[structopt(
long,
default_value = "/var/lib/moonfire-nvr/db",
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf,
/// Compare sample file lengths on disk to the database.
@ -70,10 +74,13 @@ pub struct Args {
pub fn run(args: &Args) -> Result<i32, Error> {
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
check::run(&mut conn, &check::Options {
compare_lens: args.compare_lens,
trash_orphan_sample_files: args.trash_orphan_sample_files,
delete_orphan_rows: args.delete_orphan_rows,
trash_corrupt_rows: args.trash_corrupt_rows,
})
check::run(
&mut conn,
&check::Options {
compare_lens: args.compare_lens,
trash_orphan_sample_files: args.trash_orphan_sample_files,
delete_orphan_rows: args.delete_orphan_rows,
trash_corrupt_rows: args.trash_corrupt_rows,
},
)
}

View File

@ -28,11 +28,11 @@
// 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 base::strutil::{decode_size, encode_size};
use crate::stream::{self, Opener, Stream};
use cursive::Cursive;
use cursive::traits::{Boxable, Identifiable, Finder};
use base::strutil::{decode_size, encode_size};
use cursive::traits::{Boxable, Finder, Identifiable};
use cursive::views;
use cursive::Cursive;
use db::writer;
use failure::Error;
use std::collections::BTreeMap;
@ -44,11 +44,35 @@ use url::Url;
fn get_change(siv: &mut Cursive) -> db::CameraChange {
// Note: these find_name calls are separate statements, which seems to be important:
// https://github.com/gyscos/Cursive/issues/144
let sn = siv.find_name::<views::EditView>("short_name").unwrap().get_content().as_str().into();
let d = siv.find_name::<views::TextArea>("description").unwrap().get_content().into();
let h = siv.find_name::<views::EditView>("onvif_host").unwrap().get_content().as_str().into();
let u = siv.find_name::<views::EditView>("username").unwrap().get_content().as_str().into();
let p = siv.find_name::<views::EditView>("password").unwrap().get_content().as_str().into();
let sn = siv
.find_name::<views::EditView>("short_name")
.unwrap()
.get_content()
.as_str()
.into();
let d = siv
.find_name::<views::TextArea>("description")
.unwrap()
.get_content()
.into();
let h = siv
.find_name::<views::EditView>("onvif_host")
.unwrap()
.get_content()
.as_str()
.into();
let u = siv
.find_name::<views::EditView>("username")
.unwrap()
.get_content()
.as_str()
.into();
let p = siv
.find_name::<views::EditView>("password")
.unwrap()
.get_content()
.as_str()
.into();
let mut c = db::CameraChange {
short_name: sn,
description: d,
@ -58,16 +82,28 @@ fn get_change(siv: &mut Cursive) -> db::CameraChange {
streams: Default::default(),
};
for &t in &db::ALL_STREAM_TYPES {
let u = siv.find_name::<views::EditView>(&format!("{}_rtsp_url", t.as_str()))
.unwrap().get_content().as_str().into();
let r = siv.find_name::<views::Checkbox>(&format!("{}_record", t.as_str()))
.unwrap().is_checked();
let f = i64::from_str(siv.find_name::<views::EditView>(
&format!("{}_flush_if_sec", t.as_str())).unwrap().get_content().as_str())
.unwrap_or(0);
let d = *siv.find_name::<views::SelectView<Option<i32>>>(
&format!("{}_sample_file_dir", t.as_str()))
.unwrap().selection().unwrap();
let u = siv
.find_name::<views::EditView>(&format!("{}_rtsp_url", t.as_str()))
.unwrap()
.get_content()
.as_str()
.into();
let r = siv
.find_name::<views::Checkbox>(&format!("{}_record", t.as_str()))
.unwrap()
.is_checked();
let f = i64::from_str(
siv.find_name::<views::EditView>(&format!("{}_flush_if_sec", t.as_str()))
.unwrap()
.get_content()
.as_str(),
)
.unwrap_or(0);
let d = *siv
.find_name::<views::SelectView<Option<i32>>>(&format!("{}_sample_file_dir", t.as_str()))
.unwrap()
.selection()
.unwrap();
c.streams[t.index()] = db::StreamChange {
rtsp_url: u,
sample_file_dir_id: d,
@ -90,11 +126,13 @@ fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>) {
}
};
if let Err(e) = result {
siv.add_layer(views::Dialog::text(format!("Unable to add camera: {}", e))
.title("Error")
.dismiss_button("Abort"));
siv.add_layer(
views::Dialog::text(format!("Unable to add camera: {}", e))
.title("Error")
.dismiss_button("Abort"),
);
} else {
siv.pop_layer(); // get rid of the add/edit camera dialog.
siv.pop_layer(); // get rid of the add/edit camera dialog.
// Recreate the "Edit cameras" dialog from scratch; it's easier than adding the new entry.
siv.pop_layer();
@ -105,10 +143,13 @@ fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>) {
fn press_test_inner(url: &Url) -> Result<String, Error> {
let stream = stream::FFMPEG.open(stream::Source::Rtsp {
url: url.as_str(),
redacted_url: url.as_str(), // don't need redaction in config UI.
redacted_url: url.as_str(), // don't need redaction in config UI.
})?;
let extra_data = stream.get_extra_data()?;
Ok(format!("{}x{} video stream", extra_data.entry.width, extra_data.entry.height))
Ok(format!(
"{}x{} video stream",
extra_data.entry.width, extra_data.entry.height
))
}
fn press_test(siv: &mut Cursive, t: db::StreamType) {
@ -116,22 +157,28 @@ fn press_test(siv: &mut Cursive, t: db::StreamType) {
let mut url = match Url::parse(&c.streams[t.index()].rtsp_url) {
Ok(u) => u,
Err(e) => {
siv.add_layer(views::Dialog::text(
format!("Unparseable URL: {}", e))
siv.add_layer(
views::Dialog::text(format!("Unparseable URL: {}", e))
.title("Stream test failed")
.dismiss_button("Back"));
.dismiss_button("Back"),
);
return;
},
}
};
if !c.username.is_empty() {
let _ = url.set_username(&c.username);
let _ = url.set_password(Some(&c.password));
}
siv.add_layer(views::Dialog::text(format!("Testing {} stream at {}. This may take a while \
on timeout or if you have a long key frame interval",
t.as_str(), &url))
.title("Testing"));
siv.add_layer(
views::Dialog::text(format!(
"Testing {} stream at {}. This may take a while \
on timeout or if you have a long key frame interval",
t.as_str(),
&url
))
.title("Testing"),
);
// Let siv have this thread for its event loop; do the work in a background thread.
// siv.cb_sink doesn't actually wake up the event loop. Tell siv to poll, as a workaround.
@ -147,51 +194,75 @@ fn press_test(siv: &mut Cursive, t: db::StreamType) {
Err(ref e) => {
siv.add_layer(
views::Dialog::text(format!("{} stream at {}:\n\n{}", t.as_str(), &url, e))
.title("Stream test failed")
.dismiss_button("Back"));
.title("Stream test failed")
.dismiss_button("Back"),
);
return;
},
}
Ok(ref d) => d,
};
siv.add_layer(views::Dialog::text(
format!("{} stream at {}:\n\n{}", t.as_str(), &url, description))
.title("Stream test succeeded")
.dismiss_button("Back"));
})).unwrap();
siv.add_layer(
views::Dialog::text(format!(
"{} stream at {}:\n\n{}",
t.as_str(),
&url,
description
))
.title("Stream test succeeded")
.dismiss_button("Back"),
);
}))
.unwrap();
});
}
fn press_delete(siv: &mut Cursive, db: &Arc<db::Database>, id: i32, name: String, to_delete: i64) {
let dialog = if to_delete > 0 {
let prompt = format!("Camera {} has recorded video. Please confirm the amount \
of data to delete by typing it back:\n\n{}", name,
encode_size(to_delete));
let prompt = format!(
"Camera {} has recorded video. Please confirm the amount \
of data to delete by typing it back:\n\n{}",
name,
encode_size(to_delete)
);
views::Dialog::around(
views::LinearLayout::vertical()
.child(views::TextView::new(prompt))
.child(views::DummyView)
.child(views::EditView::new().on_submit({
let db = db.clone();
move |siv, _| confirm_deletion(siv, &db, id, to_delete)
}).with_name("confirm")))
.child(views::TextView::new(prompt))
.child(views::DummyView)
.child(
views::EditView::new()
.on_submit({
let db = db.clone();
move |siv, _| confirm_deletion(siv, &db, id, to_delete)
})
.with_name("confirm"),
),
)
.button("Delete", {
let db = db.clone();
move |siv| confirm_deletion(siv, &db, id, to_delete)
})
} else {
views::Dialog::text(format!("Delete camera {}? This camera has no recorded video.", name))
views::Dialog::text(format!(
"Delete camera {}? This camera has no recorded video.",
name
))
.button("Delete", {
let db = db.clone();
move |s| actually_delete(s, &db, id)
})
}.title("Delete camera").dismiss_button("Cancel");
}
.title("Delete camera")
.dismiss_button("Cancel");
siv.add_layer(dialog);
}
fn confirm_deletion(siv: &mut Cursive, db: &Arc<db::Database>, id: i32, to_delete: i64) {
let typed = siv.find_name::<views::EditView>("confirm").unwrap().get_content();
let typed = siv
.find_name::<views::EditView>("confirm")
.unwrap()
.get_content();
if decode_size(typed.as_str()).ok() == Some(to_delete) {
siv.pop_layer(); // deletion confirmation dialog
siv.pop_layer(); // deletion confirmation dialog
let mut zero_limits = BTreeMap::new();
{
@ -202,7 +273,9 @@ fn confirm_deletion(siv: &mut Cursive, db: &Arc<db::Database>, id: i32, to_delet
Some(d) => d,
None => continue,
};
let l = zero_limits.entry(dir_id).or_insert_with(|| Vec::with_capacity(2));
let l = zero_limits
.entry(dir_id)
.or_insert_with(|| Vec::with_capacity(2));
l.push(writer::NewLimit {
stream_id,
limit: 0,
@ -211,21 +284,27 @@ fn confirm_deletion(siv: &mut Cursive, db: &Arc<db::Database>, id: i32, to_delet
}
}
if let Err(e) = lower_retention(db, zero_limits) {
siv.add_layer(views::Dialog::text(format!("Unable to delete recordings: {}", e))
.title("Error")
.dismiss_button("Abort"));
siv.add_layer(
views::Dialog::text(format!("Unable to delete recordings: {}", e))
.title("Error")
.dismiss_button("Abort"),
);
return;
}
actually_delete(siv, db, id);
} else {
siv.add_layer(views::Dialog::text("Please confirm amount.")
.title("Try again")
.dismiss_button("Back"));
siv.add_layer(
views::Dialog::text("Please confirm amount.")
.title("Try again")
.dismiss_button("Back"),
);
}
}
fn lower_retention(db: &Arc<db::Database>, zero_limits: BTreeMap<i32, Vec<writer::NewLimit>>)
-> Result<(), Error> {
fn lower_retention(
db: &Arc<db::Database>,
zero_limits: BTreeMap<i32, Vec<writer::NewLimit>>,
) -> Result<(), Error> {
let dirs_to_open: Vec<_> = zero_limits.keys().map(|id| *id).collect();
db.lock().open_sample_file_dirs(&dirs_to_open[..])?;
for (&dir_id, l) in &zero_limits {
@ -235,15 +314,17 @@ fn lower_retention(db: &Arc<db::Database>, zero_limits: BTreeMap<i32, Vec<writer
}
fn actually_delete(siv: &mut Cursive, db: &Arc<db::Database>, id: i32) {
siv.pop_layer(); // get rid of the add/edit camera dialog.
siv.pop_layer(); // get rid of the add/edit camera dialog.
let result = {
let mut l = db.lock();
l.delete_camera(id)
};
if let Err(e) = result {
siv.add_layer(views::Dialog::text(format!("Unable to delete camera: {}", e))
.title("Error")
.dismiss_button("Abort"));
siv.add_layer(
views::Dialog::text(format!("Unable to delete camera: {}", e))
.title("Error")
.dismiss_button("Abort"),
);
} else {
// Recreate the "Edit cameras" dialog from scratch; it's easier than adding the new entry.
siv.pop_layer();
@ -255,10 +336,13 @@ fn actually_delete(siv: &mut Cursive, db: &Arc<db::Database>, id: i32) {
/// (The former if `item` is None; the latter otherwise.)
fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i32>) {
let camera_list = views::ListView::new()
.child("id", views::TextView::new(match *item {
None => "<new>".to_string(),
Some(id) => id.to_string(),
}))
.child(
"id",
views::TextView::new(match *item {
None => "<new>".to_string(),
Some(id) => id.to_string(),
}),
)
.child("uuid", views::TextView::new("<new>").with_name("uuid"))
.child("short name", views::EditView::new().with_name("short_name"))
.child("onvif_host", views::EditView::new().with_name("onvif_host"))
@ -268,32 +352,54 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
let mut layout = views::LinearLayout::vertical()
.child(camera_list)
.child(views::TextView::new("description"))
.child(views::TextArea::new().with_name("description").min_height(3));
.child(
views::TextArea::new()
.with_name("description")
.min_height(3),
);
let dirs: Vec<_> = ::std::iter::once(("<none>".to_owned(), None))
.chain(db.lock()
.sample_file_dirs_by_id()
.iter()
.map(|(&id, d)| (d.path.as_str().to_owned(), Some(id))))
.collect();
.chain(
db.lock()
.sample_file_dirs_by_id()
.iter()
.map(|(&id, d)| (d.path.as_str().to_owned(), Some(id))),
)
.collect();
for &type_ in &db::ALL_STREAM_TYPES {
let list = views::ListView::new()
.child("rtsp url", views::LinearLayout::horizontal()
.child(views::EditView::new()
.with_name(format!("{}_rtsp_url", type_.as_str()))
.full_width())
.child(views::DummyView)
.child(views::Button::new("Test", move |siv| press_test(siv, type_))))
.child("sample file dir",
views::SelectView::<Option<i32>>::new()
.with_all(dirs.iter().map(|d| d.clone()))
.popup()
.with_name(format!("{}_sample_file_dir", type_.as_str())))
.child("record", views::Checkbox::new().with_name(format!("{}_record", type_.as_str())))
.child("flush_if_sec", views::EditView::new()
.with_name(format!("{}_flush_if_sec", type_.as_str())))
.child("usage/capacity",
views::TextView::new("").with_name(format!("{}_usage_cap", type_.as_str())))
.child(
"rtsp url",
views::LinearLayout::horizontal()
.child(
views::EditView::new()
.with_name(format!("{}_rtsp_url", type_.as_str()))
.full_width(),
)
.child(views::DummyView)
.child(views::Button::new("Test", move |siv| {
press_test(siv, type_)
})),
)
.child(
"sample file dir",
views::SelectView::<Option<i32>>::new()
.with_all(dirs.iter().map(|d| d.clone()))
.popup()
.with_name(format!("{}_sample_file_dir", type_.as_str())),
)
.child(
"record",
views::Checkbox::new().with_name(format!("{}_record", type_.as_str())),
)
.child(
"flush_if_sec",
views::EditView::new().with_name(format!("{}_flush_if_sec", type_.as_str())),
)
.child(
"usage/capacity",
views::TextView::new("").with_name(format!("{}_usage_cap", type_.as_str())),
)
.min_height(5);
layout.add_child(views::DummyView);
layout.add_child(views::TextView::new(format!("{} stream", type_.as_str())));
@ -304,8 +410,11 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
let dialog = if let Some(camera_id) = *item {
let l = db.lock();
let camera = l.cameras_by_id().get(&camera_id).expect("missing camera");
dialog.call_on_name("uuid", |v: &mut views::TextView| v.set_content(camera.uuid.to_string()))
.expect("missing TextView");
dialog
.call_on_name("uuid", |v: &mut views::TextView| {
v.set_content(camera.uuid.to_string())
})
.expect("missing TextView");
let mut bytes = 0;
for (i, sid) in camera.streams.iter().enumerate() {
@ -326,70 +435,96 @@ fn edit_camera_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: &Option<i
let u = if s.retain_bytes == 0 {
"0 / 0 (0.0%)".to_owned()
} else {
format!("{} / {} ({:.1}%)", s.fs_bytes, s.retain_bytes,
100. * s.fs_bytes as f32 / s.retain_bytes as f32)
format!(
"{} / {} ({:.1}%)",
s.fs_bytes,
s.retain_bytes,
100. * s.fs_bytes as f32 / s.retain_bytes as f32
)
};
dialog.call_on_name(&format!("{}_rtsp_url", t.as_str()),
|v: &mut views::EditView| v.set_content(s.rtsp_url.to_owned()));
dialog.call_on_name(&format!("{}_usage_cap", t.as_str()),
|v: &mut views::TextView| v.set_content(u));
dialog.call_on_name(&format!("{}_record", t.as_str()),
|v: &mut views::Checkbox| v.set_checked(s.record));
dialog.call_on_name(
&format!("{}_rtsp_url", t.as_str()),
|v: &mut views::EditView| v.set_content(s.rtsp_url.to_owned()),
);
dialog.call_on_name(
&format!("{}_usage_cap", t.as_str()),
|v: &mut views::TextView| v.set_content(u),
);
dialog.call_on_name(
&format!("{}_record", t.as_str()),
|v: &mut views::Checkbox| v.set_checked(s.record),
);
dialog.call_on_name(
&format!("{}_flush_if_sec", t.as_str()),
|v: &mut views::EditView| v.set_content(s.flush_if_sec.to_string()));
|v: &mut views::EditView| v.set_content(s.flush_if_sec.to_string()),
);
}
dialog.call_on_name(
&format!("{}_sample_file_dir", t.as_str()),
|v: &mut views::SelectView<Option<i32>>| v.set_selection(selected_dir));
|v: &mut views::SelectView<Option<i32>>| v.set_selection(selected_dir),
);
}
let name = camera.short_name.clone();
for &(view_id, content) in &[("short_name", &*camera.short_name),
("onvif_host", &*camera.onvif_host),
("username", &*camera.username),
("password", &*camera.password)] {
dialog.call_on_name(view_id, |v: &mut views::EditView| v.set_content(content.to_string()))
.expect("missing EditView");
for &(view_id, content) in &[
("short_name", &*camera.short_name),
("onvif_host", &*camera.onvif_host),
("username", &*camera.username),
("password", &*camera.password),
] {
dialog
.call_on_name(view_id, |v: &mut views::EditView| {
v.set_content(content.to_string())
})
.expect("missing EditView");
}
dialog.call_on_name("description",
|v: &mut views::TextArea| v.set_content(camera.description.to_string()))
.expect("missing TextArea");
dialog.title("Edit camera")
.button("Edit", {
let db = db.clone();
move |s| press_edit(s, &db, Some(camera_id))
})
.button("Delete", {
let db = db.clone();
move |s| press_delete(s, &db, camera_id, name.clone(), bytes)
})
dialog
.call_on_name("description", |v: &mut views::TextArea| {
v.set_content(camera.description.to_string())
})
.expect("missing TextArea");
dialog
.title("Edit camera")
.button("Edit", {
let db = db.clone();
move |s| press_edit(s, &db, Some(camera_id))
})
.button("Delete", {
let db = db.clone();
move |s| press_delete(s, &db, camera_id, name.clone(), bytes)
})
} else {
for t in &db::ALL_STREAM_TYPES {
dialog.call_on_name(&format!("{}_usage_cap", t.as_str()),
|v: &mut views::TextView| v.set_content("<new>"));
dialog.call_on_name(
&format!("{}_usage_cap", t.as_str()),
|v: &mut views::TextView| v.set_content("<new>"),
);
}
dialog.title("Add camera")
.button("Add", {
let db = db.clone();
move |s| press_edit(s, &db, None)
})
dialog.title("Add camera").button("Add", {
let db = db.clone();
move |s| press_edit(s, &db, None)
})
};
siv.add_layer(dialog.dismiss_button("Cancel"));
}
pub fn top_dialog(db: &Arc<db::Database>, siv: &mut Cursive) {
siv.add_layer(views::Dialog::around(
views::SelectView::new()
.on_submit({
let db = db.clone();
move |siv, item| edit_camera_dialog(&db, siv, item)
})
.item("<new camera>".to_string(), None)
.with_all(db.lock()
siv.add_layer(
views::Dialog::around(
views::SelectView::new()
.on_submit({
let db = db.clone();
move |siv, item| edit_camera_dialog(&db, siv, item)
})
.item("<new camera>".to_string(), None)
.with_all(
db.lock()
.cameras_by_id()
.iter()
.map(|(&id, camera)| (format!("{}: {}", id, camera.short_name), Some(id))))
.full_width())
.map(|(&id, camera)| (format!("{}: {}", id, camera.short_name), Some(id))),
)
.full_width(),
)
.dismiss_button("Done")
.title("Edit cameras"));
.title("Edit cameras"),
);
}

View File

@ -29,9 +29,9 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use base::strutil::{decode_size, encode_size};
use cursive::Cursive;
use cursive::traits::{Boxable, Identifiable};
use cursive::views;
use cursive::Cursive;
use db::writer;
use failure::Error;
use log::{debug, trace};
@ -44,7 +44,7 @@ struct Stream {
label: String,
used: i64,
record: bool,
retain: Option<i64>, // None if unparseable
retain: Option<i64>, // None if unparseable
}
struct Model {
@ -72,9 +72,11 @@ fn update_limits_inner(model: &Model) -> Result<(), Error> {
fn update_limits(model: &Model, siv: &mut Cursive) {
if let Err(e) = update_limits_inner(model) {
siv.add_layer(views::Dialog::text(format!("Unable to update limits: {}", e))
.dismiss_button("Back")
.title("Error"));
siv.add_layer(
views::Dialog::text(format!("Unable to update limits: {}", e))
.dismiss_button("Back")
.title("Error"),
);
}
}
@ -111,8 +113,8 @@ fn edit_limit(model: &RefCell<Model>, siv: &mut Cursive, id: i32, content: &str)
if (model.errors == 0) != (old_errors == 0) {
trace!("toggling change state: errors={}", model.errors);
siv.find_name::<views::Button>("change")
.unwrap()
.set_enabled(model.errors == 0);
.unwrap()
.set_enabled(model.errors == 0);
}
}
@ -124,35 +126,48 @@ fn edit_record(model: &RefCell<Model>, id: i32, record: bool) {
}
fn confirm_deletion(model: &RefCell<Model>, siv: &mut Cursive, to_delete: i64) {
let typed = siv.find_name::<views::EditView>("confirm")
.unwrap()
.get_content();
debug!("confirm, typed: {} vs expected: {}", typed.as_str(), to_delete);
let typed = siv
.find_name::<views::EditView>("confirm")
.unwrap()
.get_content();
debug!(
"confirm, typed: {} vs expected: {}",
typed.as_str(),
to_delete
);
if decode_size(typed.as_str()).ok() == Some(to_delete) {
actually_delete(model, siv);
} else {
siv.add_layer(views::Dialog::text("Please confirm amount.")
.title("Try again")
.dismiss_button("Back"));
siv.add_layer(
views::Dialog::text("Please confirm amount.")
.title("Try again")
.dismiss_button("Back"),
);
}
}
fn actually_delete(model: &RefCell<Model>, siv: &mut Cursive) {
let model = &*model.borrow();
let new_limits: Vec<_> =
model.streams.iter()
.map(|(&id, s)| writer::NewLimit {stream_id: id, limit: s.retain.unwrap()})
.collect();
siv.pop_layer(); // deletion confirmation
siv.pop_layer(); // retention dialog
let new_limits: Vec<_> = model
.streams
.iter()
.map(|(&id, s)| writer::NewLimit {
stream_id: id,
limit: s.retain.unwrap(),
})
.collect();
siv.pop_layer(); // deletion confirmation
siv.pop_layer(); // retention dialog
{
let mut l = model.db.lock();
l.open_sample_file_dirs(&[model.dir_id]).unwrap(); // TODO: don't unwrap.
l.open_sample_file_dirs(&[model.dir_id]).unwrap(); // TODO: don't unwrap.
}
if let Err(e) = writer::lower_retention(model.db.clone(), model.dir_id, &new_limits[..]) {
siv.add_layer(views::Dialog::text(format!("Unable to delete excess video: {}", e))
.title("Error")
.dismiss_button("Abort"));
siv.add_layer(
views::Dialog::text(format!("Unable to delete excess video: {}", e))
.title("Error")
.dismiss_button("Abort"),
);
} else {
update_limits(model, siv);
}
@ -162,26 +177,38 @@ fn press_change(model: &Rc<RefCell<Model>>, siv: &mut Cursive) {
if model.borrow().errors > 0 {
return;
}
let to_delete = model.borrow().streams.values().map(
|s| ::std::cmp::max(s.used - s.retain.unwrap(), 0)).sum();
let to_delete = model
.borrow()
.streams
.values()
.map(|s| ::std::cmp::max(s.used - s.retain.unwrap(), 0))
.sum();
debug!("change press, to_delete={}", to_delete);
if to_delete > 0 {
let prompt = format!("Some streams' usage exceeds new limit. Please confirm the amount \
of data to delete by typing it back:\n\n{}", encode_size(to_delete));
let prompt = format!(
"Some streams' usage exceeds new limit. Please confirm the amount \
of data to delete by typing it back:\n\n{}",
encode_size(to_delete)
);
let dialog = views::Dialog::around(
views::LinearLayout::vertical()
views::LinearLayout::vertical()
.child(views::TextView::new(prompt))
.child(views::DummyView)
.child(views::EditView::new().on_submit({
let model = model.clone();
move |siv, _| confirm_deletion(&model, siv, to_delete)
}).with_name("confirm")))
.button("Confirm", {
let model = model.clone();
move |siv| confirm_deletion(&model, siv, to_delete)
})
.dismiss_button("Cancel")
.title("Confirm deletion");
.child(
views::EditView::new()
.on_submit({
let model = model.clone();
move |siv, _| confirm_deletion(&model, siv, to_delete)
})
.with_name("confirm"),
),
)
.button("Confirm", {
let model = model.clone();
move |siv| confirm_deletion(&model, siv, to_delete)
})
.dismiss_button("Cancel")
.title("Confirm deletion");
siv.add_layer(dialog);
} else {
siv.pop_layer();
@ -190,23 +217,28 @@ fn press_change(model: &Rc<RefCell<Model>>, siv: &mut Cursive) {
}
pub fn top_dialog(db: &Arc<db::Database>, siv: &mut Cursive) {
siv.add_layer(views::Dialog::around(
views::SelectView::new()
.on_submit({
let db = db.clone();
move |siv, item| match *item {
Some(d) => edit_dir_dialog(&db, siv, d),
None => add_dir_dialog(&db, siv),
}
})
.item("<new sample file dir>".to_string(), None)
.with_all(db.lock()
siv.add_layer(
views::Dialog::around(
views::SelectView::new()
.on_submit({
let db = db.clone();
move |siv, item| match *item {
Some(d) => edit_dir_dialog(&db, siv, d),
None => add_dir_dialog(&db, siv),
}
})
.item("<new sample file dir>".to_string(), None)
.with_all(
db.lock()
.sample_file_dirs_by_id()
.iter()
.map(|(&id, d)| (d.path.to_string(), Some(id))))
.full_width())
.map(|(&id, d)| (d.path.to_string(), Some(id))),
)
.full_width(),
)
.dismiss_button("Done")
.title("Edit sample file directories"));
.title("Edit sample file directories"),
);
}
fn add_dir_dialog(db: &Arc<db::Database>, siv: &mut Cursive) {
@ -214,29 +246,40 @@ fn add_dir_dialog(db: &Arc<db::Database>, siv: &mut Cursive) {
views::Dialog::around(
views::LinearLayout::vertical()
.child(views::TextView::new("path"))
.child(views::EditView::new()
.on_submit({
let db = db.clone();
move |siv, path| add_dir(&db, siv, path)
})
.with_name("path")
.fixed_width(60)))
.button("Add", {
let db = db.clone();
move |siv| {
let path = siv.find_name::<views::EditView>("path").unwrap().get_content();
add_dir(&db, siv, &path)
}
})
.button("Cancel", |siv| { siv.pop_layer(); })
.title("Add sample file directory"));
.child(
views::EditView::new()
.on_submit({
let db = db.clone();
move |siv, path| add_dir(&db, siv, path)
})
.with_name("path")
.fixed_width(60),
),
)
.button("Add", {
let db = db.clone();
move |siv| {
let path = siv
.find_name::<views::EditView>("path")
.unwrap()
.get_content();
add_dir(&db, siv, &path)
}
})
.button("Cancel", |siv| {
siv.pop_layer();
})
.title("Add sample file directory"),
);
}
fn add_dir(db: &Arc<db::Database>, siv: &mut Cursive, path: &str) {
if let Err(e) = db.lock().add_sample_file_dir(path.to_owned()) {
siv.add_layer(views::Dialog::text(format!("Unable to add path {}: {}", path, e))
.dismiss_button("Back")
.title("Error"));
siv.add_layer(
views::Dialog::text(format!("Unable to add path {}: {}", path, e))
.dismiss_button("Back")
.title("Error"),
);
return;
}
siv.pop_layer();
@ -248,23 +291,25 @@ fn add_dir(db: &Arc<db::Database>, siv: &mut Cursive, path: &str) {
fn delete_dir_dialog(db: &Arc<db::Database>, siv: &mut Cursive, dir_id: i32) {
siv.add_layer(
views::Dialog::around(
views::TextView::new("Empty (no associated streams)."))
views::Dialog::around(views::TextView::new("Empty (no associated streams)."))
.button("Delete", {
let db = db.clone();
move |siv| {
delete_dir(&db, siv, dir_id)
}
move |siv| delete_dir(&db, siv, dir_id)
})
.button("Cancel", |siv| { siv.pop_layer(); })
.title("Delete sample file directory"));
.button("Cancel", |siv| {
siv.pop_layer();
})
.title("Delete sample file directory"),
);
}
fn delete_dir(db: &Arc<db::Database>, siv: &mut Cursive, dir_id: i32) {
if let Err(e) = db.lock().delete_sample_file_dir(dir_id) {
siv.add_layer(views::Dialog::text(format!("Unable to delete dir id {}: {}", dir_id, e))
.dismiss_button("Back")
.title("Error"));
siv.add_layer(
views::Dialog::text(format!("Unable to delete dir id {}: {}", dir_id, e))
.dismiss_button("Back")
.title("Error"),
);
return;
}
siv.pop_layer();
@ -284,23 +329,29 @@ fn edit_dir_dialog(db: &Arc<db::Database>, siv: &mut Cursive, dir_id: i32) {
{
let mut l = db.lock();
for (&id, s) in l.streams_by_id() {
let c = l.cameras_by_id().get(&s.camera_id).expect("stream without camera");
let c = l
.cameras_by_id()
.get(&s.camera_id)
.expect("stream without camera");
if s.sample_file_dir_id != Some(dir_id) {
continue;
}
streams.insert(id, Stream {
label: format!("{}: {}: {}", id, c.short_name, s.type_.as_str()),
used: s.fs_bytes,
record: s.record,
retain: Some(s.retain_bytes),
});
streams.insert(
id,
Stream {
label: format!("{}: {}: {}", id, c.short_name, s.type_.as_str()),
used: s.fs_bytes,
record: s.record,
retain: Some(s.retain_bytes),
},
);
total_used += s.fs_bytes;
total_retain += s.retain_bytes;
}
if streams.is_empty() {
return delete_dir_dialog(db, siv, dir_id);
}
l.open_sample_file_dirs(&[dir_id]).unwrap(); // TODO: don't unwrap.
l.open_sample_file_dirs(&[dir_id]).unwrap(); // TODO: don't unwrap.
let dir = l.sample_file_dirs_by_id().get(&dir_id).unwrap();
let stat = dir.get().unwrap().statfs().unwrap();
fs_capacity = stat.block_size() as i64 * stat.blocks_available() as i64 + total_used;
@ -326,7 +377,8 @@ fn edit_dir_dialog(db: &Arc<db::Database>, siv: &mut Cursive, dir_id: i32) {
views::LinearLayout::horizontal()
.child(views::TextView::new("record").fixed_width(RECORD_WIDTH))
.child(views::TextView::new("usage").fixed_width(BYTES_WIDTH))
.child(views::TextView::new("limit").fixed_width(BYTES_WIDTH)));
.child(views::TextView::new("limit").fixed_width(BYTES_WIDTH)),
);
for (&id, stream) in &model.borrow().streams {
let mut record_cb = views::Checkbox::new();
record_cb.set_checked(stream.record);
@ -339,50 +391,67 @@ fn edit_dir_dialog(db: &Arc<db::Database>, siv: &mut Cursive, dir_id: i32) {
views::LinearLayout::horizontal()
.child(record_cb.fixed_width(RECORD_WIDTH))
.child(views::TextView::new(encode_size(stream.used)).fixed_width(BYTES_WIDTH))
.child(views::EditView::new()
.content(encode_size(stream.retain.unwrap()))
.on_edit({
let model = model.clone();
move |siv, content, _pos| edit_limit(&model, siv, id, content)
})
.on_submit({
let model = model.clone();
move |siv, _| press_change(&model, siv)
})
.fixed_width(20))
.child(views::TextView::new("").with_name(format!("{}_ok", id)).fixed_width(1)));
.child(
views::EditView::new()
.content(encode_size(stream.retain.unwrap()))
.on_edit({
let model = model.clone();
move |siv, content, _pos| edit_limit(&model, siv, id, content)
})
.on_submit({
let model = model.clone();
move |siv, _| press_change(&model, siv)
})
.fixed_width(20),
)
.child(
views::TextView::new("")
.with_name(format!("{}_ok", id))
.fixed_width(1),
),
);
}
let over = model.borrow().total_retain > model.borrow().fs_capacity;
list.add_child(
"total",
views::LinearLayout::horizontal()
.child(views::DummyView{}.fixed_width(RECORD_WIDTH))
.child(views::TextView::new(encode_size(model.borrow().total_used))
.fixed_width(BYTES_WIDTH))
.child(views::TextView::new(encode_size(model.borrow().total_retain))
.with_name("total_retain").fixed_width(BYTES_WIDTH))
.child(views::TextView::new(if over { "*" } else { " " }).with_name("total_ok")));
.child(views::DummyView {}.fixed_width(RECORD_WIDTH))
.child(
views::TextView::new(encode_size(model.borrow().total_used))
.fixed_width(BYTES_WIDTH),
)
.child(
views::TextView::new(encode_size(model.borrow().total_retain))
.with_name("total_retain")
.fixed_width(BYTES_WIDTH),
)
.child(views::TextView::new(if over { "*" } else { " " }).with_name("total_ok")),
);
list.add_child(
"filesystem",
views::LinearLayout::horizontal()
.child(views::DummyView{}.fixed_width(3))
.child(views::DummyView{}.fixed_width(20))
.child(views::TextView::new(encode_size(model.borrow().fs_capacity)).fixed_width(25)));
.child(views::DummyView {}.fixed_width(3))
.child(views::DummyView {}.fixed_width(20))
.child(views::TextView::new(encode_size(model.borrow().fs_capacity)).fixed_width(25)),
);
let mut change_button = views::Button::new("Change", {
let model = model.clone();
move |siv| press_change(&model, siv)
});
change_button.set_enabled(!over);
let mut buttons = views::LinearLayout::horizontal()
.child(views::DummyView.full_width());
let mut buttons = views::LinearLayout::horizontal().child(views::DummyView.full_width());
buttons.add_child(change_button.with_name("change"));
buttons.add_child(views::DummyView);
buttons.add_child(views::Button::new("Cancel", |siv| { siv.pop_layer(); }));
buttons.add_child(views::Button::new("Cancel", |siv| {
siv.pop_layer();
}));
siv.add_layer(
views::Dialog::around(
views::LinearLayout::vertical()
.child(list)
.child(views::DummyView)
.child(buttons))
.title(format!("Edit retention for {}", path)));
.child(buttons),
)
.title(format!("Edit retention for {}", path)),
);
}

View File

@ -34,8 +34,8 @@
//! configuration will likely be almost entirely done through a web-based UI.
use base::clock;
use cursive::Cursive;
use cursive::views;
use cursive::Cursive;
use db;
use failure::Error;
use std::path::PathBuf;
@ -49,8 +49,12 @@ mod users;
#[derive(StructOpt)]
pub struct Args {
/// Directory holding the SQLite3 index database.
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
#[structopt(
long,
default_value = "/var/lib/moonfire-nvr/db",
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf,
}
@ -62,18 +66,20 @@ pub fn run(args: &Args) -> Result<i32, Error> {
let mut siv = cursive::default();
//siv.add_global_callback('q', |s| s.quit());
siv.add_layer(views::Dialog::around(
views::SelectView::<fn(&Arc<db::Database>, &mut Cursive)>::new()
.on_submit({
let db = db.clone();
move |siv, item| item(&db, siv)
})
.item("Cameras and streams".to_string(), cameras::top_dialog)
.item("Directories and retention".to_string(), dirs::top_dialog)
.item("Users".to_string(), users::top_dialog)
)
siv.add_layer(
views::Dialog::around(
views::SelectView::<fn(&Arc<db::Database>, &mut Cursive)>::new()
.on_submit({
let db = db.clone();
move |siv, item| item(&db, siv)
})
.item("Cameras and streams".to_string(), cameras::top_dialog)
.item("Directories and retention".to_string(), dirs::top_dialog)
.item("Users".to_string(), users::top_dialog),
)
.button("Quit", |siv| siv.quit())
.title("Main menu"));
.title("Main menu"),
);
siv.run();

View File

@ -28,33 +28,51 @@
// 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 cursive::Cursive;
use cursive::traits::{Boxable, Identifiable};
use cursive::views;
use cursive::Cursive;
use log::info;
use std::sync::Arc;
/// Builds a `UserChange` from an active `edit_user_dialog`.
fn get_change(siv: &mut Cursive, db: &db::LockedDatabase, id: Option<i32>,
pw: PasswordChange) -> db::UserChange {
fn get_change(
siv: &mut Cursive,
db: &db::LockedDatabase,
id: Option<i32>,
pw: PasswordChange,
) -> db::UserChange {
let mut change = match id {
Some(id) => db.users_by_id().get(&id).unwrap().change(),
None => db::UserChange::add_user(String::new()),
};
change.username.clear();
change.username += siv.find_name::<views::EditView>("username").unwrap().get_content().as_str();
change.username += siv
.find_name::<views::EditView>("username")
.unwrap()
.get_content()
.as_str();
match pw {
PasswordChange::Leave => {},
PasswordChange::Leave => {}
PasswordChange::Set => {
let pwd = siv.find_name::<views::EditView>("new_pw").unwrap().get_content();
let pwd = siv
.find_name::<views::EditView>("new_pw")
.unwrap()
.get_content();
change.set_password(pwd.as_str().into());
},
}
PasswordChange::Clear => change.clear_password(),
};
for (id, ref mut b) in &mut [
("perm_view_video", &mut change.permissions.view_video),
("perm_read_camera_configs", &mut change.permissions.read_camera_configs),
("perm_update_signals", &mut change.permissions.update_signals)] {
(
"perm_read_camera_configs",
&mut change.permissions.read_camera_configs,
),
(
"perm_update_signals",
&mut change.permissions.update_signals,
),
] {
**b = siv.find_name::<views::Checkbox>(id).unwrap().is_checked();
info!("{}: {}", id, **b);
}
@ -68,11 +86,13 @@ fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>, pw: Pa
l.apply_user_change(c).map(|_| ())
};
if let Err(e) = result {
siv.add_layer(views::Dialog::text(format!("Unable to apply change: {}", e))
.title("Error")
.dismiss_button("Abort"));
siv.add_layer(
views::Dialog::text(format!("Unable to apply change: {}", e))
.title("Error")
.dismiss_button("Abort"),
);
} else {
siv.pop_layer(); // get rid of the add/edit user dialog.
siv.pop_layer(); // get rid of the add/edit user dialog.
// Recreate the "Edit users" dialog from scratch; it's easier than adding the new entry.
siv.pop_layer();
@ -81,24 +101,29 @@ fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, id: Option<i32>, pw: Pa
}
fn press_delete(siv: &mut Cursive, db: &Arc<db::Database>, id: i32, name: String) {
siv.add_layer(views::Dialog::text(format!("Delete user {}?", name))
.button("Delete", {
let db = db.clone();
move |s| actually_delete(s, &db, id)
})
.title("Delete user").dismiss_button("Cancel"));
siv.add_layer(
views::Dialog::text(format!("Delete user {}?", name))
.button("Delete", {
let db = db.clone();
move |s| actually_delete(s, &db, id)
})
.title("Delete user")
.dismiss_button("Cancel"),
);
}
fn actually_delete(siv: &mut Cursive, db: &Arc<db::Database>, id: i32) {
siv.pop_layer(); // get rid of the add/edit user dialog.
siv.pop_layer(); // get rid of the add/edit user dialog.
let result = {
let mut l = db.lock();
l.delete_user(id)
};
if let Err(e) = result {
siv.add_layer(views::Dialog::text(format!("Unable to delete user: {}", e))
.title("Error")
.dismiss_button("Abort"));
siv.add_layer(
views::Dialog::text(format!("Unable to delete user: {}", e))
.title("Error")
.dismiss_button("Abort"),
);
} else {
// Recreate the "Edit users" dialog from scratch; it's easier than adding the new entry.
siv.pop_layer();
@ -114,7 +139,9 @@ enum PasswordChange {
}
fn select_set(siv: &mut Cursive) {
siv.find_name::<views::RadioButton<PasswordChange>>("pw_set").unwrap().select();
siv.find_name::<views::RadioButton<PasswordChange>>("pw_set")
.unwrap()
.select();
}
/// Adds or updates a user.
@ -128,13 +155,18 @@ fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>
username = u.map(|u| u.username.clone()).unwrap_or(String::new());
id_str = item.map(|id| id.to_string()).unwrap_or("<new>".to_string());
has_password = u.map(|u| u.has_password()).unwrap_or(false);
permissions = u.map(|u| u.permissions.clone()).unwrap_or(db::Permissions::default());
permissions = u
.map(|u| u.permissions.clone())
.unwrap_or(db::Permissions::default());
}
let top_list = views::ListView::new()
.child("id", views::TextView::new(id_str))
.child("username", views::EditView::new()
.content(username.clone())
.with_name("username"));
.child(
"username",
views::EditView::new()
.content(username.clone())
.with_name("username"),
);
let mut layout = views::LinearLayout::vertical()
.child(top_list)
.child(views::DummyView)
@ -143,32 +175,48 @@ fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>
if has_password {
layout.add_child(pw_group.button(PasswordChange::Leave, "Leave set"));
layout.add_child(pw_group.button(PasswordChange::Clear, "Clear"));
layout.add_child(views::LinearLayout::horizontal()
.child(pw_group.button(PasswordChange::Set, "Set to:")
.with_name("pw_set"))
.child(views::DummyView)
.child(views::EditView::new()
.on_edit(|siv, _, _| select_set(siv))
.with_name("new_pw")
.full_width()));
layout.add_child(
views::LinearLayout::horizontal()
.child(
pw_group
.button(PasswordChange::Set, "Set to:")
.with_name("pw_set"),
)
.child(views::DummyView)
.child(
views::EditView::new()
.on_edit(|siv, _, _| select_set(siv))
.with_name("new_pw")
.full_width(),
),
);
} else {
layout.add_child(pw_group.button(PasswordChange::Leave, "Leave unset"));
layout.add_child(views::LinearLayout::horizontal()
.child(pw_group.button(PasswordChange::Set, "Reset to:")
.with_name("pw_set"))
.child(views::DummyView)
.child(views::EditView::new()
.on_edit(|siv, _, _| select_set(siv))
.with_name("new_pw")
.full_width()));
layout.add_child(
views::LinearLayout::horizontal()
.child(
pw_group
.button(PasswordChange::Set, "Reset to:")
.with_name("pw_set"),
)
.child(views::DummyView)
.child(
views::EditView::new()
.on_edit(|siv, _, _| select_set(siv))
.with_name("new_pw")
.full_width(),
),
);
}
layout.add_child(views::DummyView);
layout.add_child(views::TextView::new("permissions"));
let mut perms = views::ListView::new();
for (name, b) in &[("view_video", permissions.view_video),
("read_camera_configs", permissions.read_camera_configs),
("update_signals", permissions.update_signals)] {
for (name, b) in &[
("view_video", permissions.view_video),
("read_camera_configs", permissions.read_camera_configs),
("update_signals", permissions.update_signals),
] {
let mut checkbox = views::Checkbox::new();
checkbox.set_checked(*b);
perms.add_child(name, checkbox.with_name(format!("perm_{}", name)));
@ -177,38 +225,43 @@ fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>
let dialog = views::Dialog::around(layout);
let dialog = if let Some(id) = item {
dialog.title("Edit user")
.button("Edit", {
let db = db.clone();
move |s| press_edit(s, &db, item, *pw_group.selection())
})
.button("Delete", {
let db = db.clone();
move |s| press_delete(s, &db, id, username.clone())
})
dialog
.title("Edit user")
.button("Edit", {
let db = db.clone();
move |s| press_edit(s, &db, item, *pw_group.selection())
})
.button("Delete", {
let db = db.clone();
move |s| press_delete(s, &db, id, username.clone())
})
} else {
dialog.title("Add user")
.button("Add", {
let db = db.clone();
move |s| press_edit(s, &db, item, *pw_group.selection())
})
dialog.title("Add user").button("Add", {
let db = db.clone();
move |s| press_edit(s, &db, item, *pw_group.selection())
})
};
siv.add_layer(dialog.dismiss_button("Cancel"));
}
pub fn top_dialog(db: &Arc<db::Database>, siv: &mut Cursive) {
siv.add_layer(views::Dialog::around(
views::SelectView::new()
.on_submit({
let db = db.clone();
move |siv, &item| edit_user_dialog(&db, siv, item)
})
.item("<new user>".to_string(), None)
.with_all(db.lock()
siv.add_layer(
views::Dialog::around(
views::SelectView::new()
.on_submit({
let db = db.clone();
move |siv, &item| edit_user_dialog(&db, siv, item)
})
.item("<new user>".to_string(), None)
.with_all(
db.lock()
.users_by_id()
.iter()
.map(|(&id, user)| (format!("{}: {}", id, user.username), Some(id))))
.full_width())
.map(|(&id, user)| (format!("{}: {}", id, user.username), Some(id))),
)
.full_width(),
)
.dismiss_button("Done")
.title("Edit users"));
.title("Edit users"),
);
}

View File

@ -30,14 +30,18 @@
use failure::Error;
use log::info;
use structopt::StructOpt;
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(StructOpt)]
pub struct Args {
/// Directory holding the SQLite3 index database.
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
#[structopt(
long,
default_value = "/var/lib/moonfire-nvr/db",
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf,
}
@ -55,12 +59,14 @@ pub fn run(args: &Args) -> Result<i32, Error> {
// page size (so reading large recording_playback rows doesn't require as many seeks). Changing
// the page size requires doing a vacuum in non-WAL mode. This will be cheap on an empty
// database. https://www.sqlite.org/pragma.html#pragma_page_size
conn.execute_batch(r#"
conn.execute_batch(
r#"
pragma journal_mode = delete;
pragma page_size = 16384;
vacuum;
pragma journal_mode = wal;
"#)?;
"#,
)?;
db::init(&mut conn)?;
info!("Database initialized.");
Ok(0)

View File

@ -32,17 +32,21 @@
use base::clock::{self, Clocks};
use db::auth::SessionFlag;
use failure::{Error, format_err};
use std::os::unix::fs::OpenOptionsExt as _;
use failure::{format_err, Error};
use std::io::Write as _;
use std::os::unix::fs::OpenOptionsExt as _;
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, Default, StructOpt)]
pub struct Args {
/// Directory holding the SQLite3 index database.
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
#[structopt(
long,
default_value = "/var/lib/moonfire-nvr/db",
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf,
/// Create a session with the given permissions.
@ -59,12 +63,16 @@ pub struct Args {
/// Write the cookie to a new curl-compatible cookie-jar file.
///
/// ---domain must be specified. This file can be used later with curl's --cookie flag.
#[structopt(long, requires("domain"), value_name="path")]
#[structopt(long, requires("domain"), value_name = "path")]
curl_cookie_jar: Option<PathBuf>,
/// Set the given db::auth::SessionFlags.
#[structopt(long, default_value="http-only,secure,same-site,same-site-strict",
value_name="flags", use_delimiter=true)]
#[structopt(
long,
default_value = "http-only,secure,same-site,same-site-strict",
value_name = "flags",
use_delimiter = true
)]
session_flags: Vec<SessionFlag>,
/// Create the session for this username.
@ -76,7 +84,8 @@ pub fn run(args: &Args) -> Result<i32, Error> {
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
let db = std::sync::Arc::new(db::Database::new(clocks.clone(), conn, true).unwrap());
let mut l = db.lock();
let u = l.get_user(&args.username)
let u = l
.get_user(&args.username)
.ok_or_else(|| format_err!("no such user {:?}", &args.username))?;
let permissions = args.permissions.as_ref().unwrap_or(&u.permissions).clone();
let creation = db::auth::Request {
@ -90,27 +99,36 @@ pub fn run(args: &Args) -> Result<i32, Error> {
}
let uid = u.id;
drop(u);
let (sid, _) = l.make_session(creation, uid,
args.domain.as_ref().map(|d| d.as_bytes().to_owned()),
flags, permissions)?;
let (sid, _) = l.make_session(
creation,
uid,
args.domain.as_ref().map(|d| d.as_bytes().to_owned()),
flags,
permissions,
)?;
let mut encoded = [0u8; 64];
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8");
if let Some(ref p) = args.curl_cookie_jar {
let d = args.domain.as_ref()
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?;
let d = args
.domain
.as_ref()
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?;
let mut f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(p)
.map_err(|e| format_err!("Unable to open {}: {}", p.display(), e))?;
write!(&mut f,
"# Netscape HTTP Cookie File\n\
# https://curl.haxx.se/docs/http-cookies.html\n\
# This file was generated by moonfire-nvr login! Edit at your own risk.\n\n\
{}\n", curl_cookie(encoded, flags, d))?;
write!(
&mut f,
"# Netscape HTTP Cookie File\n\
# https://curl.haxx.se/docs/http-cookies.html\n\
# This file was generated by moonfire-nvr login! Edit at your own risk.\n\n\
{}\n",
curl_cookie(encoded, flags, d)
)?;
f.sync_all()?;
println!("Wrote cookie to {}", p.display());
} else {
@ -120,15 +138,25 @@ pub fn run(args: &Args) -> Result<i32, Error> {
}
fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
format!("{httponly}{domain}\t{tailmatch}\t{path}\t{secure}\t{expires}\t{name}\t{value}",
httponly=if (flags & SessionFlag::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" },
domain=domain,
tailmatch="FALSE",
path="/",
secure=if (flags & SessionFlag::Secure as i32) != 0 { "TRUE" } else { "FALSE" },
expires="9223372036854775807", // 64-bit CURL_OFF_T_MAX, never expires
name="s",
value=cookie)
format!(
"{httponly}{domain}\t{tailmatch}\t{path}\t{secure}\t{expires}\t{name}\t{value}",
httponly = if (flags & SessionFlag::HttpOnly as i32) != 0 {
"#HttpOnly_"
} else {
""
},
domain = domain,
tailmatch = "FALSE",
path = "/",
secure = if (flags & SessionFlag::Secure as i32) != 0 {
"TRUE"
} else {
"FALSE"
},
expires = "9223372036854775807", // 64-bit CURL_OFF_T_MAX, never expires
name = "s",
value = cookie
)
}
#[cfg(test)]
@ -137,9 +165,14 @@ mod tests {
#[test]
fn test_curl_cookie() {
assert_eq!(curl_cookie("o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q",
SessionFlag::HttpOnly as i32, "localhost"),
"#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\
o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q");
assert_eq!(
curl_cookie(
"o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q",
SessionFlag::HttpOnly as i32,
"localhost"
),
"#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\
o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q"
);
}
}

View File

@ -37,8 +37,8 @@ use std::path::Path;
pub mod check;
pub mod config;
pub mod login;
pub mod init;
pub mod login;
pub mod run;
pub mod sql;
pub mod ts;
@ -48,23 +48,35 @@ pub mod upgrade;
enum OpenMode {
ReadOnly,
ReadWrite,
Create
Create,
}
/// Locks the directory without opening the database.
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is.
fn open_dir(db_dir: &Path, mode: OpenMode) -> Result<dir::Fd, Error> {
let dir = dir::Fd::open(db_dir, mode == OpenMode::Create)
.map_err(|e| e.context(if e == nix::Error::Sys(nix::errno::Errno::ENOENT) {
format!("db dir {} not found; try running moonfire-nvr init",
db_dir.display())
} else {
format!("unable to open db dir {}", db_dir.display())
}))?;
let dir = dir::Fd::open(db_dir, mode == OpenMode::Create).map_err(|e| {
e.context(if e == nix::Error::Sys(nix::errno::Errno::ENOENT) {
format!(
"db dir {} not found; try running moonfire-nvr init",
db_dir.display()
)
} else {
format!("unable to open db dir {}", db_dir.display())
})
})?;
let ro = mode == OpenMode::ReadOnly;
dir.lock(if ro { FlockArg::LockSharedNonblock } else { FlockArg::LockExclusiveNonblock })
.map_err(|e| e.context(format!("unable to get {} lock on db dir {} ",
if ro { "shared" } else { "exclusive" }, db_dir.display())))?;
dir.lock(if ro {
FlockArg::LockSharedNonblock
} else {
FlockArg::LockExclusiveNonblock
})
.map_err(|e| {
e.context(format!(
"unable to get {} lock on db dir {} ",
if ro { "shared" } else { "exclusive" },
db_dir.display()
))
})?;
Ok(dir)
}
@ -73,8 +85,12 @@ fn open_dir(db_dir: &Path, mode: OpenMode) -> Result<dir::Fd, Error> {
fn open_conn(db_dir: &Path, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> {
let dir = open_dir(db_dir, mode)?;
let db_path = db_dir.join("db");
info!("Opening {} in {:?} mode with SQLite version {}",
db_path.display(), mode, rusqlite::version());
info!(
"Opening {} in {:?} mode with SQLite version {}",
db_path.display(),
mode,
rusqlite::version()
);
let conn = rusqlite::Connection::open_with_flags(
db_path,
match mode {
@ -86,6 +102,7 @@ fn open_conn(db_dir: &Path, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connec
} |
// rusqlite::Connection is not Sync, so there's no reason to tell SQLite3 to use the
// serialized threading mode.
rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX)?;
rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)?;
Ok((dir, conn))
}

View File

@ -28,34 +28,42 @@
// 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 base::clock;
use crate::stream;
use crate::streamer;
use crate::web;
use base::clock;
use db::{dir, writer};
use failure::{Error, bail};
use failure::{bail, Error};
use fnv::FnvHashMap;
use futures::future::FutureExt;
use hyper::service::{make_service_fn, service_fn};
use log::{info, warn};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use structopt::StructOpt;
use tokio;
use tokio::signal::unix::{SignalKind, signal};
use tokio::signal::unix::{signal, SignalKind};
#[derive(StructOpt)]
pub struct Args {
/// Directory holding the SQLite3 index database.
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
#[structopt(
long,
default_value = "/var/lib/moonfire-nvr/db",
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf,
/// Directory holding user interface files (.html, .js, etc).
#[structopt(long, default_value = "/usr/local/lib/moonfire-nvr/ui", value_name="path",
parse(from_os_str))]
#[structopt(
long,
default_value = "/usr/local/lib/moonfire-nvr/ui",
value_name = "path",
parse(from_os_str)
)]
ui_dir: std::path::PathBuf,
/// Bind address for unencrypted HTTP server.
@ -98,7 +106,7 @@ const LOCALTIME_PATH: &'static str = "/etc/localtime";
const TIMEZONE_PATH: &'static str = "/etc/timezone";
const ZONEINFO_PATHS: [&'static str; 2] = [
"/usr/share/zoneinfo/", // Linux, macOS < High Sierra
"/var/db/timezone/zoneinfo/" // macOS High Sierra
"/var/db/timezone/zoneinfo/", // macOS High Sierra
];
fn trim_zoneinfo(p: &str) -> &str {
@ -145,25 +153,32 @@ fn resolve_zone() -> Result<String, Error> {
};
let p = trim_zoneinfo(localtime_dest);
if p.starts_with('/') {
bail!("Unable to resolve {} symlink destination {} to a timezone.",
LOCALTIME_PATH, &localtime_dest);
bail!(
"Unable to resolve {} symlink destination {} to a timezone.",
LOCALTIME_PATH,
&localtime_dest
);
}
return Ok(p.to_owned());
},
}
Err(e) => {
use ::std::io::ErrorKind;
if e.kind() != ErrorKind::NotFound && e.kind() != ErrorKind::InvalidInput {
bail!("Unable to read {} symlink: {}", LOCALTIME_PATH, e);
}
},
}
};
// If `TIMEZONE_PATH` is a file, use its contents as the zone name.
match ::std::fs::read_to_string(TIMEZONE_PATH) {
Ok(z) => return Ok(z),
Err(e) => {
bail!("Unable to resolve timezone from TZ env, {}, or {}. Last error: {}",
LOCALTIME_PATH, TIMEZONE_PATH, e);
bail!(
"Unable to resolve timezone from TZ env, {}, or {}. Last error: {}",
LOCALTIME_PATH,
TIMEZONE_PATH,
e
);
}
}
}
@ -179,7 +194,12 @@ pub async fn run(args: &Args) -> Result<i32, Error> {
let clocks = clock::RealClocks {};
let (_db_dir, conn) = super::open_conn(
&args.db_dir,
if args.read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?;
if args.read_only {
super::OpenMode::ReadOnly
} else {
super::OpenMode::ReadWrite
},
)?;
let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.read_only).unwrap());
info!("Database is loaded.");
@ -190,8 +210,11 @@ pub async fn run(args: &Args) -> Result<i32, Error> {
{
let mut l = db.lock();
let dirs_to_open: Vec<_> =
l.streams_by_id().values().filter_map(|s| s.sample_file_dir_id).collect();
let dirs_to_open: Vec<_> = l
.streams_by_id()
.values()
.filter_map(|s| s.sample_file_dir_id)
.collect();
l.open_sample_file_dirs(&dirs_to_open)?;
}
info!("Directories are opened.");
@ -212,7 +235,9 @@ pub async fn run(args: &Args) -> Result<i32, Error> {
let syncers = if !args.read_only {
let l = db.lock();
let mut dirs = FnvHashMap::with_capacity_and_hasher(
l.sample_file_dirs_by_id().len(), Default::default());
l.sample_file_dirs_by_id().len(),
Default::default(),
);
let streams = l.streams_by_id().len();
let env = streamer::Environment {
db: &db,
@ -236,11 +261,7 @@ pub async fn run(args: &Args) -> Result<i32, Error> {
let mut syncers = FnvHashMap::with_capacity_and_hasher(dirs.len(), Default::default());
for (id, dir) in dirs.drain() {
let (channel, join) = writer::start_syncer(db.clone(), id)?;
syncers.insert(id, Syncer {
dir,
channel,
join,
});
syncers.insert(id, Syncer { dir, channel, join });
}
// Then start up streams.
@ -253,10 +274,14 @@ pub async fn run(args: &Args) -> Result<i32, Error> {
let sample_file_dir_id = match stream.sample_file_dir_id {
Some(s) => s,
None => {
warn!("Can't record stream {} ({}/{}) because it has no sample file dir",
id, camera.short_name, stream.type_.as_str());
warn!(
"Can't record stream {} ({}/{}) because it has no sample file dir",
id,
camera.short_name,
stream.type_.as_str()
);
continue;
},
}
};
let rotate_offset_sec = streamer::ROTATE_INTERVAL_SEC * i as i64 / streams as i64;
let syncer = syncers.get(&sample_file_dir_id).unwrap();
@ -264,20 +289,33 @@ pub async fn run(args: &Args) -> Result<i32, Error> {
db::StreamType::SUB => object_detector.as_ref().map(|a| Arc::clone(a)),
_ => None,
};
let mut streamer = streamer::Streamer::new(&env, syncer.dir.clone(),
syncer.channel.clone(), *id, camera, stream,
rotate_offset_sec,
streamer::ROTATE_INTERVAL_SEC,
object_detector)?;
let mut streamer = streamer::Streamer::new(
&env,
syncer.dir.clone(),
syncer.channel.clone(),
*id,
camera,
stream,
rotate_offset_sec,
streamer::ROTATE_INTERVAL_SEC,
object_detector,
)?;
info!("Starting streamer for {}", streamer.short_name());
let name = format!("s-{}", streamer.short_name());
streamers.push(thread::Builder::new().name(name).spawn(move|| {
streamer.run();
}).expect("can't create thread"));
streamers.push(
thread::Builder::new()
.name(name)
.spawn(move || {
streamer.run();
})
.expect("can't create thread"),
);
}
drop(l);
Some(syncers)
} else { None };
} else {
None
};
// Start the web interface.
let make_svc = make_service_fn(move |_conn| {
@ -286,13 +324,13 @@ pub async fn run(args: &Args) -> Result<i32, Error> {
move |req| Arc::clone(&svc).serve(req)
}))
});
let server = ::hyper::Server::bind(&args.http_addr).tcp_nodelay(true).serve(make_svc);
let server = ::hyper::Server::bind(&args.http_addr)
.tcp_nodelay(true)
.serve(make_svc);
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 = 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(|_| ()));

View File

@ -30,19 +30,23 @@
//! Subcommand to run a SQLite shell.
use super::OpenMode;
use failure::Error;
use std::ffi::OsString;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::Command;
use super::OpenMode;
use structopt::StructOpt;
#[derive(StructOpt)]
pub struct Args {
/// Directory holding the SQLite3 index database.
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
#[structopt(
long,
default_value = "/var/lib/moonfire-nvr/db",
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf,
/// Opens the database in read-only mode and locks it only for shared access.
@ -60,7 +64,11 @@ pub struct Args {
}
pub fn run(args: &Args) -> Result<i32, Error> {
let mode = if args.read_only { OpenMode::ReadOnly } else { OpenMode::ReadWrite };
let mode = if args.read_only {
OpenMode::ReadOnly
} else {
OpenMode::ReadWrite
};
let _db_dir = super::open_dir(&args.db_dir, mode)?;
let mut db = OsString::new();
db.push("file:");
@ -69,5 +77,9 @@ pub fn run(args: &Args) -> Result<i32, Error> {
if args.read_only {
db.push("?mode=ro");
}
Err(Command::new("sqlite3").arg(&db).args(&args.arg).exec().into())
Err(Command::new("sqlite3")
.arg(&db)
.args(&args.arg)
.exec()
.into())
}

View File

@ -31,28 +31,35 @@
/// Upgrades the database schema.
///
/// See `guide/schema.md` for more information.
use failure::Error;
use structopt::StructOpt;
#[derive(StructOpt)]
pub struct Args {
#[structopt(long,
help = "Directory holding the SQLite3 index database.",
default_value = "/var/lib/moonfire-nvr/db",
parse(from_os_str))]
#[structopt(
long,
help = "Directory holding the SQLite3 index database.",
default_value = "/var/lib/moonfire-nvr/db",
parse(from_os_str)
)]
db_dir: std::path::PathBuf,
#[structopt(help = "When upgrading from schema version 1 to 2, the sample file directory.",
long, parse(from_os_str))]
#[structopt(
help = "When upgrading from schema version 1 to 2, the sample file directory.",
long,
parse(from_os_str)
)]
sample_file_dir: Option<std::path::PathBuf>,
#[structopt(help = "Resets the SQLite journal_mode to the specified mode prior to the \
upgrade. The default, delete, is recommended. off is very dangerous \
but may be desirable in some circumstances. See guide/schema.md for \
more information. The journal mode will be reset to wal after the \
upgrade.",
long, default_value = "delete")]
#[structopt(
help = "Resets the SQLite journal_mode to the specified mode prior to \
the upgrade. The default, delete, is recommended. off is very \
dangerous but may be desirable in some circumstances. See \
guide/schema.md for more information. The journal mode will be \
reset to wal after the upgrade.",
long,
default_value = "delete"
)]
preset_journal: String,
#[structopt(help = "Skips the normal post-upgrade vacuum operation.", long)]
@ -62,10 +69,16 @@ pub struct Args {
pub fn run(args: &Args) -> Result<i32, Error> {
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
db::upgrade::run(&db::upgrade::Args {
sample_file_dir: args.sample_file_dir.as_ref().map(std::path::PathBuf::as_path),
preset_journal: &args.preset_journal,
no_vacuum: args.no_vacuum,
}, &mut conn)?;
db::upgrade::run(
&db::upgrade::Args {
sample_file_dir: args
.sample_file_dir
.as_ref()
.map(std::path::PathBuf::as_path),
preset_journal: &args.preset_journal,
no_vacuum: args.no_vacuum,
},
&mut conn,
)?;
Ok(0)
}

View File

@ -41,7 +41,7 @@
//! would be more trouble than it's worth.
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use failure::{Error, bail, format_err};
use failure::{bail, format_err, Error};
use std::convert::TryFrom;
// See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element categories, and NAL unit
@ -49,13 +49,13 @@ use std::convert::TryFrom;
const NAL_UNIT_SEQ_PARAMETER_SET: u8 = 7;
const NAL_UNIT_PIC_PARAMETER_SET: u8 = 8;
const NAL_UNIT_TYPE_MASK: u8 = 0x1F; // bottom 5 bits of first byte of unit.
const NAL_UNIT_TYPE_MASK: u8 = 0x1F; // bottom 5 bits of first byte of unit.
// For certain common sub stream anamorphic resolutions, add a pixel aspect ratio box.
const PIXEL_ASPECT_RATIOS: [((u16, u16), (u16, u16)); 4] = [
((320, 240), ( 4, 3)),
((320, 240), (4, 3)),
((352, 240), (40, 33)),
((640, 480), ( 4, 3)),
((640, 480), (4, 3)),
((704, 480), (40, 33)),
];
@ -90,18 +90,22 @@ fn default_pixel_aspect_ratio(width: u16, height: u16) -> (u16, u16) {
/// TODO: detect invalid byte streams. For example, several 0x00s not followed by a 0x01, a stream
/// stream not starting with 0x00 0x00 0x00 0x01, or an empty NAL unit.
fn decode_h264_annex_b<'a, F>(mut data: &'a [u8], mut f: F) -> Result<(), Error>
where F: FnMut(&'a [u8]) -> Result<(), Error> {
where
F: FnMut(&'a [u8]) -> Result<(), Error>,
{
let start_code = &b"\x00\x00\x01"[..];
use nom::FindSubstring;
'outer: while let Some(pos) = data.find_substring(start_code) {
let mut unit = &data[0..pos];
data = &data[pos + start_code.len() ..];
data = &data[pos + start_code.len()..];
// Have zero or more bytes that end in a start code. Strip out any trailing 0x00s and
// process the unit if there's anything left.
loop {
match unit.last() {
None => continue 'outer,
Some(b) if *b == 0 => { unit = &unit[..unit.len()-1]; },
Some(b) if *b == 0 => {
unit = &unit[..unit.len() - 1];
}
Some(_) => break,
}
}
@ -139,8 +143,8 @@ fn parse_annex_b_extra_data(data: &[u8]) -> Result<(&[u8], &[u8]), Error> {
/// <https://github.com/dholroyd/h264-reader/issues/4>.
fn decode(encoded: &[u8]) -> Vec<u8> {
struct NalRead(Vec<u8>);
use h264_reader::Context;
use h264_reader::nal::NalHandler;
use h264_reader::Context;
impl NalHandler for NalRead {
type Ctx = ();
fn start(&mut self, _ctx: &mut Context<Self::Ctx>, _header: h264_reader::nal::NalHeader) {}
@ -177,8 +181,7 @@ impl ExtraData {
let ctx;
let sps_owner;
let sps; // reference to either within ctx or to sps_owner.
if extradata.starts_with(b"\x00\x00\x00\x01") ||
extradata.starts_with(b"\x00\x00\x01") {
if extradata.starts_with(b"\x00\x00\x00\x01") || extradata.starts_with(b"\x00\x00\x01") {
// ffmpeg supplied "extradata" in Annex B format.
let (s, p) = parse_annex_b_extra_data(extradata)?;
let rbsp = decode(&s[1..]);
@ -196,9 +199,11 @@ impl ExtraData {
if avcc.num_of_sequence_parameter_sets() != 1 {
bail!("Multiple SPSs!");
}
ctx = avcc.create_context(())
ctx = avcc
.create_context(())
.map_err(|e| format_err!("Can't load SPS+PPS: {:?}", e))?;
sps = ctx.sps_by_id(h264_reader::nal::pps::ParamSetId::from_u32(0).unwrap())
sps = ctx
.sps_by_id(h264_reader::nal::pps::ParamSetId::from_u32(0).unwrap())
.ok_or_else(|| format_err!("No SPS 0"))?;
};
@ -212,23 +217,23 @@ impl ExtraData {
sample_entry.extend_from_slice(b"\x00\x00\x00\x00avc1\x00\x00\x00\x00\x00\x00\x00\x01");
// VisualSampleEntry, ISO/IEC 14496-12 section 12.1.3.
sample_entry.extend_from_slice(&[0; 16]); // pre-defined + reserved
sample_entry.extend_from_slice(&[0; 16]); // pre-defined + reserved
sample_entry.write_u16::<BigEndian>(width)?;
sample_entry.write_u16::<BigEndian>(height)?;
sample_entry.extend_from_slice(&[
0x00, 0x48, 0x00, 0x00, // horizresolution
0x00, 0x48, 0x00, 0x00, // vertresolution
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x01, // frame count
0x00, 0x00, 0x00, 0x00, // compressorname
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x18, 0xff, 0xff, // depth + pre_defined
0x00, 0x48, 0x00, 0x00, // horizresolution
0x00, 0x48, 0x00, 0x00, // vertresolution
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x01, // frame count
0x00, 0x00, 0x00, 0x00, // compressorname
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x18, 0xff, 0xff, // depth + pre_defined
]);
// AVCSampleEntry, ISO/IEC 14496-15 section 5.3.4.1.
@ -245,10 +250,10 @@ impl ExtraData {
// "emulation_prevention_three_byte" in ISO/IEC 14496-10 section 7.4.
// It looks like 00 is not a valid value of profile_idc, so this distinction
// shouldn't be relevant here. And ffmpeg seems to ignore it.
sample_entry.push(1); // configurationVersion
sample_entry.push(sps[1]); // profile_idc . AVCProfileIndication
sample_entry.push(sps[2]); // ...misc bits... . profile_compatibility
sample_entry.push(sps[3]); // level_idc . AVCLevelIndication
sample_entry.push(1); // configurationVersion
sample_entry.push(sps[1]); // profile_idc . AVCProfileIndication
sample_entry.push(sps[2]); // ...misc bits... . profile_compatibility
sample_entry.push(sps[3]); // level_idc . AVCLevelIndication
// Hardcode lengthSizeMinusOne to 3, matching TransformSampleData's 4-byte
// lengths.
@ -260,41 +265,48 @@ impl ExtraData {
sample_entry.push(0xe1);
sample_entry.write_u16::<BigEndian>(u16::try_from(sps.len())?)?;
sample_entry.extend_from_slice(sps);
sample_entry.push(1); // # of PPSs.
sample_entry.push(1); // # of PPSs.
sample_entry.write_u16::<BigEndian>(u16::try_from(pps.len())?)?;
sample_entry.extend_from_slice(pps);
} else {
sample_entry.extend_from_slice(extradata);
};
// Fix up avc1 and avcC box lengths.
let cur_pos = sample_entry.len();
BigEndian::write_u32(&mut sample_entry[avcc_len_pos .. avcc_len_pos + 4],
u32::try_from(cur_pos - avcc_len_pos)?);
BigEndian::write_u32(
&mut sample_entry[avcc_len_pos..avcc_len_pos + 4],
u32::try_from(cur_pos - avcc_len_pos)?,
);
// PixelAspectRatioBox, ISO/IEC 14496-12 section 12.1.4.2.
// Write a PixelAspectRatioBox if necessary, as the sub streams can be be anamorphic.
let pasp = sps.vui_parameters.as_ref()
.and_then(|v| v.aspect_ratio_info.as_ref())
.and_then(|a| a.clone().get())
.unwrap_or_else(|| default_pixel_aspect_ratio(width, height));
let pasp = sps
.vui_parameters
.as_ref()
.and_then(|v| v.aspect_ratio_info.as_ref())
.and_then(|a| a.clone().get())
.unwrap_or_else(|| default_pixel_aspect_ratio(width, height));
if pasp != (1, 1) {
sample_entry.extend_from_slice(b"\x00\x00\x00\x10pasp"); // length + box name
sample_entry.extend_from_slice(b"\x00\x00\x00\x10pasp"); // length + box name
sample_entry.write_u32::<BigEndian>(pasp.0.into())?;
sample_entry.write_u32::<BigEndian>(pasp.1.into())?;
}
let cur_pos = sample_entry.len();
BigEndian::write_u32(&mut sample_entry[avc1_len_pos .. avc1_len_pos + 4],
u32::try_from(cur_pos - avc1_len_pos)?);
BigEndian::write_u32(
&mut sample_entry[avc1_len_pos..avc1_len_pos + 4],
u32::try_from(cur_pos - avc1_len_pos)?,
);
let profile_idc = sample_entry[103];
let constraint_flags = sample_entry[104];
let level_idc = sample_entry[105];
let rfc6381_codec =
format!("avc1.{:02x}{:02x}{:02x}", profile_idc, constraint_flags, level_idc);
let rfc6381_codec = format!(
"avc1.{:02x}{:02x}{:02x}",
profile_idc, constraint_flags, level_idc
);
Ok(ExtraData {
entry: db::VideoSampleEntryToInsert {
data: sample_entry,
@ -321,7 +333,7 @@ pub fn transform_sample_data(annexb_sample: &[u8], avc_sample: &mut Vec<u8>) ->
avc_sample.reserve(annexb_sample.len() + 4);
decode_h264_annex_b(annexb_sample, |unit| {
// 4-byte length; this must match ParseExtraData's lengthSizeMinusOne == 3.
avc_sample.write_u32::<BigEndian>(unit.len() as u32)?; // length
avc_sample.write_u32::<BigEndian>(unit.len() as u32)?; // length
avc_sample.extend_from_slice(unit);
Ok(())
})?;
@ -332,6 +344,7 @@ pub fn transform_sample_data(annexb_sample: &[u8], avc_sample: &mut Vec<u8>) ->
mod tests {
use db::testutil;
#[rustfmt::skip]
const ANNEX_B_TEST_INPUT: [u8; 35] = [
0x00, 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f,
0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,
@ -340,6 +353,7 @@ mod tests {
0xee, 0x3c, 0x80,
];
#[rustfmt::skip]
const AVC_DECODER_CONFIG_TEST_INPUT: [u8; 38] = [
0x01, 0x4d, 0x00, 0x1f, 0xff, 0xe1, 0x00, 0x17,
0x67, 0x4d, 0x00, 0x1f, 0x9a, 0x66, 0x02, 0x80,
@ -348,6 +362,7 @@ mod tests {
0x00, 0x04, 0x68, 0xee, 0x3c, 0x80,
];
#[rustfmt::skip]
const TEST_OUTPUT: [u8; 132] = [
0x00, 0x00, 0x00, 0x84, 0x61, 0x76, 0x63, 0x31,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
@ -376,8 +391,9 @@ mod tests {
super::decode_h264_annex_b(data, |p| {
pieces.push(p);
Ok(())
}).unwrap();
assert_eq!(&pieces, &[&data[4 .. 27], &data[31 ..]]);
})
.unwrap();
assert_eq!(&pieces, &[&data[4..27], &data[31..]]);
}
#[test]
@ -404,6 +420,7 @@ mod tests {
#[test]
fn test_transform_sample_data() {
testutil::init();
#[rustfmt::skip]
const INPUT: [u8; 64] = [
0x00, 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f,
0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,
@ -420,6 +437,7 @@ mod tests {
0xff, 0x8c, 0xd6, 0x35,
// (truncated)
];
#[rustfmt::skip]
const EXPECTED_OUTPUT: [u8; 64] = [
0x00, 0x00, 0x00, 0x17, 0x67, 0x4d, 0x00, 0x1f,
0x9a, 0x66, 0x02, 0x80, 0x2d, 0xff, 0x35, 0x01,

View File

@ -29,15 +29,15 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use db::auth::SessionHash;
use failure::{Error, format_err};
use serde::{Deserialize, Serialize};
use failure::{format_err, Error};
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::ops::Not;
use uuid::Uuid;
#[derive(Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct TopLevel<'a> {
pub time_zone_name: &'a str,
@ -57,7 +57,7 @@ pub struct TopLevel<'a> {
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct Session {
pub username: String,
@ -67,7 +67,9 @@ pub struct Session {
impl Session {
fn serialize_csrf<S>(csrf: &SessionHash, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
where
S: Serializer,
{
let mut tmp = [0u8; 32];
csrf.encode_base64(&mut tmp);
serializer.serialize_str(::std::str::from_utf8(&tmp[..]).expect("base64 is UTF-8"))
@ -77,7 +79,7 @@ impl Session {
/// JSON serialization wrapper for a single camera when processing `/api/` and
/// `/api/cameras/<uuid>/`. See `design/api.md` for details.
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct Camera<'a> {
pub uuid: Uuid,
pub short_name: &'a str,
@ -91,7 +93,7 @@ pub struct Camera<'a> {
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct CameraConfig<'a> {
pub onvif_host: &'a str,
pub username: &'a str,
@ -99,7 +101,7 @@ pub struct CameraConfig<'a> {
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct Stream<'a> {
pub retain_bytes: i64,
pub min_start_time_90k: Option<i64>,
@ -117,13 +119,13 @@ pub struct Stream<'a> {
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct StreamConfig<'a> {
pub rtsp_url: &'a str,
}
#[derive(Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct Signal<'a> {
pub id: u32,
#[serde(serialize_with = "Signal::serialize_cameras")]
@ -134,27 +136,27 @@ pub struct Signal<'a> {
}
#[derive(Deserialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub enum PostSignalsEndBase {
Epoch,
Now,
}
#[derive(Deserialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct LoginRequest<'a> {
pub username: &'a str,
pub password: String,
}
#[derive(Deserialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct LogoutRequest<'a> {
pub csrf: &'a str,
}
#[derive(Deserialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct PostSignalsRequest {
pub signal_ids: Vec<u32>,
pub states: Vec<u16>,
@ -164,13 +166,13 @@ pub struct PostSignalsRequest {
}
#[derive(Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct PostSignalsResponse {
pub time_90k: i64,
}
#[derive(Default, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct Signals {
pub times_90k: Vec<i64>,
pub signal_ids: Vec<u32>,
@ -178,7 +180,7 @@ pub struct Signals {
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct SignalType<'a> {
pub uuid: Uuid,
@ -187,7 +189,7 @@ pub struct SignalType<'a> {
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct SignalTypeState<'a> {
value: u16,
name: &'a str,
@ -198,8 +200,12 @@ pub struct SignalTypeState<'a> {
}
impl<'a> Camera<'a> {
pub fn wrap(c: &'a db::Camera, db: &'a db::LockedDatabase, include_days: bool,
include_config: bool) -> Result<Self, Error> {
pub fn wrap(
c: &'a db::Camera,
db: &'a db::LockedDatabase,
include_days: bool,
include_config: bool,
) -> Result<Self, Error> {
Ok(Camera {
uuid: c.uuid,
short_name: &c.short_name,
@ -220,11 +226,17 @@ impl<'a> Camera<'a> {
}
fn serialize_streams<S>(streams: &[Option<Stream>; 2], serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(streams.len()))?;
for (i, s) in streams.iter().enumerate() {
if let &Some(ref s) = s {
map.serialize_key(db::StreamType::from_index(i).expect("invalid stream type index").as_str())?;
map.serialize_key(
db::StreamType::from_index(i)
.expect("invalid stream type index")
.as_str(),
)?;
map.serialize_value(s)?;
}
}
@ -233,13 +245,20 @@ impl<'a> Camera<'a> {
}
impl<'a> Stream<'a> {
fn wrap(db: &'a db::LockedDatabase, id: Option<i32>, include_days: bool, include_config: bool)
-> Result<Option<Self>, Error> {
fn wrap(
db: &'a db::LockedDatabase,
id: Option<i32>,
include_days: bool,
include_config: bool,
) -> Result<Option<Self>, Error> {
let id = match id {
Some(id) => id,
None => return Ok(None),
};
let s = db.streams_by_id().get(&id).ok_or_else(|| format_err!("missing stream {}", id))?;
let s = db
.streams_by_id()
.get(&id)
.ok_or_else(|| format_err!("missing stream {}", id))?;
Ok(Some(Stream {
retain_bytes: s.retain_bytes,
min_start_time_90k: s.range.as_ref().map(|r| r.start.0),
@ -257,9 +276,13 @@ impl<'a> Stream<'a> {
}))
}
fn serialize_days<S>(days: &Option<BTreeMap<db::StreamDayKey, db::StreamDayValue>>,
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
fn serialize_days<S>(
days: &Option<BTreeMap<db::StreamDayKey, db::StreamDayValue>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let days = match days.as_ref() {
Some(d) => d,
None => return serializer.serialize_none(),
@ -268,7 +291,7 @@ impl<'a> Stream<'a> {
for (k, v) in days {
map.serialize_key(k.as_ref())?;
let bounds = k.bounds();
map.serialize_value(&StreamDayValue{
map.serialize_value(&StreamDayValue {
start_time_90k: bounds.start.0,
end_time_90k: bounds.end.0,
total_duration_90k: v.duration.0,
@ -289,16 +312,19 @@ impl<'a> Signal<'a> {
}
}
fn serialize_cameras<S>(cameras: &(&db::Signal, &db::LockedDatabase),
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
fn serialize_cameras<S>(
cameras: &(&db::Signal, &db::LockedDatabase),
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let (s, db) = cameras;
let mut map = serializer.serialize_map(Some(s.cameras.len()))?;
for sc in &s.cameras {
let c = db.cameras_by_id()
.get(&sc.camera_id)
.ok_or_else(|| S::Error::custom(format!("signal has missing camera id {}",
sc.camera_id)))?;
let c = db.cameras_by_id().get(&sc.camera_id).ok_or_else(|| {
S::Error::custom(format!("signal has missing camera id {}", sc.camera_id))
})?;
map.serialize_key(&c.uuid)?;
map.serialize_value(match sc.type_ {
db::signal::SignalCameraType::Direct => "direct",
@ -317,9 +343,10 @@ impl<'a> SignalType<'a> {
}
}
fn serialize_states<S>(type_: &db::signal::Type,
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
fn serialize_states<S>(type_: &db::signal::Type, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(type_.states.len()))?;
for s in &type_.states {
seq.serialize_element(&SignalTypeState::wrap(s))?;
@ -340,7 +367,7 @@ impl<'a> SignalTypeState<'a> {
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
struct StreamDayValue {
pub start_time_90k: i64,
pub end_time_90k: i64,
@ -350,24 +377,33 @@ struct StreamDayValue {
impl<'a> TopLevel<'a> {
/// Serializes cameras as a list (rather than a map), optionally including the `days` and
/// `cameras` fields.
fn serialize_cameras<S>(cameras: &(&db::LockedDatabase, bool, bool),
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
fn serialize_cameras<S>(
cameras: &(&db::LockedDatabase, bool, bool),
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let (db, include_days, include_config) = *cameras;
let cs = db.cameras_by_id();
let mut seq = serializer.serialize_seq(Some(cs.len()))?;
for (_, c) in cs {
seq.serialize_element(
&Camera::wrap(c, db, include_days, include_config)
.map_err(|e| S::Error::custom(e))?)?;
.map_err(|e| S::Error::custom(e))?,
)?;
}
seq.end()
}
/// Serializes signals as a list (rather than a map), optionally including the `days` field.
fn serialize_signals<S>(signals: &(&db::LockedDatabase, bool),
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
fn serialize_signals<S>(
signals: &(&db::LockedDatabase, bool),
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let (db, include_days) = *signals;
let ss = db.signals_by_id();
let mut seq = serializer.serialize_seq(Some(ss.len()))?;
@ -378,9 +414,10 @@ impl<'a> TopLevel<'a> {
}
/// Serializes signals as a list (rather than a map), optionally including the `days` field.
fn serialize_signal_types<S>(db: &db::LockedDatabase,
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
fn serialize_signal_types<S>(db: &db::LockedDatabase, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ss = db.signal_types_by_uuid();
let mut seq = serializer.serialize_seq(Some(ss.len()))?;
for (u, t) in ss {
@ -391,7 +428,7 @@ impl<'a> TopLevel<'a> {
}
#[derive(Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct ListRecordings<'a> {
pub recordings: Vec<Recording>,
@ -403,22 +440,27 @@ pub struct ListRecordings<'a> {
}
impl<'a> ListRecordings<'a> {
fn serialize_video_sample_entries<S>(video_sample_entries: &(&db::LockedDatabase, Vec<i32>),
serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
fn serialize_video_sample_entries<S>(
video_sample_entries: &(&db::LockedDatabase, Vec<i32>),
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let (db, ref v) = *video_sample_entries;
let mut map = serializer.serialize_map(Some(v.len()))?;
for id in v {
map.serialize_entry(
id,
&VideoSampleEntry::from(&db.video_sample_entries_by_id().get(id).unwrap()))?;
&VideoSampleEntry::from(&db.video_sample_entries_by_id().get(id).unwrap()),
)?;
}
map.end()
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct Recording {
pub start_time_90k: i64,
pub end_time_90k: i64,
@ -439,7 +481,7 @@ pub struct Recording {
}
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
#[serde(rename_all = "camelCase")]
pub struct VideoSampleEntry {
pub width: u16,
pub height: u16,

View File

@ -28,7 +28,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#![cfg_attr(all(feature="nightly", test), feature(test))]
#![cfg_attr(all(feature = "nightly", test), feature(test))]
use log::{debug, error};
use std::str::FromStr;
@ -40,7 +40,7 @@ mod analytics;
/// Stub implementation of analytics module when not compiled with TensorFlow Lite.
#[cfg(not(feature = "analytics"))]
mod analytics {
use failure::{Error, bail};
use failure::{bail, Error};
pub struct ObjectDetector;
@ -53,13 +53,18 @@ mod analytics {
pub struct ObjectDetectorStream;
impl ObjectDetectorStream {
pub fn new(_par: ffmpeg::avcodec::InputCodecParameters<'_>,
_detector: &ObjectDetector) -> Result<Self, Error> {
pub fn new(
_par: ffmpeg::avcodec::InputCodecParameters<'_>,
_detector: &ObjectDetector,
) -> Result<Self, Error> {
unimplemented!();
}
pub fn process_frame(&mut self, _pkt: &ffmpeg::avcodec::Packet<'_>,
_detector: &ObjectDetector) -> Result<(), Error> {
pub fn process_frame(
&mut self,
_pkt: &ffmpeg::avcodec::Packet<'_>,
_detector: &ObjectDetector,
) -> Result<(), Error> {
unimplemented!();
}
}
@ -76,7 +81,10 @@ mod streamer;
mod web;
#[derive(StructOpt)]
#[structopt(name="moonfire-nvr", about="security camera network video recorder")]
#[structopt(
name = "moonfire-nvr",
about = "security camera network video recorder"
)]
enum Args {
/// Checks database integrity (like fsck).
Check(cmds::check::Args),
@ -128,10 +136,12 @@ impl Args {
fn main() {
let args = Args::from_args();
let mut h = mylog::Builder::new()
.set_format(::std::env::var("MOONFIRE_FORMAT")
.map_err(|_| ())
.and_then(|s| mylog::Format::from_str(&s))
.unwrap_or(mylog::Format::Google))
.set_format(
::std::env::var("MOONFIRE_FORMAT")
.map_err(|_| ())
.and_then(|s| mylog::Format::from_str(&s))
.unwrap_or(mylog::Format::Google),
)
.set_spec(&::std::env::var("MOONFIRE_LOG").unwrap_or("info".to_owned()))
.build();
h.clone().install().unwrap();
@ -144,10 +154,10 @@ fn main() {
Err(e) => {
error!("Exiting due to error: {}", base::prettify_failure(&e));
::std::process::exit(1);
},
}
Ok(rv) => {
debug!("Exiting with status {}", rv);
std::process::exit(rv)
},
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -30,17 +30,17 @@
//! Tools for implementing a `http_serve::Entity` body composed from many "slices".
use crate::body::{wrap_error, BoxedError};
use base::format_err_t;
use crate::body::{BoxedError, wrap_error};
use failure::{Error, bail};
use futures::{Stream, stream, stream::StreamExt};
use failure::{bail, Error};
use futures::{stream, stream::StreamExt, Stream};
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 {
pub trait Slice: fmt::Debug + Sized + Sync + 'static {
type Ctx: Send + Sync + Clone;
type Chunk: Send + Sync;
@ -52,15 +52,22 @@ pub trait Slice : fmt::Debug + Sized + Sync + 'static {
/// Gets the body bytes indicated by `r`, which is relative to this slice's start.
/// 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 = Result<Self::Chunk, BoxedError>> + Sync + Send>;
fn get_range(
&self,
ctx: &Self::Ctx,
r: Range<u64>,
len: u64,
) -> Box<dyn Stream<Item = Result<Self::Chunk, BoxedError>> + Sync + Send>;
fn get_slices(ctx: &Self::Ctx) -> &Slices<Self>;
}
/// Helper to serve byte ranges from a body which is broken down into many "slices".
/// This is used to implement `.mp4` serving in `mp4::File` from `mp4::Slice` enums.
pub struct Slices<S> where S: Slice {
pub struct Slices<S>
where
S: Slice,
{
/// The total byte length of the `Slices`.
/// Equivalent to `self.slices.back().map(|s| s.end()).unwrap_or(0)`; kept for convenience and
/// to avoid a branch.
@ -70,22 +77,45 @@ pub struct Slices<S> where S: Slice {
slices: Vec<S>,
}
impl<S> fmt::Debug for Slices<S> where S: Slice {
impl<S> fmt::Debug for Slices<S>
where
S: Slice,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} slices with overall length {}:", self.slices.len(), self.len)?;
write!(
f,
"{} slices with overall length {}:",
self.slices.len(),
self.len
)?;
let mut start = 0;
for (i, s) in self.slices.iter().enumerate() {
let end = s.end();
write!(f, "\ni {:7}: range [{:12}, {:12}) len {:12}: {:?}",
i, start, end, end - start, s)?;
write!(
f,
"\ni {:7}: range [{:12}, {:12}) len {:12}: {:?}",
i,
start,
end,
end - start,
s
)?;
start = end;
}
Ok(())
}
}
impl<S> Slices<S> where S: Slice {
pub fn new() -> Self { Slices{len: 0, slices: Vec::new()} }
impl<S> Slices<S>
where
S: Slice,
{
pub fn new() -> Self {
Slices {
len: 0,
slices: Vec::new(),
}
}
/// Reserves space for at least `additional` more slices to be appended.
pub fn reserve(&mut self, additional: usize) {
@ -95,8 +125,13 @@ impl<S> Slices<S> where S: Slice {
/// Appends the given slice, which must have end > the Slices's current len.
pub fn append(&mut self, slice: S) -> Result<(), Error> {
if slice.end() <= self.len {
bail!("end {} <= len {} while adding slice {:?} to slices:\n{:?}",
slice.end(), self.len, slice, self);
bail!(
"end {} <= len {} while adding slice {:?} to slices:\n{:?}",
slice.end(),
self.len,
slice,
self
);
}
self.len = slice.end();
self.slices.push(slice);
@ -104,59 +139,78 @@ impl<S> Slices<S> where S: Slice {
}
/// Returns the total byte length of all slices.
pub fn len(&self) -> u64 { self.len }
pub fn len(&self) -> u64 {
self.len
}
/// Returns the number of slices.
pub fn num(&self) -> usize { self.slices.len() }
pub fn num(&self) -> usize {
self.slices.len()
}
/// 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 = Result<S::Chunk, BoxedError>> + Sync + Send> {
pub fn get_range(
&self,
ctx: &S::Ctx,
range: Range<u64>,
) -> Box<dyn Stream<Item = Result<S::Chunk, BoxedError>> + Sync + Send> {
if range.start > range.end || range.end > self.len {
return Box::new(stream::once(futures::future::err(wrap_error(format_err_t!(
Internal, "Bad range {:?} for slice of length {}", range, self.len)))));
return Box::new(stream::once(futures::future::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
// (from the preceding slice) the start of its range.
let (i, slice_start) = match self.slices.binary_search_by_key(&range.start, |s| s.end()) {
Ok(i) => (i+1, self.slices[i].end()), // desired start == slice i's end; first is i+1!
Ok(i) => (i + 1, self.slices[i].end()), // desired start == slice i's end; first is i+1!
Err(i) if i == 0 => (0, 0), // desired start < slice 0's end; first is 0.
Err(i) => (i, self.slices[i-1].end()), // desired start < slice i's end; first is i.
Err(i) => (i, self.slices[i - 1].end()), // desired start < slice i's end; first is i.
};
// Iterate through and write each slice until the end.
let start_pos = range.start - slice_start;
let bodies = stream::unfold(
(ctx.clone(), i, start_pos, slice_start), move |(c, i, start_pos, slice_start)| {
let (body, min_end);
{
let self_ = S::get_slices(&c);
if i == self_.slices.len() { return futures::future::ready(None) }
let s = &self_.slices[i];
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);
};
futures::future::ready(Some((Pin::from(body), (c, i+1, 0, min_end))))
});
(ctx.clone(), i, start_pos, slice_start),
move |(c, i, start_pos, slice_start)| {
let (body, min_end);
{
let self_ = S::get_slices(&c);
if i == self_.slices.len() {
return futures::future::ready(None);
}
let s = &self_.slices[i];
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);
};
futures::future::ready(Some((Pin::from(body), (c, i + 1, 0, min_end))))
},
);
Box::new(bodies.flatten())
}
}
#[cfg(test)]
mod tests {
use super::{Slice, Slices};
use crate::body::BoxedError;
use db::testutil;
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)]
pub struct FakeChunk {
@ -174,30 +228,45 @@ mod tests {
type Ctx = &'static Slices<FakeSlice>;
type Chunk = FakeChunk;
fn end(&self) -> u64 { self.end }
fn get_range(&self, _ctx: &&'static Slices<FakeSlice>, r: Range<u64>, _l: u64)
-> Box<dyn Stream<Item = Result<FakeChunk, BoxedError>> + Send + Sync> {
Box::new(stream::once(futures::future::ok(FakeChunk{slice: self.name, range: r})))
fn end(&self) -> u64 {
self.end
}
fn get_slices(ctx: &&'static Slices<FakeSlice>) -> &'static Slices<Self> { *ctx }
fn get_range(
&self,
_ctx: &&'static Slices<FakeSlice>,
r: Range<u64>,
_l: u64,
) -> 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
}
}
lazy_static! {
#[rustfmt::skip]
static ref SLICES: Slices<FakeSlice> = {
let mut s = Slices::new();
s.append(FakeSlice{end: 5, name: "a"}).unwrap();
s.append(FakeSlice{end: 5+13, name: "b"}).unwrap();
s.append(FakeSlice{end: 5+13+7, name: "c"}).unwrap();
s.append(FakeSlice{end: 5+13+7+17, name: "d"}).unwrap();
s.append(FakeSlice{end: 5+13+7+17+19, name: "e"}).unwrap();
s.append(FakeSlice { end: 5, name: "a" }).unwrap();
s.append(FakeSlice { end: 5 + 13, name: "b" }).unwrap();
s.append(FakeSlice { end: 5 + 13 + 7, name: "c" }).unwrap();
s.append(FakeSlice { end: 5 + 13 + 7 + 17, name: "d" }).unwrap();
s.append(FakeSlice { end: 5 + 13 + 7 + 17 + 19, name: "e" }).unwrap();
s
};
}
async fn get_range(r: Range<u64>) -> Vec<FakeChunk> {
Pin::from(SLICES.get_range(&&*SLICES, r)).try_collect().await.unwrap()
Pin::from(SLICES.get_range(&&*SLICES, r))
.try_collect()
.await
.unwrap()
}
#[test]
@ -210,48 +279,68 @@ mod tests {
pub async fn exact_slice() {
// Test writing exactly slice b.
testutil::init();
let out = get_range(5 .. 18).await;
assert_eq!(&[FakeChunk{slice: "b", range: 0 .. 13}], &out[..]);
let out = get_range(5..18).await;
assert_eq!(
&[FakeChunk {
slice: "b",
range: 0..13
}],
&out[..]
);
}
#[tokio::test]
pub async fn offset_first() {
// Test writing part of slice a.
testutil::init();
let out = get_range(1 .. 3).await;
assert_eq!(&[FakeChunk{slice: "a", range: 1 .. 3}], &out[..]);
let out = get_range(1..3).await;
assert_eq!(
&[FakeChunk {
slice: "a",
range: 1..3
}],
&out[..]
);
}
#[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 = get_range(17 .. 26).await;
assert_eq!(&[
FakeChunk{slice: "b", range: 12 .. 13},
FakeChunk{slice: "c", range: 0 .. 7},
FakeChunk{slice: "d", range: 0 .. 1},
], &out[..]);
let out = get_range(17..26).await;
#[rustfmt::skip]
assert_eq!(
&[
FakeChunk { slice: "b", range: 12..13 },
FakeChunk { slice: "c", range: 0..7 },
FakeChunk { slice: "d", range: 0..1 },
],
&out[..]
);
}
#[tokio::test]
pub async fn everything() {
// Test writing the whole Slices.
testutil::init();
let out = get_range(0 .. 61).await;
assert_eq!(&[
FakeChunk{slice: "a", range: 0 .. 5},
FakeChunk{slice: "b", range: 0 .. 13},
FakeChunk{slice: "c", range: 0 .. 7},
FakeChunk{slice: "d", range: 0 .. 17},
FakeChunk{slice: "e", range: 0 .. 19},
], &out[..]);
let out = get_range(0..61).await;
#[rustfmt::skip]
assert_eq!(
&[
FakeChunk { slice: "a", range: 0..5 },
FakeChunk { slice: "b", range: 0..13 },
FakeChunk { slice: "c", range: 0..7 },
FakeChunk { slice: "d", range: 0..17 },
FakeChunk { slice: "e", range: 0..19 },
],
&out[..]
);
}
#[tokio::test]
pub async fn at_end() {
testutil::init();
let out = get_range(61 .. 61).await;
let out = get_range(61..61).await;
let empty: &[FakeChunk] = &[];
assert_eq!(empty, &out[..]);
}

View File

@ -30,7 +30,7 @@
use crate::h264;
use cstr::cstr;
use failure::{Error, bail};
use failure::{bail, Error};
use ffmpeg;
use lazy_static::lazy_static;
use log::{debug, warn};
@ -50,13 +50,10 @@ pub enum Source<'a> {
File(&'a str),
/// An RTSP stream, for production use.
Rtsp {
url: &'a str,
redacted_url: &'a str
},
Rtsp { url: &'a str, redacted_url: &'a str },
}
pub trait Opener<S : Stream> : Sync {
pub trait Opener<S: Stream>: Sync {
fn open(&self, src: Source) -> Result<S, Error>;
}
@ -70,8 +67,10 @@ pub struct Ffmpeg {}
impl Ffmpeg {
fn new() -> Ffmpeg {
START.call_once(|| { ffmpeg::Ffmpeg::new(); });
Ffmpeg{}
START.call_once(|| {
ffmpeg::Ffmpeg::new();
});
Ffmpeg {}
}
}
@ -84,39 +83,57 @@ impl Opener<FfmpegStream> for Ffmpeg {
let mut open_options = ffmpeg::avutil::Dictionary::new();
// Work around https://github.com/scottlamb/moonfire-nvr/issues/10
open_options.set(cstr!("advanced_editlist"), cstr!("false")).unwrap();
open_options
.set(cstr!("advanced_editlist"), cstr!("false"))
.unwrap();
let url = format!("file:{}", filename);
let i = InputFormatContext::open(&CString::new(url.clone()).unwrap(),
&mut open_options)?;
let i = InputFormatContext::open(
&CString::new(url.clone()).unwrap(),
&mut open_options,
)?;
if !open_options.empty() {
warn!("While opening URL {}, some options were not understood: {}",
url, open_options);
warn!(
"While opening URL {}, some options were not understood: {}",
url, open_options
);
}
i
}
Source::Rtsp{url, redacted_url} => {
Source::Rtsp { url, redacted_url } => {
let mut open_options = ffmpeg::avutil::Dictionary::new();
open_options.set(cstr!("rtsp_transport"), cstr!("tcp")).unwrap();
open_options.set(cstr!("user-agent"), cstr!("moonfire-nvr")).unwrap();
open_options
.set(cstr!("rtsp_transport"), cstr!("tcp"))
.unwrap();
open_options
.set(cstr!("user-agent"), cstr!("moonfire-nvr"))
.unwrap();
// 10-second socket timeout, in microseconds.
open_options.set(cstr!("stimeout"), cstr!("10000000")).unwrap();
open_options
.set(cstr!("stimeout"), cstr!("10000000"))
.unwrap();
// Without this option, the first packet has an incorrect pts.
// https://trac.ffmpeg.org/ticket/5018
open_options.set(cstr!("fflags"), cstr!("nobuffer")).unwrap();
open_options
.set(cstr!("fflags"), cstr!("nobuffer"))
.unwrap();
// Moonfire NVR currently only supports video, so receiving audio is wasteful.
// It also triggers <https://github.com/scottlamb/moonfire-nvr/issues/36>.
open_options.set(cstr!("allowed_media_types"), cstr!("video")).unwrap();
open_options
.set(cstr!("allowed_media_types"), cstr!("video"))
.unwrap();
let i = InputFormatContext::open(&CString::new(url).unwrap(), &mut open_options)?;
if !open_options.empty() {
warn!("While opening URL {}, some options were not understood: {}",
redacted_url, open_options);
warn!(
"While opening URL {}, some options were not understood: {}",
redacted_url, open_options
);
}
i
},
}
};
input.find_stream_info()?;
@ -125,7 +142,7 @@ impl Opener<FfmpegStream> for Ffmpeg {
let mut video_i = None;
{
let s = input.streams();
for i in 0 .. s.len() {
for i in 0..s.len() {
if s.get(i).codecpar().codec_type().is_video() {
debug!("Video stream index is {}", i);
video_i = Some(i);
@ -138,10 +155,7 @@ impl Opener<FfmpegStream> for Ffmpeg {
None => bail!("no video stream"),
};
Ok(FfmpegStream {
input,
video_i,
})
Ok(FfmpegStream { input, video_i })
}
}
@ -159,7 +173,11 @@ impl Stream for FfmpegStream {
let video = self.input.streams().get(self.video_i);
let tb = video.time_base();
if tb.num != 1 || tb.den != 90000 {
bail!("video stream has timebase {}/{}; expected 1/90000", tb.num, tb.den);
bail!(
"video stream has timebase {}/{}; expected 1/90000",
tb.num,
tb.den
);
}
let codec = video.codecpar();
let codec_id = codec.codec_id();
@ -167,8 +185,11 @@ impl Stream for FfmpegStream {
bail!("stream's video codec {:?} is not h264", codec_id);
}
let dims = codec.dims();
h264::ExtraData::parse(codec.extradata(), u16::try_from(dims.width)?,
u16::try_from(dims.height)?)
h264::ExtraData::parse(
codec.extradata(),
u16::try_from(dims.width)?,
u16::try_from(dims.height)?,
)
}
fn get_next<'i>(&'i mut self) -> Result<ffmpeg::avcodec::Packet<'i>, ffmpeg::Error> {

View File

@ -28,11 +28,11 @@
// 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 base::clock::{Clocks, TimerGuard};
use crate::h264;
use crate::stream;
use db::{Camera, Database, Stream, dir, recording, writer};
use failure::{Error, bail, format_err};
use base::clock::{Clocks, TimerGuard};
use db::{dir, recording, writer, Camera, Database, Stream};
use failure::{bail, format_err, Error};
use log::{debug, info, trace, warn};
use std::result::Result;
use std::sync::atomic::{AtomicBool, Ordering};
@ -43,13 +43,21 @@ use url::Url;
pub static ROTATE_INTERVAL_SEC: i64 = 60;
/// Common state that can be used by multiple `Streamer` instances.
pub struct Environment<'a, 'b, C, S> where C: Clocks + Clone, S: 'a + stream::Stream {
pub struct Environment<'a, 'b, C, S>
where
C: Clocks + Clone,
S: 'a + stream::Stream,
{
pub opener: &'a dyn stream::Opener<S>,
pub db: &'b Arc<Database<C>>,
pub shutdown: &'b Arc<AtomicBool>,
}
pub struct Streamer<'a, C, S> where C: Clocks + Clone, S: 'a + stream::Stream {
pub struct Streamer<'a, C, S>
where
C: Clocks + Clone,
S: 'a + stream::Stream,
{
shutdown: Arc<AtomicBool>,
// State below is only used by the thread in Run.
@ -66,17 +74,27 @@ pub struct Streamer<'a, C, S> where C: Clocks + Clone, S: 'a + stream::Stream {
detector: Option<Arc<crate::analytics::ObjectDetector>>,
}
impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::Stream {
pub fn new<'b>(env: &Environment<'a, 'b, C, S>, dir: Arc<dir::SampleFileDir>,
syncer_channel: writer::SyncerChannel<::std::fs::File>,
stream_id: i32, c: &Camera, s: &Stream, rotate_offset_sec: i64,
rotate_interval_sec: i64,
detector: Option<Arc<crate::analytics::ObjectDetector>>)
-> Result<Self, Error> {
impl<'a, C, S> Streamer<'a, C, S>
where
C: 'a + Clocks + Clone,
S: 'a + stream::Stream,
{
pub fn new<'b>(
env: &Environment<'a, 'b, C, S>,
dir: Arc<dir::SampleFileDir>,
syncer_channel: writer::SyncerChannel<::std::fs::File>,
stream_id: i32,
c: &Camera,
s: &Stream,
rotate_offset_sec: i64,
rotate_interval_sec: i64,
detector: Option<Arc<crate::analytics::ObjectDetector>>,
) -> Result<Self, Error> {
let mut url = Url::parse(&s.rtsp_url)?;
let mut redacted_url = url.clone();
if !c.username.is_empty() {
url.set_username(&c.username).map_err(|_| format_err!("can't set username"))?;
url.set_username(&c.username)
.map_err(|_| format_err!("can't set username"))?;
redacted_url.set_username(&c.username).unwrap();
url.set_password(Some(&c.password)).unwrap();
redacted_url.set_password(Some("redacted")).unwrap();
@ -97,14 +115,20 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::
})
}
pub fn short_name(&self) -> &str { &self.short_name }
pub fn short_name(&self) -> &str {
&self.short_name
}
pub fn run(&mut self) {
while !self.shutdown.load(Ordering::SeqCst) {
if let Err(e) = self.run_once() {
let sleep_time = time::Duration::seconds(1);
warn!("{}: sleeping for {:?} after error: {}",
self.short_name, sleep_time, base::prettify_failure(&e));
warn!(
"{}: sleeping for {:?} after error: {}",
self.short_name,
sleep_time,
base::prettify_failure(&e)
);
self.db.clocks().sleep(sleep_time);
}
}
@ -127,21 +151,31 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::
let mut detector_stream = match self.detector.as_ref() {
None => None,
Some(od) => Some(crate::analytics::ObjectDetectorStream::new(
stream.get_video_codecpar(), &od)?),
stream.get_video_codecpar(),
&od,
)?),
};
let extra_data = stream.get_extra_data()?;
let video_sample_entry_id = {
let _t = TimerGuard::new(&clocks, || "inserting video sample entry");
self.db.lock().insert_video_sample_entry(extra_data.entry)?
};
debug!("{}: video_sample_entry_id={}", self.short_name, video_sample_entry_id);
debug!(
"{}: video_sample_entry_id={}",
self.short_name, video_sample_entry_id
);
let mut seen_key_frame = false;
// Seconds since epoch at which to next rotate.
let mut rotate: Option<i64> = None;
let mut transformed = Vec::new();
let mut w = writer::Writer::new(&self.dir, &self.db, &self.syncer_channel, self.stream_id,
video_sample_entry_id);
let mut w = writer::Writer::new(
&self.dir,
&self.db,
&self.syncer_channel,
self.stream_id,
video_sample_entry_id,
);
while !self.shutdown.load(Ordering::SeqCst) {
let pkt = {
let _t = TimerGuard::new(&clocks, || "getting next packet");
@ -168,22 +202,32 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::
} else {
Some(r)
}
} else { None };
} else {
None
};
let r = match rotate {
Some(r) => r,
None => {
let sec = frame_realtime.sec;
let r = sec - (sec % self.rotate_interval_sec) + self.rotate_offset_sec;
let r = r + if r <= sec { self.rotate_interval_sec } else { 0 };
let r = r + if r <= sec {
self.rotate_interval_sec
} else {
0
};
// On the first recording, set rotate time to not the next rotate offset, but
// the one after, so that it's longer than usual rather than shorter than
// usual. This ensures there's plenty of frame times to use when calculating
// the start time.
let r = r + if w.previously_opened()? { 0 } else { self.rotate_interval_sec };
let r = r + if w.previously_opened()? {
0
} else {
self.rotate_interval_sec
};
let _t = TimerGuard::new(&clocks, || "creating writer");
r
},
}
};
let orig_data = match pkt.data() {
Some(d) => d,
@ -195,8 +239,9 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::
} else {
orig_data
};
let _t = TimerGuard::new(&clocks,
|| format!("writing {} bytes", transformed_data.len()));
let _t = TimerGuard::new(&clocks, || {
format!("writing {} bytes", transformed_data.len())
});
w.write(transformed_data, local_time, pts, pkt.is_key())?;
rotate = Some(r);
}
@ -210,17 +255,17 @@ impl<'a, C, S> Streamer<'a, C, S> where C: 'a + Clocks + Clone, S: 'a + stream::
#[cfg(test)]
mod tests {
use base::clock::{self, Clocks};
use crate::h264;
use crate::stream::{self, Opener, Stream};
use db::{CompositeId, recording, testutil};
use failure::{Error, bail};
use base::clock::{self, Clocks};
use db::{recording, testutil, CompositeId};
use failure::{bail, Error};
use log::trace;
use parking_lot::Mutex;
use std::cmp;
use std::convert::TryFrom;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use time;
struct ProxyingStream<'a> {
@ -234,8 +279,11 @@ mod tests {
}
impl<'a> ProxyingStream<'a> {
fn new(clocks: &'a clock::SimulatedClocks, buffered: time::Duration,
inner: stream::FfmpegStream) -> ProxyingStream {
fn new(
clocks: &'a clock::SimulatedClocks,
buffered: time::Duration,
inner: stream::FfmpegStream,
) -> ProxyingStream {
clocks.sleep(buffered);
ProxyingStream {
clocks: clocks,
@ -265,7 +313,8 @@ mod tests {
{
let goal = pkt.pts().unwrap() + pkt.duration() as i64;
let goal = time::Duration::nanoseconds(
goal * 1_000_000_000 / recording::TIME_UNITS_PER_SEC);
goal * 1_000_000_000 / recording::TIME_UNITS_PER_SEC,
);
let duration = goal - self.slept;
let buf_part = cmp::min(self.buffered, duration);
self.buffered = self.buffered - buf_part;
@ -293,7 +342,9 @@ mod tests {
self.inner.get_video_codecpar()
}
fn get_extra_data(&self) -> Result<h264::ExtraData, Error> { self.inner.get_extra_data() }
fn get_extra_data(&self) -> Result<h264::ExtraData, Error> {
self.inner.get_extra_data()
}
}
struct MockOpener<'a> {
@ -305,7 +356,7 @@ mod tests {
impl<'a> stream::Opener<ProxyingStream<'a>> for MockOpener<'a> {
fn open(&self, src: stream::Source) -> Result<ProxyingStream<'a>, Error> {
match src {
stream::Source::Rtsp{url, ..} => assert_eq!(url, &self.expected_url),
stream::Source::Rtsp { url, .. } => assert_eq!(url, &self.expected_url),
stream::Source::File(_) => panic!("expected rtsp url"),
};
let mut l = self.streams.lock();
@ -313,12 +364,12 @@ mod tests {
Some(stream) => {
trace!("MockOpener returning next stream");
Ok(stream)
},
}
None => {
trace!("MockOpener shutting down");
self.shutdown.store(true, Ordering::SeqCst);
bail!("done")
},
}
}
}
}
@ -335,14 +386,15 @@ mod tests {
let mut it = recording::SampleIndexIterator::new();
let mut frames = Vec::new();
while it.next(&rec.video_index).unwrap() {
frames.push(Frame{
frames.push(Frame {
start_90k: it.start_90k,
duration_90k: it.duration_90k,
is_key: it.is_key(),
});
}
Ok(frames)
}).unwrap()
})
.unwrap()
}
#[test]
@ -350,14 +402,16 @@ mod tests {
testutil::init();
// 2015-04-25 00:00:00 UTC
let clocks = clock::SimulatedClocks::new(time::Timespec::new(1429920000, 0));
clocks.sleep(time::Duration::seconds(86400)); // to 2015-04-26 00:00:00 UTC
clocks.sleep(time::Duration::seconds(86400)); // to 2015-04-26 00:00:00 UTC
let stream = stream::FFMPEG.open(stream::Source::File("src/testdata/clip.mp4")).unwrap();
let stream = stream::FFMPEG
.open(stream::Source::File("src/testdata/clip.mp4"))
.unwrap();
let mut stream = ProxyingStream::new(&clocks, time::Duration::seconds(2), stream);
stream.ts_offset = 123456; // starting pts of the input should be irrelevant
stream.ts_offset = 123456; // starting pts of the input should be irrelevant
stream.ts_offset_pkts_left = u32::max_value();
stream.pkts_left = u32::max_value();
let opener = MockOpener{
let opener = MockOpener {
expected_url: "rtsp://foo:bar@test-camera/main".to_owned(),
streams: Mutex::new(vec![stream]),
shutdown: Arc::new(AtomicBool::new(false)),
@ -373,9 +427,23 @@ mod tests {
let l = db.db.lock();
let camera = l.cameras_by_id().get(&testutil::TEST_CAMERA_ID).unwrap();
let s = l.streams_by_id().get(&testutil::TEST_STREAM_ID).unwrap();
let dir = db.dirs_by_stream_id.get(&testutil::TEST_STREAM_ID).unwrap().clone();
stream = super::Streamer::new(&env, dir, db.syncer_channel.clone(),
testutil::TEST_STREAM_ID, camera, s, 0, 3, None).unwrap();
let dir = db
.dirs_by_stream_id
.get(&testutil::TEST_STREAM_ID)
.unwrap()
.clone();
stream = super::Streamer::new(
&env,
dir,
db.syncer_channel.clone(),
testutil::TEST_STREAM_ID,
camera,
s,
0,
3,
None,
)
.unwrap();
}
stream.run();
assert!(opener.streams.lock().is_empty());
@ -386,25 +454,28 @@ mod tests {
// 3-second boundaries (such as 2016-04-26 00:00:03), rotation happens somewhat later:
// * the first rotation is always skipped
// * the second rotation is deferred until a key frame.
#[rustfmt::skip]
assert_eq!(get_frames(&db, CompositeId::new(testutil::TEST_STREAM_ID, 0)), &[
Frame{start_90k: 0, duration_90k: 90379, is_key: true},
Frame{start_90k: 90379, duration_90k: 89884, is_key: false},
Frame{start_90k: 180263, duration_90k: 89749, is_key: false},
Frame{start_90k: 270012, duration_90k: 89981, is_key: false}, // pts_time 3.0001...
Frame{start_90k: 359993, duration_90k: 90055, is_key: true},
Frame{start_90k: 450048, duration_90k: 89967, is_key: false},
Frame{start_90k: 540015, duration_90k: 90021, is_key: false}, // pts_time 6.0001...
Frame{start_90k: 630036, duration_90k: 89958, is_key: false},
Frame { start_90k: 0, duration_90k: 90379, is_key: true },
Frame { start_90k: 90379, duration_90k: 89884, is_key: false },
Frame { start_90k: 180263, duration_90k: 89749, is_key: false },
Frame { start_90k: 270012, duration_90k: 89981, is_key: false }, // pts_time 3.0001...
Frame { start_90k: 359993, duration_90k: 90055, is_key: true },
Frame { start_90k: 450048, duration_90k: 89967, is_key: false },
Frame { start_90k: 540015, duration_90k: 90021, is_key: false }, // pts_time 6.0001...
Frame { start_90k: 630036, duration_90k: 89958, is_key: false },
]);
#[rustfmt::skip]
assert_eq!(get_frames(&db, CompositeId::new(testutil::TEST_STREAM_ID, 1)), &[
Frame{start_90k: 0, duration_90k: 90011, is_key: true},
Frame{start_90k: 90011, duration_90k: 0, is_key: false},
Frame { start_90k: 0, duration_90k: 90011, is_key: true },
Frame { start_90k: 90011, duration_90k: 0, is_key: false },
]);
let mut recordings = Vec::new();
db.list_recordings_by_id(testutil::TEST_STREAM_ID, 0..2, &mut |r| {
recordings.push(r);
Ok(())
}).unwrap();
})
.unwrap();
assert_eq!(2, recordings.len());
assert_eq!(0, recordings[0].id.recording());
assert_eq!(recording::Time(128700575999999), recordings[0].start);

File diff suppressed because it is too large Load Diff