Merge branch 'master' into new-schema

This commit is contained in:
Scott Lamb 2020-04-17 23:33:46 -07:00
commit 618d0d71be
24 changed files with 864 additions and 789 deletions

154
Cargo.lock generated
View File

@ -15,15 +15,6 @@ dependencies = [
"const-random",
]
[[package]]
name = "aho-corasick"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.9.0"
@ -299,6 +290,7 @@ dependencies = [
"atty",
"bitflags",
"strsim 0.8.0",
"term_size",
"textwrap",
"unicode-width",
"vec_map",
@ -533,18 +525,6 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "docopt"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969"
dependencies = [
"lazy_static",
"regex",
"serde",
"strsim 0.9.3",
]
[[package]]
name = "dtoa"
version = "0.4.4"
@ -1075,6 +1055,19 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexical-core"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890"
dependencies = [
"arrayvec 0.4.12",
"cfg-if",
"rustc_version",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.66"
@ -1258,8 +1251,8 @@ dependencies = [
"lazy_static",
"libc",
"log",
"nom 5.1.1",
"parking_lot",
"regex",
"time 0.1.42",
]
@ -1288,7 +1281,6 @@ dependencies = [
"prettydiff",
"protobuf",
"protobuf-codegen-pure",
"regex",
"ring",
"rusqlite",
"smallvec 1.1.0",
@ -1319,7 +1311,6 @@ dependencies = [
"bytes",
"cstr",
"cursive",
"docopt",
"failure",
"fnv",
"futures",
@ -1338,16 +1329,17 @@ dependencies = [
"moonfire-tflite",
"mylog",
"nix",
"nom 5.1.1",
"parking_lot",
"protobuf",
"reffers",
"regex",
"reqwest",
"ring",
"rusqlite",
"serde",
"serde_json",
"smallvec 1.1.0",
"structopt 0.3.13",
"tempdir",
"time 0.1.42",
"tokio",
@ -1445,6 +1437,17 @@ dependencies = [
"version_check 0.1.5",
]
[[package]]
name = "nom"
version = "5.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6"
dependencies = [
"lexical-core",
"memchr",
"version_check 0.9.1",
]
[[package]]
name = "num"
version = "0.2.1"
@ -1672,7 +1675,7 @@ checksum = "5240be0c9ea1bc7887819a36264cb9475eb71c58749808e5b989c8c1fdc67acf"
dependencies = [
"ansi_term 0.9.0",
"prettytable-rs",
"structopt",
"structopt 0.2.18",
]
[[package]]
@ -1689,6 +1692,32 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "proc-macro-error"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678"
dependencies = [
"proc-macro-error-attr",
"proc-macro2 1.0.8",
"quote 1.0.2",
"syn 1.0.14",
"version_check 0.9.1",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53"
dependencies = [
"proc-macro2 1.0.8",
"quote 1.0.2",
"syn 1.0.14",
"syn-mid",
"version_check 0.9.1",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.11"
@ -1912,18 +1941,6 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "regex"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-automata"
version = "0.1.8"
@ -1933,12 +1950,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "regex-syntax"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90"
[[package]]
name = "remove_dir_all"
version = "0.5.2"
@ -2331,6 +2342,12 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
[[package]]
name = "static_assertions"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
[[package]]
name = "strsim"
version = "0.8.0"
@ -2350,7 +2367,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7"
dependencies = [
"clap",
"structopt-derive",
"structopt-derive 0.2.18",
]
[[package]]
name = "structopt"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff6da2e8d107dfd7b74df5ef4d205c6aebee0706c647f6bc6a2d5789905c00fb"
dependencies = [
"clap",
"lazy_static",
"structopt-derive 0.4.6",
]
[[package]]
@ -2365,6 +2393,19 @@ dependencies = [
"syn 0.15.44",
]
[[package]]
name = "structopt-derive"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a489c87c08fbaf12e386665109dd13470dcc9c4583ea3e10dd2b4523e5ebd9ac"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2 1.0.8",
"quote 1.0.2",
"syn 1.0.14",
]
[[package]]
name = "subtle"
version = "1.0.0"
@ -2393,6 +2434,17 @@ dependencies = [
"unicode-xid 0.2.0",
]
[[package]]
name = "syn-mid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
dependencies = [
"proc-macro2 1.0.8",
"quote 1.0.2",
"syn 1.0.14",
]
[[package]]
name = "synstructure"
version = "0.12.3"
@ -2457,18 +2509,10 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"term_size",
"unicode-width",
]
[[package]]
name = "thread_local"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
dependencies = [
"lazy_static",
]
[[package]]
name = "time"
version = "0.1.42"
@ -2866,7 +2910,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
dependencies = [
"nom",
"nom 4.2.3",
]
[[package]]

View File

@ -28,7 +28,6 @@ byteorder = "1.0"
cstr = "0.1.7"
cursive = "0.14.0"
db = { package = "moonfire-db", path = "db" }
docopt = "1.0"
failure = "0.1.1"
ffmpeg = { package = "moonfire-ffmpeg", git = "https://github.com/scottlamb/moonfire-ffmpeg" }
futures = "0.3"
@ -42,18 +41,19 @@ libc = "0.2"
log = { version = "0.4", features = ["release_max_level_info"] }
memchr = "2.0.2"
memmap = "0.7"
moonfire-tflite = { git = "https://github.com/scottlamb/moonfire-tflite", features = ["edgetpu"], optional = true }
mylog = { git = "https://github.com/scottlamb/mylog" }
nix = "0.16.1"
nom = "5.1.1"
parking_lot = { version = "0.9", features = [] }
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
reffers = "0.6.0"
regex = "1.0"
ring = "0.14.6"
rusqlite = "0.21.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
smallvec = "1.0"
moonfire-tflite = { git = "https://github.com/scottlamb/moonfire-tflite", features = ["edgetpu"], optional = true }
structopt = { version = "0.3.13", features = ["default", "wrap_help"] }
time = "0.1"
tokio = { version = "0.2.0", features = ["blocking", "macros", "rt-threaded", "signal"] }
tokio-tungstenite = "0.10.1"

View File

@ -17,5 +17,5 @@ lazy_static = "1.0"
libc = "0.2"
log = "0.4"
parking_lot = { version = "0.9", features = [] }
regex = "1.0"
nom = "5.1.1"
time = "0.1"

View File

@ -29,6 +29,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
pub mod clock;
pub mod time;
mod error;
pub mod strutil;

View File

@ -28,10 +28,13 @@
// 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 lazy_static::lazy_static;
use regex::Regex;
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 std::fmt::Write as _;
use std::str::FromStr as _;
static MULTIPLIERS: [(char, u64); 4] = [
// (suffix character, power of 2)
@ -58,32 +61,33 @@ pub fn encode_size(mut raw: i64) -> String {
encoded
}
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)),
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"))
)))
)),
|(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)
}
/// Decodes a human-readable size as output by encode_size.
pub fn decode_size(encoded: &str) -> Result<i64, ()> {
let mut decoded = 0i64;
lazy_static! {
static ref RE: Regex = Regex::new(r"\s*([0-9]+)([TGMK])?,?\s*").unwrap();
}
let mut last_pos = 0;
for cap in RE.captures_iter(encoded) {
let whole_cap = cap.get(0).unwrap();
if whole_cap.start() > last_pos {
return Err(());
}
last_pos = whole_cap.end();
let mut piece = i64::from_str(&cap[1]).map_err(|_| ())?;
if let Some(m) = cap.get(2) {
let m = m.as_str().as_bytes()[0] as char;
for &(some_m, n) in &MULTIPLIERS {
if some_m == m {
piece *= 1i64<<n;
break;
}
}
}
decoded += piece;
}
if last_pos < encoded.len() {
let (remaining, decoded) = decode_size_internal(encoded).map_err(|_e| ())?;
if !remaining.is_empty() {
return Err(());
}
Ok(decoded)
@ -130,6 +134,7 @@ mod tests {
#[test]
fn test_decode() {
assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20);
assert_eq!(super::decode_size("100M 42").unwrap(), (100i64 << 20) + 42);
}
#[test]

329
base/time.rs Normal file
View File

@ -0,0 +1,329 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2016-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Time and durations for Moonfire NVR's internal format.
use failure::{Error, bail, format_err};
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::str::FromStr;
use time;
type IResult<'a, I, O> = nom::IResult<I, O, nom::error::VerboseError<&'a str>>;
pub const TIME_UNITS_PER_SEC: i64 = 90_000;
/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
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 Fn(&'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))
}
/// Parses `YYYY-mm-dd` into pieces.
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))
))(input)
}
/// Parses `HH:MM[:SS[:FFFFF]]` into pieces.
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)?;
let (sec, opt_subsec) = stuff.unwrap_or((0, None));
Ok((input, (hr, min, sec, opt_subsec.unwrap_or(0))))
}
/// 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 }
})
))(input)
}
impl Time {
pub fn new(tm: time::Timespec) -> Self {
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()) }
/// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string.
///
/// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds.
///
/// The latter is a date such as `2006-01-02T15:04:05`, followed by an optional 90,000ths of
/// a second such as `:00001`, followed by an optional time zone offset such as `Z` or
/// `-07:00`. A missing fraction is assumed to be 0. A missing time zone offset implies the
/// local time zone.
pub fn parse(input: &str) -> Result<Self, Error> {
// First try parsing as 90,000ths of a second since epoch.
match i64::from_str(input) {
Ok(i) => return Ok(Time(i)),
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))
}
})?;
if remaining != "" {
bail!("unexpected suffix {:?} following time string", remaining);
}
let (tm_hour, tm_min, tm_sec, subsec) = opt_time.unwrap_or((0, 0, 0, 0));
let mut tm = time::Tm {
tm_sec,
tm_min,
tm_hour,
tm_mday,
tm_mon,
tm_year,
tm_wday: 0,
tm_yday: 0,
tm_isdst: -1,
tm_utcoff: 0,
tm_nsec: 0,
};
if tm.tm_mon == 0 {
bail!("time {:?} has month 0", input);
}
tm.tm_mon -= 1;
if tm.tm_year < 1900 {
bail!("time {:?} has year before 1900", input);
}
tm.tm_year -= 1900;
// The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0,
// mktime() otherwise. If a zone is specified, use the timegm path and a manual offset.
// If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the
// chrono crate's lead and just use 0 or 1 to choose between these functions.
let sec = if let Some(off) = opt_zone {
tm.to_timespec().sec + i64::from(off)
} else {
tm.tm_utcoff = 1;
tm.to_timespec().sec
};
Ok(Time(sec * TIME_UNITS_PER_SEC + i64::from(subsec)))
}
/// Convert to unix seconds by floor method (rounding down).
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) }
}
impl ops::Sub for Time {
type Output = Duration;
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 }
}
impl ops::Add<Duration> for Time {
type Output = Time;
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) }
}
impl fmt::Debug for Time {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Write both the raw and display forms.
write!(f, "{} /* {} */", self.0, self)
}
}
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 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)
}
}
/// A duration specified in 1/90,000ths of a second.
/// Durations are typically non-negative, but a `moonfire_db::db::CameraDayValue::duration` may be
/// negative.
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct Duration(pub i64);
impl Duration {
pub fn to_tm_duration(&self) -> time::Duration {
time::Duration::nanoseconds(self.0 * 100000 / 9)
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut seconds = self.0 / TIME_UNITS_PER_SEC;
const MINUTE_IN_SECONDS: i64 = 60;
const HOUR_IN_SECONDS: i64 = 60 * MINUTE_IN_SECONDS;
const DAY_IN_SECONDS: i64 = 24 * HOUR_IN_SECONDS;
let days = seconds / DAY_IN_SECONDS;
seconds %= DAY_IN_SECONDS;
let hours = seconds / HOUR_IN_SECONDS;
seconds %= HOUR_IN_SECONDS;
let minutes = seconds / MINUTE_IN_SECONDS;
seconds %= MINUTE_IN_SECONDS;
let mut have_written = if days > 0 {
write!(f, "{} day{}", days, if days == 1 { "" } else { "s" })?;
true
} else {
false
};
if hours > 0 {
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" })?;
have_written = true;
}
if seconds > 0 || !have_written {
write!(f, "{}{} second{}", if have_written { " " } else { "" },
seconds, if seconds == 1 { "" } else { "s" })?;
}
Ok(())
}
}
impl ops::Add for Duration {
type Output = Duration;
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 }
}
impl ops::SubAssign for Duration {
fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 }
}
#[cfg(test)]
mod tests {
use super::{Duration, Time, TIME_UNITS_PER_SEC};
#[test]
fn test_parse_time() {
std::env::set_var("TZ", "America/Los_Angeles");
time::tzset();
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-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-02Z", 102254400000000),
("102261550050000", 102261550050000),
];
for test in tests {
assert_eq!(test.1, Time::parse(test.0).unwrap().0, "parsing {}", test.0);
}
}
#[test]
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)));
}
#[test]
fn test_display_duration() {
let tests = &[
// (output, seconds)
("0 seconds", 0),
("1 second", 1),
("1 minute", 60),
("1 minute 1 second", 61),
("2 minutes", 120),
("1 hour", 3600),
("1 hour 1 minute", 3660),
("2 hours", 7200),
("1 day", 86400),
("1 day 1 hour", 86400 + 3600),
("2 days", 2 * 86400),
];
for test in tests {
assert_eq!(test.0, format!("{}", Duration(test.1 * TIME_UNITS_PER_SEC)));
}
}
}

View File

@ -31,7 +31,6 @@ odds = { version = "0.3.1", features = ["std-vec"] }
parking_lot = { version = "0.9", features = [] }
prettydiff = "0.3.1"
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
regex = "1.0"
ring = "0.14.6"
rusqlite = "0.21.0"
smallvec = "1.0"

View File

@ -42,6 +42,7 @@ use rusqlite::{Connection, Transaction, params};
use std::collections::BTreeMap;
use std::fmt;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc;
lazy_static! {
@ -57,7 +58,7 @@ pub(crate) fn set_test_config() {
Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2)));
}
enum UserFlags {
enum UserFlag {
Disabled = 1,
}
@ -91,7 +92,7 @@ impl User {
}
pub fn has_password(&self) -> bool { self.password_hash.is_some() }
fn disabled(&self) -> bool { (self.flags & UserFlags::Disabled as i32) != 0 }
fn disabled(&self) -> bool { (self.flags & UserFlag::Disabled as i32) != 0 }
}
/// A change to a user.
@ -132,7 +133,7 @@ impl UserChange {
}
pub fn disable(&mut self) {
self.flags |= UserFlags::Disabled as i32;
self.flags |= UserFlag::Disabled as i32;
}
}
@ -194,13 +195,29 @@ impl rusqlite::types::FromSql for FromSqlIpAddr {
}
}
pub enum SessionFlags {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(i32)]
pub enum SessionFlag {
HttpOnly = 1,
Secure = 2,
SameSite = 4,
SameSiteStrict = 8,
}
impl FromStr for SessionFlag {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"http-only" => Ok(Self::HttpOnly),
"secure" => Ok(Self::Secure),
"same-site" => Ok(Self::SameSite),
"same-site-strict" => Ok(Self::SameSiteStrict),
_ => bail!("No such session flag {:?}", s),
}
}
}
#[derive(Copy, Clone)]
pub enum RevocationReason {
LoggedOut = 1,
@ -210,7 +227,7 @@ pub enum RevocationReason {
#[derive(Debug, Default)]
pub struct Session {
user_id: i32,
flags: i32, // bitmask of SessionFlags enum values
flags: i32, // bitmask of SessionFlag enum values
domain: Option<Vec<u8>>,
description: Option<String>,
seed: Seed,

View File

@ -41,7 +41,7 @@ use log::warn;
use protobuf::Message;
use nix::{NixPath, fcntl::{FlockArg, OFlag}, sys::stat::Mode};
use nix::sys::statvfs::Statvfs;
use std::ffi::{CStr, CString};
use std::ffi::CStr;
use std::fs;
use std::io::{Read, Write};
use std::os::unix::io::{AsRawFd, RawFd};
@ -104,16 +104,14 @@ impl Drop for Fd {
impl Fd {
/// Opens the given path as a directory.
pub fn open(path: &str, mkdir: bool) -> Result<Fd, nix::Error> {
let cstring = CString::new(path).map_err(|_| nix::Error::InvalidPath)?;
pub fn open<P: ?Sized + NixPath>(path: &P, mkdir: bool) -> Result<Fd, nix::Error> {
if mkdir {
match nix::unistd::mkdir(cstring.as_c_str(), nix::sys::stat::Mode::S_IRWXU) {
match nix::unistd::mkdir(path, nix::sys::stat::Mode::S_IRWXU) {
Ok(()) | Err(nix::Error::Sys(nix::errno::Errno::EEXIST)) => {},
Err(e) => return Err(e),
}
}
let fd = nix::fcntl::open(cstring.as_c_str(), OFlag::O_DIRECTORY | OFlag::O_RDONLY,
Mode::empty())?;
let fd = nix::fcntl::open(path, OFlag::O_DIRECTORY | OFlag::O_RDONLY, Mode::empty())?;
Ok(Fd(fd))
}

View File

@ -30,199 +30,17 @@
use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32};
use crate::db;
use failure::{Error, bail, format_err};
use lazy_static::lazy_static;
use failure::{Error, bail};
use log::trace;
use regex::Regex;
use std::ops;
use std::fmt;
use std::ops::Range;
use std::str::FromStr;
use time;
pub const TIME_UNITS_PER_SEC: i64 = 90000;
pub use base::time::TIME_UNITS_PER_SEC;
pub const DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC;
pub const MAX_RECORDING_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC;
/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct Time(pub i64);
impl Time {
pub fn new(tm: time::Timespec) -> Self {
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()) }
/// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string.
///
/// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds.
///
/// The latter is a string such as `2006-01-02T15:04:05`, followed by an optional 90,000ths of
/// a second such as `:00001`, followed by an optional time zone offset such as `Z` or
/// `-07:00`. A missing fraction is assumed to be 0. A missing time zone offset implies the
/// local time zone.
pub fn parse(s: &str) -> Result<Self, Error> {
lazy_static! {
static ref RE: Regex = Regex::new(r#"(?x)
^
([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})
(?::([0-9]{5}))?
(Z|[+-]([0-9]{2}):([0-9]{2}))?
$"#).unwrap();
}
// First try parsing as 90,000ths of a second since epoch.
match i64::from_str(s) {
Ok(i) => return Ok(Time(i)),
Err(_) => {},
}
// If that failed, parse as a time string or bust.
let c = RE.captures(s).ok_or_else(|| format_err!("unparseable time {:?}", s))?;
let mut tm = time::Tm{
tm_sec: i32::from_str(c.get(6).unwrap().as_str()).unwrap(),
tm_min: i32::from_str(c.get(5).unwrap().as_str()).unwrap(),
tm_hour: i32::from_str(c.get(4).unwrap().as_str()).unwrap(),
tm_mday: i32::from_str(c.get(3).unwrap().as_str()).unwrap(),
tm_mon: i32::from_str(c.get(2).unwrap().as_str()).unwrap(),
tm_year: i32::from_str(c.get(1).unwrap().as_str()).unwrap(),
tm_wday: 0,
tm_yday: 0,
tm_isdst: -1,
tm_utcoff: 0,
tm_nsec: 0,
};
if tm.tm_mon == 0 {
bail!("time {:?} has month 0", s);
}
tm.tm_mon -= 1;
if tm.tm_year < 1900 {
bail!("time {:?} has year before 1900", s);
}
tm.tm_year -= 1900;
// The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0,
// mktime() otherwise. If a zone is specified, use the timegm path and a manual offset.
// If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the
// chrono crate's lead and just use 0 or 1 to choose between these functions.
let sec = if let Some(zone) = c.get(8) {
tm.to_timespec().sec + if zone.as_str() == "Z" {
0
} else {
let off = i64::from_str(c.get(9).unwrap().as_str()).unwrap() * 3600 +
i64::from_str(c.get(10).unwrap().as_str()).unwrap() * 60;
if zone.as_str().as_bytes()[0] == b'-' { off } else { -off }
}
} else {
tm.tm_utcoff = 1;
tm.to_timespec().sec
};
let fraction = if let Some(f) = c.get(7) { i64::from_str(f.as_str()).unwrap() } else { 0 };
Ok(Time(sec * TIME_UNITS_PER_SEC + fraction))
}
/// Convert to unix seconds by floor method (rounding down).
pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC }
}
impl ops::Sub for Time {
type Output = Duration;
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 }
}
impl ops::Add<Duration> for Time {
type Output = Time;
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) }
}
impl fmt::Debug for Time {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Write both the raw and display forms.
write!(f, "{} /* {} */", self.0, self)
}
}
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 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)
}
}
/// A duration specified in 1/90,000ths of a second.
/// Durations are typically non-negative, but a `db::CameraDayValue::duration` may be negative.
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct Duration(pub i64);
impl Duration {
pub fn to_tm_duration(&self) -> time::Duration {
time::Duration::nanoseconds(self.0 * 100000 / 9)
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut seconds = self.0 / TIME_UNITS_PER_SEC;
const MINUTE_IN_SECONDS: i64 = 60;
const HOUR_IN_SECONDS: i64 = 60 * MINUTE_IN_SECONDS;
const DAY_IN_SECONDS: i64 = 24 * HOUR_IN_SECONDS;
let days = seconds / DAY_IN_SECONDS;
seconds %= DAY_IN_SECONDS;
let hours = seconds / HOUR_IN_SECONDS;
seconds %= HOUR_IN_SECONDS;
let minutes = seconds / MINUTE_IN_SECONDS;
seconds %= MINUTE_IN_SECONDS;
let mut have_written = if days > 0 {
write!(f, "{} day{}", days, if days == 1 { "" } else { "s" })?;
true
} else {
false
};
if hours > 0 {
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" })?;
have_written = true;
}
if seconds > 0 || !have_written {
write!(f, "{}{} second{}", if have_written { " " } else { "" },
seconds, if seconds == 1 { "" } else { "s" })?;
}
Ok(())
}
}
impl ops::Add for Duration {
type Output = Duration;
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 }
}
impl ops::SubAssign for Duration {
fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 }
}
pub use base::time::Time;
pub use base::time::Duration;
/// An iterator through a sample index.
/// Initially invalid; call `next()` before each read.
@ -533,52 +351,6 @@ mod tests {
use super::*;
use crate::testutil::{self, TestDb};
#[test]
fn test_parse_time() {
testutil::init();
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:05:00001", 102261874050001), // implied -08:00
("2006-01-02T15:04:05-00:00", 102259282050000),
("2006-01-02T15:04:05Z", 102259282050000),
("102261550050000", 102261550050000),
];
for test in tests {
assert_eq!(test.1, Time::parse(test.0).unwrap().0, "parsing {}", test.0);
}
}
#[test]
fn test_format_time() {
testutil::init();
assert_eq!("2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000)));
}
#[test]
fn test_display_duration() {
testutil::init();
let tests = &[
// (output, seconds)
("0 seconds", 0),
("1 second", 1),
("1 minute", 60),
("1 minute 1 second", 61),
("2 minutes", 120),
("1 hour", 3600),
("1 hour 1 minute", 3660),
("2 hours", 7200),
("1 day", 86400),
("1 day 1 hour", 86400 + 3600),
("2 days", 2 * 86400),
];
for test in tests {
assert_eq!(test.0, format!("{}", Duration(test.1 * TIME_UNITS_PER_SEC)));
}
}
/// Tests encoding the example from design/schema.md.
#[test]
fn test_encode_example() {

View File

@ -53,9 +53,9 @@ const UPGRADE_NOTES: &'static str =
#[derive(Debug)]
pub struct Args<'a> {
pub flag_sample_file_dir: Option<&'a str>,
pub flag_preset_journal: &'a str,
pub flag_no_vacuum: bool,
pub sample_file_dir: Option<&'a std::path::Path>,
pub preset_journal: &'a str,
pub no_vacuum: bool,
}
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
@ -88,7 +88,7 @@ fn upgrade(args: &Args, target_ver: i32, conn: &mut rusqlite::Connection) -> Res
bail!("Database is at negative version {}!", old_ver);
}
info!("Upgrading database from version {} to version {}...", old_ver, target_ver);
set_journal_mode(&conn, args.flag_preset_journal)?;
set_journal_mode(&conn, args.preset_journal)?;
for ver in old_ver .. target_ver {
info!("...from version {} to version {}", ver, ver + 1);
let tx = conn.transaction()?;
@ -120,7 +120,7 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> {
// WAL is the preferred journal mode for normal operation; it reduces the number of syncs
// without compromising safety.
set_journal_mode(&conn, "wal")?;
if !args.flag_no_vacuum {
if !args.no_vacuum {
info!("...vacuuming database after upgrade.");
conn.execute_batch(r#"
pragma page_size = 16384;
@ -159,7 +159,7 @@ impl NixPath for UuidPath {
mod tests {
use crate::compare;
use crate::testutil;
use failure::{ResultExt, format_err};
use failure::ResultExt;
use fnv::FnvHashMap;
use super::*;
@ -209,7 +209,7 @@ mod tests {
fn upgrade_and_compare() -> Result<(), Error> {
testutil::init();
let tmpdir = tempdir::TempDir::new("moonfire-nvr-test")?;
let path = tmpdir.path().to_str().ok_or_else(|| format_err!("invalid UTF-8"))?.to_owned();
//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#"
@ -252,9 +252,9 @@ mod tests {
(5, Some(include_str!("v5.sql"))),
(6, Some(include_str!("../schema.sql")))] {
upgrade(&Args {
flag_sample_file_dir: Some(&path),
flag_preset_journal: "delete",
flag_no_vacuum: false,
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)?;

View File

@ -42,7 +42,7 @@ use uuid::Uuid;
pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
let sample_file_path =
args.flag_sample_file_dir
args.sample_file_dir
.ok_or_else(|| format_err!("--sample-file-dir required when upgrading from \
schema version 1 to 2."))?;
@ -122,6 +122,9 @@ 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#"
insert into sample_file_dir (path, uuid, last_complete_open_id)
values (?, ?, ?)
@ -293,7 +296,7 @@ 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: &str, dir: &mut nix::dir::Dir,
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#"
@ -337,7 +340,7 @@ fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir,
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);
bail!("{} is missing from dir {}!", uuid.0, sample_file_path.display());
}
}
}
@ -360,7 +363,7 @@ fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir,
if !files.is_empty() {
bail!("{} unexpected sample file uuids in dir {}: {:?}!",
files.len(), sample_file_path, files);
files.len(), sample_file_path.display(), files);
}
Ok(())
}

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2018 The Moonfire NVR Authors
// Copyright (C) 2018-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -32,36 +32,25 @@
use db::check;
use failure::Error;
use serde::Deserialize;
use std::path::PathBuf;
use structopt::StructOpt;
static USAGE: &'static str = r#"
Checks database integrity.
#[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))]
db_dir: PathBuf,
Usage:
moonfire-nvr check [options]
moonfire-nvr check --help
Options:
--db-dir=DIR Set the directory holding the SQLite3 index database.
This is typically on a flash device.
[default: /var/lib/moonfire-nvr/db]
--compare-lens Compare sample file lengths on disk to the database.
"#;
#[derive(Debug, Deserialize)]
struct Args {
flag_db_dir: String,
flag_compare_lens: bool,
/// Compare sample file lengths on disk to the database.
#[structopt(long)]
compare_lens: bool,
}
pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
pub fn run(args: &Args) -> Result<(), Error> {
// TODO: ReadOnly should be sufficient but seems to fail.
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
check::run(&conn, &check::Options {
compare_lens: args.flag_compare_lens,
compare_lens: args.compare_lens,
})
}

View File

@ -38,36 +38,24 @@ use cursive::Cursive;
use cursive::views;
use db;
use failure::Error;
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
use structopt::StructOpt;
mod cameras;
mod dirs;
mod users;
static USAGE: &'static str = r#"
Interactive configuration editor.
Usage:
moonfire-nvr config [options]
moonfire-nvr config --help
Options:
--db-dir=DIR Set the directory holding the SQLite3 index database.
This is typically on a flash device.
[default: /var/lib/moonfire-nvr/db]
"#;
#[derive(Debug, Deserialize)]
struct Args {
flag_db_dir: String,
#[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))]
db_dir: PathBuf,
}
pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
pub fn run(args: &Args) -> Result<(), Error> {
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
let clocks = clock::RealClocks {};
let db = Arc::new(db::Database::new(clocks, conn, true)?);

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2016 The Moonfire NVR Authors
// Copyright (C) 2016-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -30,31 +30,19 @@
use failure::Error;
use log::info;
use serde::Deserialize;
use structopt::StructOpt;
use std::path::PathBuf;
static USAGE: &'static str = r#"
Initializes a database.
Usage:
moonfire-nvr init [options]
moonfire-nvr init --help
Options:
--db-dir=DIR Set the directory holding the SQLite3 index database.
This is typically on a flash device.
[default: /var/lib/moonfire-nvr/db]
"#;
#[derive(Debug, Deserialize)]
struct Args {
flag_db_dir: String,
#[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))]
db_dir: PathBuf,
}
pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::Create)?;
pub fn run(args: &Args) -> Result<(), Error> {
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::Create)?;
// Check if the database has already been initialized.
let cur_ver = db::get_schema_version(&conn)?;

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2019 The Moonfire NVR Authors
// Copyright (C) 2019-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -31,92 +31,74 @@
//! Subcommand to login a user (without requiring a password).
use base::clock::{self, Clocks};
use db::auth::SessionFlags;
use failure::{Error, ResultExt, bail, format_err};
use serde::Deserialize;
use db::auth::SessionFlag;
use failure::{Error, format_err};
use std::os::unix::fs::OpenOptionsExt as _;
use std::io::Write as _;
use std::path::PathBuf;
use structopt::StructOpt;
static USAGE: &'static str = r#"
Logs in a user, returning the session cookie.
#[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))]
db_dir: PathBuf,
This is a privileged command that directly accesses the database. It doesn't
check the user's password and even can be used to create sessions with
permissions the user doesn't have.
/// Create a session with the given permissions.
///
/// If unspecified, uses user's default permissions.
#[structopt(long, value_name="perms",
parse(try_from_str = protobuf::text_format::parse_from_str))]
permissions: Option<db::Permissions>,
Usage:
/// Restrict this cookie to the given domain.
#[structopt(long)]
domain: Option<String>,
moonfire-nvr login [options] <username>
moonfire-nvr login --help
/// 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")]
curl_cookie_jar: Option<PathBuf>,
Options:
/// Set the given db::auth::SessionFlags.
#[structopt(long, default_value="http-only,secure,same-site,same-site-strict",
value_name="flags", use_delimiter=true)]
session_flags: Vec<SessionFlag>,
--db-dir=DIR Set the directory holding the SQLite3 index database. This
is typically on a flash device.
[default: /var/lib/moonfire-nvr/db]
--permissions=PERMISSIONS
Create a session with the given permissions. If
unspecified, uses user's default permissions.
--domain=DOMAIN The domain this cookie lives on. Optional.
--curl-cookie-jar=FILE
Writes the cookie to a new curl-compatible cookie-jar
file. --domain must be specified. This can be used later
with curl's --cookie flag.
--session-flags=FLAGS
Set the given db::auth::SessionFlags.
[default: http-only,secure,same-site,same-site-strict]
"#;
#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
struct Args {
flag_db_dir: String,
flag_permissions: Option<String>,
flag_domain: Option<String>,
flag_curl_cookie_jar: Option<PathBuf>,
flag_session_flags: String,
arg_username: String,
/// Create the session for this username.
username: String,
}
pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
pub fn run(args: &Args) -> Result<(), Error> {
let clocks = clock::RealClocks {};
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
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.arg_username)
.ok_or_else(|| format_err!("no such user {:?}", &args.arg_username))?;
let permissions = match args.flag_permissions {
None => u.permissions.clone(),
Some(s) => protobuf::text_format::parse_from_str(&s)
.context("unable to parse --permissions")?
};
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 {
when_sec: Some(db.clocks().realtime().sec),
user_agent: None,
addr: None,
};
let mut flags = 0;
for f in args.flag_session_flags.split(',') {
flags |= match f {
"http-only" => SessionFlags::HttpOnly,
"secure" => SessionFlags::Secure,
"same-site" => SessionFlags::SameSite,
"same-site-strict" => SessionFlags::SameSiteStrict,
_ => bail!("unknown session flag {:?}", f),
} as i32;
for f in &args.session_flags {
flags |= *f as i32;
}
let uid = u.id;
drop(u);
let (sid, _) = l.make_session(creation, uid,
args.flag_domain.as_ref().map(|d| d.as_bytes().to_owned()),
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.flag_curl_cookie_jar {
let d = args.flag_domain.as_ref()
if let Some(ref p) = args.curl_cookie_jar {
let d = args.domain.as_ref()
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?;
let mut f = std::fs::OpenOptions::new()
.write(true)
@ -139,11 +121,11 @@ pub fn run() -> Result<(), 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 & SessionFlags::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" },
httponly=if (flags & SessionFlag::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" },
domain=domain,
tailmatch="FALSE",
path="/",
secure=if (flags & SessionFlags::Secure as i32) != 0 { "TRUE" } else { "FALSE" },
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)
@ -153,24 +135,10 @@ fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
mod tests {
use super::*;
#[test]
fn test_args() {
let args: Args = docopt::Docopt::new(USAGE).unwrap()
.argv(&["nvr", "login", "--curl-cookie-jar=foo.txt", "slamb"])
.deserialize().unwrap();
assert_eq!(args, Args {
flag_db_dir: "/var/lib/moonfire-nvr/db".to_owned(),
flag_curl_cookie_jar: Some(PathBuf::from("foo.txt")),
flag_session_flags: "http-only,secure,same-site,same-site-strict".to_owned(),
arg_username: "slamb".to_owned(),
..Default::default()
});
}
#[test]
fn test_curl_cookie() {
assert_eq!(curl_cookie("o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q",
SessionFlags::HttpOnly as i32, "localhost"),
SessionFlag::HttpOnly as i32, "localhost"),
"#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\
o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q");
}

View File

@ -29,48 +29,19 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use db::dir;
use docopt;
use failure::{Error, Fail};
use nix::fcntl::FlockArg;
use rusqlite;
use serde::Deserialize;
use std::path::Path;
mod check;
mod config;
mod login;
mod init;
mod run;
mod sql;
mod ts;
mod upgrade;
#[derive(Debug, Deserialize)]
pub enum Command {
Check,
Config,
Login,
Init,
Run,
Sql,
Ts,
Upgrade,
}
impl Command {
pub fn run(&self) -> Result<(), Error> {
match *self {
Command::Check => check::run(),
Command::Config => config::run(),
Command::Login => login::run(),
Command::Init => init::run(),
Command::Run => run::run(),
Command::Sql => sql::run(),
Command::Ts => ts::run(),
Command::Upgrade => upgrade::run(),
}
}
}
pub mod check;
pub mod config;
pub mod login;
pub mod init;
pub mod run;
pub mod sql;
pub mod ts;
pub mod upgrade;
#[derive(Copy, Clone, PartialEq, Eq)]
enum OpenMode {
@ -81,10 +52,10 @@ enum OpenMode {
/// 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: &str, mode: OpenMode) -> Result<dir::Fd, Error> {
fn open_dir(db_dir: &Path, mode: OpenMode) -> Result<dir::Fd, Error> {
let dir = dir::Fd::open(db_dir, mode == OpenMode::Create)?;
let ro = mode == OpenMode::ReadOnly;
dir.lock(if ro { FlockArg::LockExclusiveNonblock } else { FlockArg::LockSharedNonblock })
dir.lock(if ro { FlockArg::LockSharedNonblock } else { FlockArg::LockExclusiveNonblock })
.map_err(|e| e.context(format!("db dir {:?} already in use; can't get {} lock",
db_dir, if ro { "shared" } else { "exclusive" })))?;
Ok(dir)
@ -92,10 +63,10 @@ fn open_dir(db_dir: &str, mode: OpenMode) -> Result<dir::Fd, Error> {
/// Locks and opens the database.
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is.
fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> {
fn open_conn(db_dir: &Path, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> {
let dir = open_dir(db_dir, mode)?;
let conn = rusqlite::Connection::open_with_flags(
Path::new(&db_dir).join("db"),
db_dir.join("db"),
match mode {
OpenMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
OpenMode::ReadWrite => rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE,
@ -108,9 +79,3 @@ fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connect
rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX)?;
Ok((dir, conn))
}
fn parse_args<'a, T>(usage: &str) -> Result<T, Error> where T: ::serde::Deserialize<'a> {
Ok(docopt::Docopt::new(usage)
.and_then(|d| d.deserialize())
.unwrap_or_else(|e| e.exit()))
}

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2016 The Moonfire NVR Authors
// Copyright (C) 2016-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -33,19 +33,66 @@ use crate::stream;
use crate::streamer;
use crate::web;
use db::{dir, writer};
use failure::{Error, ResultExt, bail};
use failure::{Error, bail};
use fnv::FnvHashMap;
use futures::future::FutureExt;
use hyper::service::{make_service_fn, service_fn};
use log::{info, warn};
use serde::Deserialize;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use structopt::StructOpt;
use tokio;
use tokio::signal::unix::{SignalKind, signal};
#[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))]
db_dir: PathBuf,
/// Directory holding user interface files (.html, .js, etc).
#[structopt(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.
#[structopt(long, default_value = "0.0.0.0:8080", parse(try_from_str))]
http_addr: std::net::SocketAddr,
/// Open the database in read-only mode and disables recording.
///
/// Note this is incompatible with authentication, so you'll likely want to specify
/// --allow_unauthenticated_permissions.
#[structopt(long)]
read_only: bool,
/// Allow unauthenticated access to the web interface, with the given permissions (may be
/// empty). Should be a text Permissions protobuf such as "view_videos: true".
///
/// Note that even an empty string allows some basic access that would be rejected if the
/// argument were omitted.
#[structopt(long, parse(try_from_str = protobuf::text_format::parse_from_str))]
allow_unauthenticated_permissions: Option<db::Permissions>,
/// Trust X-Real-IP: and X-Forwarded-Proto: headers on the incoming request.
///
/// Set this only after ensuring your proxy server is configured to set them and that no
/// untrusted requests bypass the proxy server. You may want to specify
/// --http-addr=127.0.0.1:8080.
#[structopt(long)]
trust_forward_hdrs: bool,
/// Perform object detection on SUB streams.
///
/// Note: requires compilation with --feature=analytics.
#[structopt(long)]
object_detection: bool,
}
// These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles).
// They seem to be correct for Linux and macOS at least.
const LOCALTIME_PATH: &'static str = "/etc/localtime";
@ -55,47 +102,6 @@ const ZONEINFO_PATHS: [&'static str; 2] = [
"/var/db/timezone/zoneinfo/" // macOS High Sierra
];
const USAGE: &'static str = r#"
Usage: moonfire-nvr run [options]
Options:
-h, --help Show this message.
--db-dir=DIR Set the directory holding the SQLite3 index database.
This is typically on a flash device.
[default: /var/lib/moonfire-nvr/db]
--ui-dir=DIR Set the directory with the user interface files
(.html, .js, etc).
[default: /usr/local/lib/moonfire-nvr/ui]
--http-addr=ADDR Set the bind address for the unencrypted HTTP server.
[default: 0.0.0.0:8080]
--read-only Forces read-only mode / disables recording.
--allow-unauthenticated-permissions=PERMISSIONS
Allow unauthenticated access to the web interface,
with the given permissions (may be empty).
PERMISSIONS should be a text Permissions protobuf
such as "view_videos: true". NOTE: even an empty
string allows some basic access that would be
rejected if the argument were omitted.
--trust-forward-hdrs Trust X-Real-IP: and X-Forwarded-Proto: headers on
the incoming request. Set this only after ensuring
your proxy server is configured to set them and that
no untrusted requests bypass the proxy server.
You may want to specify --http-addr=127.0.0.1:8080.
--object-detection Perform object detection on SUB streams.
Note: requires compilation with --feature=analytics.
"#;
#[derive(Debug, Deserialize)]
struct Args {
flag_db_dir: String,
flag_http_addr: String,
flag_ui_dir: String,
flag_read_only: bool,
flag_allow_unauthenticated_permissions: Option<String>,
flag_trust_forward_hdrs: bool,
flag_object_detection: bool,
}
fn trim_zoneinfo(p: &str) -> &str {
for zp in &ZONEINFO_PATHS {
if p.starts_with(zp) {
@ -170,16 +176,15 @@ struct Syncer {
}
#[tokio::main]
pub async fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
pub async fn run(args: &Args) -> Result<(), Error> {
let clocks = clock::RealClocks {};
let (_db_dir, conn) = super::open_conn(
&args.flag_db_dir,
if args.flag_read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?;
let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.flag_read_only).unwrap());
&args.db_dir,
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.");
let object_detector = match args.flag_object_detection {
let object_detector = match args.object_detection {
false => None,
true => Some(crate::analytics::ObjectDetector::new()?),
};
@ -194,22 +199,18 @@ pub async fn run() -> Result<(), Error> {
let time_zone_name = resolve_zone()?;
info!("Resolved timezone: {}", &time_zone_name);
let allow_unauthenticated_permissions = args.flag_allow_unauthenticated_permissions
.map(|s| protobuf::text_format::parse_from_str(&s))
.transpose()
.context("Unable to parse --allow-unauthenticated-permissions")?;
let s = web::Service::new(web::Config {
db: db.clone(),
ui_dir: Some(&args.flag_ui_dir),
allow_unauthenticated_permissions,
trust_forward_hdrs: args.flag_trust_forward_hdrs,
ui_dir: Some(&args.ui_dir),
allow_unauthenticated_permissions: args.allow_unauthenticated_permissions.clone(),
trust_forward_hdrs: args.trust_forward_hdrs,
time_zone_name,
})?;
// Start a streamer for each stream.
let shutdown_streamers = Arc::new(AtomicBool::new(false));
let mut streamers = Vec::new();
let syncers = if !args.flag_read_only {
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());
@ -280,14 +281,13 @@ pub async fn run() -> Result<(), Error> {
} else { None };
// Start the web interface.
let addr = args.flag_http_addr.parse().unwrap();
let make_svc = make_service_fn(move |_conn| {
futures::future::ok::<_, std::convert::Infallible>(service_fn({
let mut s = s.clone();
move |req| Pin::from(s.serve(req))
}))
});
let server = ::hyper::server::Server::bind(&addr)
let server = ::hyper::server::Server::bind(&args.http_addr)
.tcp_nodelay(true)
.serve(make_svc);

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2019 The Moonfire NVR Authors
// Copyright (C) 2019-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -31,45 +31,43 @@
//! Subcommand to run a SQLite shell.
use failure::Error;
use serde::Deserialize;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
use super::OpenMode;
use structopt::StructOpt;
static USAGE: &'static str = r#"
Runs a SQLite shell on the Moonfire NVR database with locking.
#[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))]
db_dir: PathBuf,
Usage:
/// Opens the database in read-only mode and locks it only for shared access.
///
/// This can be run simultaneously with "moonfire-nvr run --read-only".
#[structopt(long)]
read_only: bool,
moonfire-nvr sql [options] [--] [<arg>...]
moonfire-nvr sql --help
Positional arguments will be passed to sqlite3. Use the -- separator to pass
sqlite3 options, as in "moonfire-nvr sql -- -line 'select username from user'".
Options:
--db-dir=DIR Set the directory holding the SQLite3 index database.
This is typically on a flash device.
[default: /var/lib/moonfire-nvr/db]
--read-only Accesses the database in read-only mode.
"#;
#[derive(Debug, Deserialize)]
struct Args {
flag_db_dir: String,
flag_read_only: bool,
arg_arg: Vec<String>,
/// Arguments to pass to sqlite3.
///
/// Use the -- separator to pass sqlite3 options, as in
/// "moonfire-nvr sql -- -line 'select username from user'".
#[structopt(parse(from_os_str))]
arg: Vec<OsString>,
}
pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
let mode = if args.flag_read_only { OpenMode::ReadWrite } else { OpenMode::ReadOnly };
let _db_dir = super::open_dir(&args.flag_db_dir, mode)?;
let mut db = format!("file:{}/db", &args.flag_db_dir);
if args.flag_read_only {
db.push_str("?mode=ro");
pub fn run(args: &Args) -> Result<(), Error> {
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:");
db.push(&args.db_dir);
db.push("/db");
if args.read_only {
db.push("?mode=ro");
}
Command::new("sqlite3").arg(&db).args(&args.arg_arg).status()?;
Command::new("sqlite3").arg(&db).args(&args.arg).status()?;
Ok(())
}

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2016 The Moonfire NVR Authors
// Copyright (C) 2016-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -29,21 +29,22 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use failure::Error;
use serde::Deserialize;
use structopt::StructOpt;
const USAGE: &'static str = r#"
Usage: moonfire-nvr ts <ts>...
moonfire-nvr ts --help
"#;
#[derive(Debug, Deserialize)]
struct Args {
arg_ts: Vec<String>,
#[derive(StructOpt)]
pub struct Args {
/// Timestamp(s) to translate.
///
/// May be either an integer or an RFC-3339-like string:
/// YYYY-mm-dd[THH:MM[:SS[:FFFFF]]][{Z,{+,-,}HH:MM}].
///
/// Eg: 142913484000000, 2020-04-26, 2020-04-26T12:00:00:00000-07:00.
#[structopt(required = true)]
timestamps: Vec<String>,
}
pub fn run() -> Result<(), Error> {
let arg: Args = super::parse_args(&USAGE)?;
for timestamp in &arg.arg_ts {
pub fn run(args: &Args) -> Result<(), Error> {
for timestamp in &args.timestamps {
let t = db::recording::Time::parse(timestamp)?;
println!("{} == {}", t, t.0);
}

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2016 The Moonfire NVR Authors
// Copyright (C) 2016-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -33,45 +33,38 @@
/// See `guide/schema.md` for more information.
use failure::Error;
use serde::Deserialize;
use structopt::StructOpt;
const USAGE: &'static str = r#"
Upgrade to the latest database schema.
Usage: moonfire-nvr upgrade [options]
Options:
-h, --help Show this message.
--db-dir=DIR Set the directory holding the SQLite3 index database.
This is typically on a flash device.
[default: /var/lib/moonfire-nvr/db]
--sample-file-dir=DIR When upgrading from schema version 1 to 2, the sample file directory.
This is typically on a hard drive.
--preset-journal=MODE 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.
[default: delete]
--no-vacuum Skips the normal post-upgrade vacuum operation.
"#;
#[derive(Debug, Deserialize)]
#[derive(StructOpt)]
pub struct Args {
flag_db_dir: String,
flag_sample_file_dir: Option<String>,
flag_preset_journal: String,
flag_no_vacuum: bool,
#[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))]
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")]
preset_journal: String,
#[structopt(help = "Skips the normal post-upgrade vacuum operation.", long)]
no_vacuum: bool,
}
pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
pub fn run(args: &Args) -> Result<(), Error> {
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
db::upgrade::run(&db::upgrade::Args {
flag_sample_file_dir: args.flag_sample_file_dir.as_ref().map(|s| s.as_str()),
flag_preset_journal: &args.flag_preset_journal,
flag_no_vacuum: args.flag_no_vacuum,
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)
}

View File

@ -42,8 +42,6 @@
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use failure::{Error, bail, format_err};
use lazy_static::lazy_static;
use regex::bytes::Regex;
use std::convert::TryFrom;
// See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element categories, and NAL unit
@ -91,15 +89,28 @@ 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>(data: &'a [u8], mut f: F) -> Result<(), Error>
fn decode_h264_annex_b<'a, F>(mut data: &'a [u8], mut f: F) -> Result<(), Error>
where F: FnMut(&'a [u8]) -> Result<(), Error> {
lazy_static! {
static ref START_CODE: Regex = Regex::new(r"(\x00{2,}\x01)").unwrap();
}
for unit in START_CODE.split(data) {
if !unit.is_empty() {
f(unit)?;
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() ..];
// 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(_) => break,
}
}
f(unit)?;
}
// No remaining start codes; likely a unit left.
if !data.is_empty() {
f(data)?;
}
Ok(())
}

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2016 The Moonfire NVR Authors
// Copyright (C) 2016-2020 The Moonfire NVR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@ -31,7 +31,7 @@
#![cfg_attr(all(feature="nightly", test), feature(test))]
use log::{error, info};
use serde::Deserialize;
use structopt::StructOpt;
#[cfg(feature = "analytics")]
mod analytics;
@ -74,39 +74,53 @@ mod stream;
mod streamer;
mod web;
/// Commandline usage string. This is in the particular format expected by the `docopt` crate.
/// Besides being printed on --help or argument parsing error, it's actually parsed to define the
/// allowed commandline arguments and their defaults.
const USAGE: &'static str = "
Usage: moonfire-nvr <command> [<args>...]
moonfire-nvr (--help | --version)
#[derive(StructOpt)]
#[structopt(name="moonfire-nvr", about="security camera network video recorder")]
enum Args {
/// Checks database integrity (like fsck).
Check(cmds::check::Args),
Options:
-h, --help Show this message.
--version Show the version of moonfire-nvr.
/// Interactively edits configuration.
Config(cmds::config::Args),
Commands:
check Check database integrity
init Initialize a database
run Run the daemon: record from cameras and serve HTTP
shell Start an interactive shell to modify the database
ts Translate human-readable and numeric timestamps
upgrade Upgrade the database to the latest schema
";
/// Initializes a database.
Init(cmds::init::Args),
/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate.
#[derive(Debug, Deserialize)]
struct Args {
arg_command: Option<cmds::Command>,
/// Logs in a user, returning the session cookie.
///
/// This is a privileged command that directly accesses the database. It doesn't check the
/// user's password and even can be used to create sessions with permissions the user doesn't
/// have.
Login(cmds::login::Args),
/// Runs the server, saving recordings and allowing web access.
Run(cmds::run::Args),
/// Runs a SQLite3 shell on Moonfire NVR's index database.
///
/// Note this locks the database to prevent simultaneous access with a running server. The
/// server maintains cached state which could be invalidated otherwise.
Sql(cmds::sql::Args),
/// Translates between integer and human-readable timestamps.
Ts(cmds::ts::Args),
/// Upgrades to the latest database schema.
Upgrade(cmds::upgrade::Args),
}
fn version() -> String {
let major = option_env!("CARGO_PKG_VERSION_MAJOR");
let minor = option_env!("CARGO_PKG_VERSION_MAJOR");
let patch = option_env!("CARGO_PKG_VERSION_MAJOR");
match (major, minor, patch) {
(Some(major), Some(minor), Some(patch)) => format!("{}.{}.{}", major, minor, patch),
_ => "".to_owned(),
impl Args {
fn run(&self) -> Result<(), failure::Error> {
match self {
Args::Check(ref a) => cmds::check::run(a),
Args::Config(ref a) => cmds::config::run(a),
Args::Init(ref a) => cmds::init::run(a),
Args::Login(ref a) => cmds::login::run(a),
Args::Run(ref a) => cmds::run::run(a),
Args::Sql(ref a) => cmds::sql::run(a),
Args::Ts(ref a) => cmds::ts::run(a),
Args::Upgrade(ref a) => cmds::upgrade::run(a),
}
}
}
@ -119,14 +133,7 @@ fn parse_fmt<S: AsRef<str>>(fmt: S) -> Option<mylog::Format> {
}
fn main() {
// Parse commandline arguments.
// (Note this differs from cmds::parse_args in that it specifies options_first.)
let args: Args = docopt::Docopt::new(USAGE)
.and_then(|d| d.options_first(true)
.version(Some(version()))
.deserialize())
.unwrap_or_else(|e| e.exit());
let args = Args::from_args();
let mut h = mylog::Builder::new()
.set_format(::std::env::var("MOONFIRE_FORMAT")
.ok()
@ -136,7 +143,7 @@ fn main() {
.build();
h.clone().install().unwrap();
if let Err(e) = { let _a = h.r#async(); args.arg_command.unwrap().run() } {
if let Err(e) = { let _a = h.r#async(); args.run() } {
error!("{:?}", e);
::std::process::exit(1);
}

View File

@ -34,7 +34,6 @@ use bytes::Bytes;
use crate::body::{Body, BoxedError};
use crate::json;
use crate::mp4;
use base64;
use bytes::{BufMut, BytesMut};
use core::borrow::Borrow;
use core::str::FromStr;
@ -46,12 +45,12 @@ use futures::sink::SinkExt;
use futures::future::{self, Future, TryFutureExt};
use futures::stream::StreamExt;
use http::{Request, Response, status::StatusCode};
use http_serve;
use http::header::{self, HeaderValue};
use lazy_static::lazy_static;
use log::{debug, info, warn};
use regex::Regex;
use serde_json;
use nom::IResult;
use nom::bytes::complete::{take_while1, tag};
use nom::combinator::{all_consuming, map, map_res, opt};
use nom::sequence::{preceded, tuple};
use std::collections::HashMap;
use std::cmp;
use std::fs;
@ -64,14 +63,6 @@ use tokio_tungstenite::tungstenite;
use url::form_urlencoded;
use uuid::Uuid;
lazy_static! {
/// Regex used to parse the `s` query parameter to `view.mp4`.
/// As described in `design/api.md`, this is of the form
/// `START_ID[-END_ID][@OPEN_ID][.[REL_START_TIME]-[REL_END_TIME]]`.
static ref SEGMENTS_RE: Regex =
Regex::new(r"^(\d+)(-\d+)?(@\d+)?(?:\.(\d+)?-(\d+)?)?$").unwrap();
}
type BoxedFuture = Box<dyn Future<Output = Result<Response<Body>, BoxedError>> +
Sync + Send + 'static>;
@ -204,41 +195,48 @@ struct Segments {
end_time: Option<i64>,
}
fn num<'a, T: FromStr>() -> impl Fn(&'a str) -> IResult<&'a str, T> {
map_res(take_while1(|c: char| c.is_ascii_digit()), FromStr::from_str)
}
impl Segments {
pub fn parse(input: &str) -> Result<Segments, ()> {
let caps = SEGMENTS_RE.captures(input).ok_or(())?;
let ids_start = i32::from_str(caps.get(1).unwrap().as_str()).map_err(|_| ())?;
let ids_end = match caps.get(2) {
Some(m) => i32::from_str(&m.as_str()[1..]).map_err(|_| ())?,
None => ids_start,
} + 1;
let open_id = match caps.get(3) {
Some(m) => Some(u32::from_str(&m.as_str()[1..]).map_err(|_| ())?),
None => None,
};
if ids_start < 0 || ids_end <= ids_start {
/// Parses the `s` query parameter to `view.mp4` as described in `design/api.md`.
/// Doesn't do any validation.
fn parse(i: &str) -> IResult<&str, Segments> {
// Parse START_ID[-END_ID] into Range<i32>.
// Note that END_ID is inclusive, but Ranges are half-open.
let (i, ids) = map(tuple((num::<i32>(), opt(preceded(tag("-"), num::<i32>())))),
|(start, end)| start .. end.unwrap_or(start) + 1)(i)?;
// Parse [@OPEN_ID] into Option<u32>.
let (i, open_id) = opt(preceded(tag("@"), num::<u32>()))(i)?;
// Parse [.[REL_START_TIME]-[REL_END_TIME]] into (i64, Option<i64>).
let (i, (start_time, end_time)) = map(
opt(preceded(tag("."), tuple((opt(num::<i64>()), tag("-"), opt(num::<i64>()))))),
|t| {
t.map(|(s, _, e)| (s.unwrap_or(0), e))
.unwrap_or((0, None))
})(i)?;
Ok((i, Segments { ids, open_id, start_time, end_time, }))
}
}
impl FromStr for Segments {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (_, s) = all_consuming(Segments::parse)(s).map_err(|_| ())?;
if s.ids.end <= s.ids.start {
return Err(());
}
let start_time = caps.get(4).map_or(Ok(0), |m| i64::from_str(m.as_str())).map_err(|_| ())?;
if start_time < 0 {
return Err(());
if let Some(e) = s.end_time {
if e < s.start_time {
return Err(());
}
}
let end_time = match caps.get(5) {
Some(v) => {
let e = i64::from_str(v.as_str()).map_err(|_| ())?;
if e <= start_time {
return Err(());
}
Some(e)
},
None => None
};
Ok(Segments {
ids: ids_start .. ids_end,
open_id,
start_time,
end_time,
})
Ok(s)
}
}
@ -421,7 +419,7 @@ impl ServiceInner {
let (key, value) = (key.borrow(), value.borrow());
match key {
"s" => {
let s = Segments::parse(value).map_err(
let s = Segments::from_str(value).map_err(
|()| plain_response(StatusCode::BAD_REQUEST,
format!("invalid s parameter: {}", value)))?;
debug!("stream_view_mp4: appending s={:?}", s);
@ -587,10 +585,10 @@ impl ServiceInner {
}.to_owned();
let mut l = self.db.lock();
let is_secure = self.is_secure(req);
let flags = (auth::SessionFlags::HttpOnly as i32) |
(auth::SessionFlags::SameSite as i32) |
(auth::SessionFlags::SameSiteStrict as i32) |
if is_secure { auth::SessionFlags::Secure as i32 } else { 0 };
let flags = (auth::SessionFlag::HttpOnly as i32) |
(auth::SessionFlag::SameSite as i32) |
(auth::SessionFlag::SameSiteStrict as i32) |
if is_secure { auth::SessionFlag::Secure as i32 } else { 0 };
let (sid, _) = l.login_by_password(authreq, &r.username, r.password, Some(domain),
flags)
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
@ -796,7 +794,7 @@ async fn with_json_body(mut req: Request<hyper::Body>)
pub struct Config<'a> {
pub db: Arc<db::Database>,
pub ui_dir: Option<&'a str>,
pub ui_dir: Option<&'a std::path::Path>,
pub trust_forward_hdrs: bool,
pub time_zone_name: String,
pub allow_unauthenticated_permissions: Option<db::Permissions>,
@ -839,12 +837,12 @@ impl Service {
})))
}
fn fill_ui_files(dir: &str, files: &mut HashMap<String, UiFile>) {
fn fill_ui_files(dir: &std::path::Path, files: &mut HashMap<String, UiFile>) {
let r = match fs::read_dir(dir) {
Ok(r) => r,
Err(e) => {
warn!("Unable to search --ui-dir={}; will serve no static files. Error was: {}",
dir, e);
dir.display(), e);
return;
}
};
@ -1075,6 +1073,7 @@ mod tests {
use futures::future::FutureExt;
use log::info;
use std::collections::HashMap;
use std::str::FromStr;
use super::Segments;
struct Server {
@ -1221,25 +1220,25 @@ mod tests {
fn test_segments() {
testutil::init();
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: None},
Segments::parse("1").unwrap());
Segments::from_str("1").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 0, end_time: None},
Segments::parse("1@42").unwrap());
Segments::from_str("1@42").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: None},
Segments::parse("1.26-").unwrap());
Segments::from_str("1.26-").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 26, end_time: None},
Segments::parse("1@42.26-").unwrap());
Segments::from_str("1@42.26-").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: Some(42)},
Segments::parse("1.-42").unwrap());
Segments::from_str("1.-42").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: Some(42)},
Segments::parse("1.26-42").unwrap());
Segments::from_str("1.26-42").unwrap());
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: None},
Segments::parse("1-5").unwrap());
Segments::from_str("1-5").unwrap());
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: None},
Segments::parse("1-5.26-").unwrap());
Segments::from_str("1-5.26-").unwrap());
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: Some(42)},
Segments::parse("1-5.-42").unwrap());
Segments::from_str("1-5.-42").unwrap());
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: Some(42)},
Segments::parse("1-5.26-42").unwrap());
Segments::from_str("1-5.26-42").unwrap());
}
#[tokio::test]