diff --git a/Cargo.lock b/Cargo.lock
index 3b24c3d..1b9e8a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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]]
diff --git a/Cargo.toml b/Cargo.toml
index 4200e62..8f8da7a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/base/Cargo.toml b/base/Cargo.toml
index 790a6cc..c759c39 100644
--- a/base/Cargo.toml
+++ b/base/Cargo.toml
@@ -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"
diff --git a/base/lib.rs b/base/lib.rs
index be3e65e..c5c38f9 100644
--- a/base/lib.rs
+++ b/base/lib.rs
@@ -29,6 +29,7 @@
// along with this program. If not, see .
pub mod clock;
+pub mod time;
mod error;
pub mod strutil;
diff --git a/base/strutil.rs b/base/strutil.rs
index de48c0b..24ddb90 100644
--- a/base/strutil.rs
+++ b/base/strutil.rs
@@ -28,10 +28,13 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-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 {
- 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<.
+
+//! 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>;
+
+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 {
+ // 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::parse(s) }
+}
+
+impl ops::Sub for Time {
+ type Output = Duration;
+ fn sub(self, rhs: Time) -> Duration { Duration(self.0 - rhs.0) }
+}
+
+impl ops::AddAssign for Time {
+ fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
+}
+
+impl ops::Add for Time {
+ type Output = Time;
+ fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
+}
+
+impl ops::Sub 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)));
+ }
+ }
+}
diff --git a/db/Cargo.toml b/db/Cargo.toml
index 3f39976..df7ac26 100644
--- a/db/Cargo.toml
+++ b/db/Cargo.toml
@@ -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"
diff --git a/db/auth.rs b/db/auth.rs
index 555e2a3..2970674 100644
--- a/db/auth.rs
+++ b/db/auth.rs
@@ -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 {
+ 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>,
description: Option,
seed: Seed,
diff --git a/db/dir.rs b/db/dir.rs
index 0772001..c905e8b 100644
--- a/db/dir.rs
+++ b/db/dir.rs
@@ -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 {
- let cstring = CString::new(path).map_err(|_| nix::Error::InvalidPath)?;
+ pub fn open(path: &P, mkdir: bool) -> Result {
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))
}
diff --git a/db/recording.rs b/db/recording.rs
index efc55bd..c73cafe 100644
--- a/db/recording.rs
+++ b/db/recording.rs
@@ -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 {
- 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 for Time {
- fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
-}
-
-impl ops::Add for Time {
- type Output = Time;
- fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
-}
-
-impl ops::Sub 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() {
diff --git a/db/upgrade/mod.rs b/db/upgrade/mod.rs
index 2a916cd..22cd8b7 100644
--- a/db/upgrade/mod.rs
+++ b/db/upgrade/mod.rs
@@ -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)?;
diff --git a/db/upgrade/v1_to_v2.rs b/db/upgrade/v1_to_v2.rs
index 9cc2649..cc59923 100644
--- a/db/upgrade/v1_to_v2.rs
+++ b/db/upgrade/v1_to_v2.rs
@@ -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(())
}
diff --git a/src/cmds/check.rs b/src/cmds/check.rs
index df64276..3619cba 100644
--- a/src/cmds/check.rs
+++ b/src/cmds/check.rs
@@ -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,
})
}
diff --git a/src/cmds/config/mod.rs b/src/cmds/config/mod.rs
index 1ca54c9..d2296ae 100644
--- a/src/cmds/config/mod.rs
+++ b/src/cmds/config/mod.rs
@@ -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)?);
diff --git a/src/cmds/init.rs b/src/cmds/init.rs
index 1099a47..bc791da 100644
--- a/src/cmds/init.rs
+++ b/src/cmds/init.rs
@@ -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)?;
diff --git a/src/cmds/login.rs b/src/cmds/login.rs
index 93b6d41..46462a0 100644
--- a/src/cmds/login.rs
+++ b/src/cmds/login.rs
@@ -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,
-Usage:
+ /// Restrict this cookie to the given domain.
+ #[structopt(long)]
+ domain: Option,
- moonfire-nvr login [options]
- 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,
-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,
- --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,
- flag_domain: Option,
- flag_curl_cookie_jar: Option,
- 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");
}
diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs
index c2d3840..e635bc5 100644
--- a/src/cmds/mod.rs
+++ b/src/cmds/mod.rs
@@ -29,48 +29,19 @@
// along with this program. If not, see .
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 {
+fn open_dir(db_dir: &Path, mode: OpenMode) -> Result {
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 {
/// 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 where T: ::serde::Deserialize<'a> {
- Ok(docopt::Docopt::new(usage)
- .and_then(|d| d.deserialize())
- .unwrap_or_else(|e| e.exit()))
-}
diff --git a/src/cmds/run.rs b/src/cmds/run.rs
index 22a3b4f..8907d4d 100644
--- a/src/cmds/run.rs
+++ b/src/cmds/run.rs
@@ -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,
+
+ /// 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,
- 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);
diff --git a/src/cmds/sql.rs b/src/cmds/sql.rs
index 8e5cc2c..74e59e5 100644
--- a/src/cmds/sql.rs
+++ b/src/cmds/sql.rs
@@ -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] [--] [...]
- 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,
+ /// 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,
}
-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(())
}
diff --git a/src/cmds/ts.rs b/src/cmds/ts.rs
index a637b86..9625c39 100644
--- a/src/cmds/ts.rs
+++ b/src/cmds/ts.rs
@@ -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 .
use failure::Error;
-use serde::Deserialize;
+use structopt::StructOpt;
-const USAGE: &'static str = r#"
-Usage: moonfire-nvr ts ...
- moonfire-nvr ts --help
-"#;
-
-#[derive(Debug, Deserialize)]
-struct Args {
- arg_ts: Vec,
+#[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,
}
-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);
}
diff --git a/src/cmds/upgrade/mod.rs b/src/cmds/upgrade/mod.rs
index 615ea66..c1505e4 100644
--- a/src/cmds/upgrade/mod.rs
+++ b/src/cmds/upgrade/mod.rs
@@ -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,
- 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,
+
+ #[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)
}
diff --git a/src/h264.rs b/src/h264.rs
index 538c62f..7e1ce52 100644
--- a/src/h264.rs
+++ b/src/h264.rs
@@ -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(())
}
diff --git a/src/main.rs b/src/main.rs
index b2ba361..7493b3b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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 [...]
- 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,
+ /// 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>(fmt: S) -> Option {
}
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);
}
diff --git a/src/web.rs b/src/web.rs
index 288f246..f668074 100644
--- a/src/web.rs
+++ b/src/web.rs
@@ -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, BoxedError>> +
Sync + Send + 'static>;
@@ -204,41 +195,48 @@ struct Segments {
end_time: Option,
}
+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 {
- 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.
+ // Note that END_ID is inclusive, but Ranges are half-open.
+ let (i, ids) = map(tuple((num::(), opt(preceded(tag("-"), num::())))),
+ |(start, end)| start .. end.unwrap_or(start) + 1)(i)?;
+
+ // Parse [@OPEN_ID] into Option.
+ let (i, open_id) = opt(preceded(tag("@"), num::()))(i)?;
+
+ // Parse [.[REL_START_TIME]-[REL_END_TIME]] into (i64, Option).
+ let (i, (start_time, end_time)) = map(
+ opt(preceded(tag("."), tuple((opt(num::()), tag("-"), opt(num::()))))),
+ |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 {
+ 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)
pub struct Config<'a> {
pub db: Arc,
- 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,
@@ -839,12 +837,12 @@ impl Service {
})))
}
- fn fill_ui_files(dir: &str, files: &mut HashMap) {
+ fn fill_ui_files(dir: &std::path::Path, files: &mut HashMap) {
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]