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", "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]] [[package]]
name = "ansi_term" name = "ansi_term"
version = "0.9.0" version = "0.9.0"
@ -299,6 +290,7 @@ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"strsim 0.8.0", "strsim 0.8.0",
"term_size",
"textwrap", "textwrap",
"unicode-width", "unicode-width",
"vec_map", "vec_map",
@ -533,18 +525,6 @@ dependencies = [
"winapi 0.3.8", "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]] [[package]]
name = "dtoa" name = "dtoa"
version = "0.4.4" version = "0.4.4"
@ -1075,6 +1055,19 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 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]] [[package]]
name = "libc" name = "libc"
version = "0.2.66" version = "0.2.66"
@ -1258,8 +1251,8 @@ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
"nom 5.1.1",
"parking_lot", "parking_lot",
"regex",
"time 0.1.42", "time 0.1.42",
] ]
@ -1288,7 +1281,6 @@ dependencies = [
"prettydiff", "prettydiff",
"protobuf", "protobuf",
"protobuf-codegen-pure", "protobuf-codegen-pure",
"regex",
"ring", "ring",
"rusqlite", "rusqlite",
"smallvec 1.1.0", "smallvec 1.1.0",
@ -1319,7 +1311,6 @@ dependencies = [
"bytes", "bytes",
"cstr", "cstr",
"cursive", "cursive",
"docopt",
"failure", "failure",
"fnv", "fnv",
"futures", "futures",
@ -1338,16 +1329,17 @@ dependencies = [
"moonfire-tflite", "moonfire-tflite",
"mylog", "mylog",
"nix", "nix",
"nom 5.1.1",
"parking_lot", "parking_lot",
"protobuf", "protobuf",
"reffers", "reffers",
"regex",
"reqwest", "reqwest",
"ring", "ring",
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",
"smallvec 1.1.0", "smallvec 1.1.0",
"structopt 0.3.13",
"tempdir", "tempdir",
"time 0.1.42", "time 0.1.42",
"tokio", "tokio",
@ -1445,6 +1437,17 @@ dependencies = [
"version_check 0.1.5", "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]] [[package]]
name = "num" name = "num"
version = "0.2.1" version = "0.2.1"
@ -1672,7 +1675,7 @@ checksum = "5240be0c9ea1bc7887819a36264cb9475eb71c58749808e5b989c8c1fdc67acf"
dependencies = [ dependencies = [
"ansi_term 0.9.0", "ansi_term 0.9.0",
"prettytable-rs", "prettytable-rs",
"structopt", "structopt 0.2.18",
] ]
[[package]] [[package]]
@ -1689,6 +1692,32 @@ dependencies = [
"unicode-width", "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]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.11" version = "0.5.11"
@ -1912,18 +1941,6 @@ dependencies = [
"stable_deref_trait", "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]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.1.8" version = "0.1.8"
@ -1933,12 +1950,6 @@ dependencies = [
"byteorder", "byteorder",
] ]
[[package]]
name = "regex-syntax"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.2" version = "0.5.2"
@ -2331,6 +2342,12 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
[[package]]
name = "static_assertions"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@ -2350,7 +2367,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7"
dependencies = [ dependencies = [
"clap", "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]] [[package]]
@ -2365,6 +2393,19 @@ dependencies = [
"syn 0.15.44", "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]] [[package]]
name = "subtle" name = "subtle"
version = "1.0.0" version = "1.0.0"
@ -2393,6 +2434,17 @@ dependencies = [
"unicode-xid 0.2.0", "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]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.12.3" version = "0.12.3"
@ -2457,18 +2509,10 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [ dependencies = [
"term_size",
"unicode-width", "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]] [[package]]
name = "time" name = "time"
version = "0.1.42" version = "0.1.42"
@ -2866,7 +2910,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
dependencies = [ dependencies = [
"nom", "nom 4.2.3",
] ]
[[package]] [[package]]

View File

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

View File

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

View File

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

View File

@ -28,10 +28,13 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
use lazy_static::lazy_static; use nom::IResult;
use regex::Regex; 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::fmt::Write as _;
use std::str::FromStr as _;
static MULTIPLIERS: [(char, u64); 4] = [ static MULTIPLIERS: [(char, u64); 4] = [
// (suffix character, power of 2) // (suffix character, power of 2)
@ -58,32 +61,33 @@ pub fn encode_size(mut raw: i64) -> String {
encoded 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. /// Decodes a human-readable size as output by encode_size.
pub fn decode_size(encoded: &str) -> Result<i64, ()> { pub fn decode_size(encoded: &str) -> Result<i64, ()> {
let mut decoded = 0i64; let (remaining, decoded) = decode_size_internal(encoded).map_err(|_e| ())?;
lazy_static! { if !remaining.is_empty() {
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() {
return Err(()); return Err(());
} }
Ok(decoded) Ok(decoded)
@ -130,6 +134,7 @@ mod tests {
#[test] #[test]
fn test_decode() { fn test_decode() {
assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20); assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20);
assert_eq!(super::decode_size("100M 42").unwrap(), (100i64 << 20) + 42);
} }
#[test] #[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 = [] } parking_lot = { version = "0.9", features = [] }
prettydiff = "0.3.1" prettydiff = "0.3.1"
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" } protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
regex = "1.0"
ring = "0.14.6" ring = "0.14.6"
rusqlite = "0.21.0" rusqlite = "0.21.0"
smallvec = "1.0" smallvec = "1.0"

View File

@ -42,6 +42,7 @@ use rusqlite::{Connection, Transaction, params};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt; use std::fmt;
use std::net::IpAddr; use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
lazy_static! { lazy_static! {
@ -57,7 +58,7 @@ pub(crate) fn set_test_config() {
Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2))); Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2)));
} }
enum UserFlags { enum UserFlag {
Disabled = 1, Disabled = 1,
} }
@ -91,7 +92,7 @@ impl User {
} }
pub fn has_password(&self) -> bool { self.password_hash.is_some() } 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. /// A change to a user.
@ -132,7 +133,7 @@ impl UserChange {
} }
pub fn disable(&mut self) { 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, HttpOnly = 1,
Secure = 2, Secure = 2,
SameSite = 4, SameSite = 4,
SameSiteStrict = 8, 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)] #[derive(Copy, Clone)]
pub enum RevocationReason { pub enum RevocationReason {
LoggedOut = 1, LoggedOut = 1,
@ -210,7 +227,7 @@ pub enum RevocationReason {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Session { pub struct Session {
user_id: i32, user_id: i32,
flags: i32, // bitmask of SessionFlags enum values flags: i32, // bitmask of SessionFlag enum values
domain: Option<Vec<u8>>, domain: Option<Vec<u8>>,
description: Option<String>, description: Option<String>,
seed: Seed, seed: Seed,

View File

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

View File

@ -30,199 +30,17 @@
use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32}; use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32};
use crate::db; use crate::db;
use failure::{Error, bail, format_err}; use failure::{Error, bail};
use lazy_static::lazy_static;
use log::trace; use log::trace;
use regex::Regex;
use std::ops;
use std::fmt;
use std::ops::Range; 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 DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC;
pub const MAX_RECORDING_DURATION: i64 = 5 * 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. pub use base::time::Time;
#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)] pub use base::time::Duration;
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 }
}
/// An iterator through a sample index. /// An iterator through a sample index.
/// Initially invalid; call `next()` before each read. /// Initially invalid; call `next()` before each read.
@ -533,52 +351,6 @@ mod tests {
use super::*; use super::*;
use crate::testutil::{self, TestDb}; 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. /// Tests encoding the example from design/schema.md.
#[test] #[test]
fn test_encode_example() { fn test_encode_example() {

View File

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

View File

@ -42,7 +42,7 @@ use uuid::Uuid;
pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> { pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
let sample_file_path = 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 \ .ok_or_else(|| format_err!("--sample-file-dir required when upgrading from \
schema version 1 to 2."))?; 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)?; 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#" tx.execute(r#"
insert into sample_file_dir (path, uuid, last_complete_open_id) insert into sample_file_dir (path, uuid, last_complete_open_id)
values (?, ?, ?) values (?, ?, ?)
@ -293,7 +296,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
/// * optional: reserved sample file uuids. /// * optional: reserved sample file uuids.
/// * optional: meta and meta-tmp from half-completed update attempts. /// * optional: meta and meta-tmp from half-completed update attempts.
/// * forbidden: anything else. /// * 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> { tx: &rusqlite::Transaction) -> Result<(), Error> {
// Build a hash of the uuids found in the directory. // Build a hash of the uuids found in the directory.
let n: i64 = tx.query_row(r#" 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()? { while let Some(row) = rows.next()? {
let uuid: crate::db::FromSqlUuid = row.get(0)?; let uuid: crate::db::FromSqlUuid = row.get(0)?;
if !files.remove(&uuid.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() { if !files.is_empty() {
bail!("{} unexpected sample file uuids in dir {}: {:?}!", bail!("{} unexpected sample file uuids in dir {}: {:?}!",
files.len(), sample_file_path, files); files.len(), sample_file_path.display(), files);
} }
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -32,36 +32,25 @@
use db::check; use db::check;
use failure::Error; use failure::Error;
use serde::Deserialize; use std::path::PathBuf;
use structopt::StructOpt;
static USAGE: &'static str = r#" #[derive(StructOpt)]
Checks database integrity. 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: /// Compare sample file lengths on disk to the database.
#[structopt(long)]
moonfire-nvr check [options] compare_lens: bool,
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,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
// TODO: ReadOnly should be sufficient but seems to fail. // 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 { 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 cursive::views;
use db; use db;
use failure::Error; use failure::Error;
use serde::Deserialize; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use structopt::StructOpt;
mod cameras; mod cameras;
mod dirs; mod dirs;
mod users; mod users;
static USAGE: &'static str = r#" #[derive(StructOpt)]
Interactive configuration editor. pub struct Args {
/// Directory holding the SQLite3 index database.
Usage: #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
moonfire-nvr config [options] db_dir: PathBuf,
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,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
let clocks = clock::RealClocks {}; let clocks = clock::RealClocks {};
let db = Arc::new(db::Database::new(clocks, conn, true)?); 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. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -30,31 +30,19 @@
use failure::Error; use failure::Error;
use log::info; use log::info;
use serde::Deserialize; use structopt::StructOpt;
use std::path::PathBuf;
static USAGE: &'static str = r#" #[derive(StructOpt)]
Initializes a database. pub struct Args {
/// Directory holding the SQLite3 index database.
Usage: #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
moonfire-nvr init [options] db_dir: PathBuf,
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,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::Create)?;
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::Create)?;
// Check if the database has already been initialized. // Check if the database has already been initialized.
let cur_ver = db::get_schema_version(&conn)?; 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. // 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 // 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 // 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). //! Subcommand to login a user (without requiring a password).
use base::clock::{self, Clocks}; use base::clock::{self, Clocks};
use db::auth::SessionFlags; use db::auth::SessionFlag;
use failure::{Error, ResultExt, bail, format_err}; use failure::{Error, format_err};
use serde::Deserialize;
use std::os::unix::fs::OpenOptionsExt as _; use std::os::unix::fs::OpenOptionsExt as _;
use std::io::Write as _; use std::io::Write as _;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt;
static USAGE: &'static str = r#" #[derive(Debug, Default, StructOpt)]
Logs in a user, returning the session cookie. 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 /// Create a session with the given permissions.
check the user's password and even can be used to create sessions with ///
permissions the user doesn't have. /// 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> /// Write the cookie to a new curl-compatible cookie-jar file.
moonfire-nvr login --help ///
/// ---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 /// Create the session for this username.
is typically on a flash device. username: String,
[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,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
let clocks = clock::RealClocks {}; 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 db = std::sync::Arc::new(db::Database::new(clocks.clone(), conn, true).unwrap());
let mut l = db.lock(); let mut l = db.lock();
let u = l.get_user(&args.arg_username) let u = l.get_user(&args.username)
.ok_or_else(|| format_err!("no such user {:?}", &args.arg_username))?; .ok_or_else(|| format_err!("no such user {:?}", &args.username))?;
let permissions = match args.flag_permissions { let permissions = args.permissions.as_ref().unwrap_or(&u.permissions).clone();
None => u.permissions.clone(),
Some(s) => protobuf::text_format::parse_from_str(&s)
.context("unable to parse --permissions")?
};
let creation = db::auth::Request { let creation = db::auth::Request {
when_sec: Some(db.clocks().realtime().sec), when_sec: Some(db.clocks().realtime().sec),
user_agent: None, user_agent: None,
addr: None, addr: None,
}; };
let mut flags = 0; let mut flags = 0;
for f in args.flag_session_flags.split(',') { for f in &args.session_flags {
flags |= match f { flags |= *f as i32;
"http-only" => SessionFlags::HttpOnly,
"secure" => SessionFlags::Secure,
"same-site" => SessionFlags::SameSite,
"same-site-strict" => SessionFlags::SameSiteStrict,
_ => bail!("unknown session flag {:?}", f),
} as i32;
} }
let uid = u.id; let uid = u.id;
drop(u); drop(u);
let (sid, _) = l.make_session(creation, uid, 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)?; flags, permissions)?;
let mut encoded = [0u8; 64]; let mut encoded = [0u8; 64];
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded); base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8"); let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8");
if let Some(ref p) = args.flag_curl_cookie_jar { if let Some(ref p) = args.curl_cookie_jar {
let d = args.flag_domain.as_ref() let d = args.domain.as_ref()
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?; .ok_or_else(|| format_err!("--cookiejar requires --domain"))?;
let mut f = std::fs::OpenOptions::new() let mut f = std::fs::OpenOptions::new()
.write(true) .write(true)
@ -139,11 +121,11 @@ pub fn run() -> Result<(), Error> {
fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String { 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}", 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, domain=domain,
tailmatch="FALSE", tailmatch="FALSE",
path="/", 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 expires="9223372036854775807", // 64-bit CURL_OFF_T_MAX, never expires
name="s", name="s",
value=cookie) value=cookie)
@ -153,24 +135,10 @@ fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
mod tests { mod tests {
use super::*; 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] #[test]
fn test_curl_cookie() { fn test_curl_cookie() {
assert_eq!(curl_cookie("o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q", 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\ "#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\
o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q"); o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q");
} }

View File

@ -29,48 +29,19 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
use db::dir; use db::dir;
use docopt;
use failure::{Error, Fail}; use failure::{Error, Fail};
use nix::fcntl::FlockArg; use nix::fcntl::FlockArg;
use rusqlite; use rusqlite;
use serde::Deserialize;
use std::path::Path; use std::path::Path;
mod check; pub mod check;
mod config; pub mod config;
mod login; pub mod login;
mod init; pub mod init;
mod run; pub mod run;
mod sql; pub mod sql;
mod ts; pub mod ts;
mod upgrade; pub 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(),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
enum OpenMode { enum OpenMode {
@ -81,10 +52,10 @@ enum OpenMode {
/// Locks the directory without opening the database. /// 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. /// 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 dir = dir::Fd::open(db_dir, mode == OpenMode::Create)?;
let ro = mode == OpenMode::ReadOnly; 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", .map_err(|e| e.context(format!("db dir {:?} already in use; can't get {} lock",
db_dir, if ro { "shared" } else { "exclusive" })))?; db_dir, if ro { "shared" } else { "exclusive" })))?;
Ok(dir) Ok(dir)
@ -92,10 +63,10 @@ fn open_dir(db_dir: &str, mode: OpenMode) -> Result<dir::Fd, Error> {
/// Locks and opens the database. /// Locks and opens the database.
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is. /// 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 dir = open_dir(db_dir, mode)?;
let conn = rusqlite::Connection::open_with_flags( let conn = rusqlite::Connection::open_with_flags(
Path::new(&db_dir).join("db"), db_dir.join("db"),
match mode { match mode {
OpenMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, OpenMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
OpenMode::ReadWrite => rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, 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)?; rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX)?;
Ok((dir, conn)) 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. // 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 // 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 // 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::streamer;
use crate::web; use crate::web;
use db::{dir, writer}; use db::{dir, writer};
use failure::{Error, ResultExt, bail}; use failure::{Error, bail};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use futures::future::FutureExt; use futures::future::FutureExt;
use hyper::service::{make_service_fn, service_fn}; use hyper::service::{make_service_fn, service_fn};
use log::{info, warn}; use log::{info, warn};
use serde::Deserialize; use std::path::PathBuf;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::thread; use std::thread;
use structopt::StructOpt;
use tokio; use tokio;
use tokio::signal::unix::{SignalKind, signal}; 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). // 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. // They seem to be correct for Linux and macOS at least.
const LOCALTIME_PATH: &'static str = "/etc/localtime"; const LOCALTIME_PATH: &'static str = "/etc/localtime";
@ -55,47 +102,6 @@ const ZONEINFO_PATHS: [&'static str; 2] = [
"/var/db/timezone/zoneinfo/" // macOS High Sierra "/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 { fn trim_zoneinfo(p: &str) -> &str {
for zp in &ZONEINFO_PATHS { for zp in &ZONEINFO_PATHS {
if p.starts_with(zp) { if p.starts_with(zp) {
@ -170,16 +176,15 @@ struct Syncer {
} }
#[tokio::main] #[tokio::main]
pub async fn run() -> Result<(), Error> { pub async fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
let clocks = clock::RealClocks {}; let clocks = clock::RealClocks {};
let (_db_dir, conn) = super::open_conn( let (_db_dir, conn) = super::open_conn(
&args.flag_db_dir, &args.db_dir,
if args.flag_read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?; if args.read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?;
let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.flag_read_only).unwrap()); let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.read_only).unwrap());
info!("Database is loaded."); info!("Database is loaded.");
let object_detector = match args.flag_object_detection { let object_detector = match args.object_detection {
false => None, false => None,
true => Some(crate::analytics::ObjectDetector::new()?), true => Some(crate::analytics::ObjectDetector::new()?),
}; };
@ -194,22 +199,18 @@ pub async fn run() -> Result<(), Error> {
let time_zone_name = resolve_zone()?; let time_zone_name = resolve_zone()?;
info!("Resolved timezone: {}", &time_zone_name); 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 { let s = web::Service::new(web::Config {
db: db.clone(), db: db.clone(),
ui_dir: Some(&args.flag_ui_dir), ui_dir: Some(&args.ui_dir),
allow_unauthenticated_permissions, allow_unauthenticated_permissions: args.allow_unauthenticated_permissions.clone(),
trust_forward_hdrs: args.flag_trust_forward_hdrs, trust_forward_hdrs: args.trust_forward_hdrs,
time_zone_name, time_zone_name,
})?; })?;
// Start a streamer for each stream. // Start a streamer for each stream.
let shutdown_streamers = Arc::new(AtomicBool::new(false)); let shutdown_streamers = Arc::new(AtomicBool::new(false));
let mut streamers = Vec::new(); let mut streamers = Vec::new();
let syncers = if !args.flag_read_only { let syncers = if !args.read_only {
let l = db.lock(); let l = db.lock();
let mut dirs = FnvHashMap::with_capacity_and_hasher( let mut dirs = FnvHashMap::with_capacity_and_hasher(
l.sample_file_dirs_by_id().len(), Default::default()); l.sample_file_dirs_by_id().len(), Default::default());
@ -280,14 +281,13 @@ pub async fn run() -> Result<(), Error> {
} else { None }; } else { None };
// Start the web interface. // Start the web interface.
let addr = args.flag_http_addr.parse().unwrap();
let make_svc = make_service_fn(move |_conn| { let make_svc = make_service_fn(move |_conn| {
futures::future::ok::<_, std::convert::Infallible>(service_fn({ futures::future::ok::<_, std::convert::Infallible>(service_fn({
let mut s = s.clone(); let mut s = s.clone();
move |req| Pin::from(s.serve(req)) 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) .tcp_nodelay(true)
.serve(make_svc); .serve(make_svc);

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -31,45 +31,43 @@
//! Subcommand to run a SQLite shell. //! Subcommand to run a SQLite shell.
use failure::Error; use failure::Error;
use serde::Deserialize; use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use super::OpenMode; use super::OpenMode;
use structopt::StructOpt;
static USAGE: &'static str = r#" #[derive(StructOpt)]
Runs a SQLite shell on the Moonfire NVR database with locking. 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>...] /// Arguments to pass to sqlite3.
moonfire-nvr sql --help ///
/// Use the -- separator to pass sqlite3 options, as in
Positional arguments will be passed to sqlite3. Use the -- separator to pass /// "moonfire-nvr sql -- -line 'select username from user'".
sqlite3 options, as in "moonfire-nvr sql -- -line 'select username from user'". #[structopt(parse(from_os_str))]
arg: Vec<OsString>,
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>,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let mode = if args.read_only { OpenMode::ReadOnly } else { OpenMode::ReadWrite };
let _db_dir = super::open_dir(&args.db_dir, mode)?;
let mode = if args.flag_read_only { OpenMode::ReadWrite } else { OpenMode::ReadOnly }; let mut db = OsString::new();
let _db_dir = super::open_dir(&args.flag_db_dir, mode)?; db.push("file:");
let mut db = format!("file:{}/db", &args.flag_db_dir); db.push(&args.db_dir);
if args.flag_read_only { db.push("/db");
db.push_str("?mode=ro"); 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(()) Ok(())
} }

View File

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

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -33,45 +33,38 @@
/// See `guide/schema.md` for more information. /// See `guide/schema.md` for more information.
use failure::Error; use failure::Error;
use serde::Deserialize; use structopt::StructOpt;
const USAGE: &'static str = r#" #[derive(StructOpt)]
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)]
pub struct Args { pub struct Args {
flag_db_dir: String, #[structopt(long,
flag_sample_file_dir: Option<String>, help = "Directory holding the SQLite3 index database.",
flag_preset_journal: String, default_value = "/var/lib/moonfire-nvr/db",
flag_no_vacuum: bool, 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> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
db::upgrade::run(&db::upgrade::Args { db::upgrade::run(&db::upgrade::Args {
flag_sample_file_dir: args.flag_sample_file_dir.as_ref().map(|s| s.as_str()), sample_file_dir: args.sample_file_dir.as_ref().map(std::path::PathBuf::as_path),
flag_preset_journal: &args.flag_preset_journal, preset_journal: &args.preset_journal,
flag_no_vacuum: args.flag_no_vacuum, no_vacuum: args.no_vacuum,
}, &mut conn) }, &mut conn)
} }

View File

@ -42,8 +42,6 @@
use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use failure::{Error, bail, format_err}; use failure::{Error, bail, format_err};
use lazy_static::lazy_static;
use regex::bytes::Regex;
use std::convert::TryFrom; use std::convert::TryFrom;
// See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element categories, and NAL unit // 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 /// 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. /// 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> { where F: FnMut(&'a [u8]) -> Result<(), Error> {
lazy_static! { let start_code = &b"\x00\x00\x01"[..];
static ref START_CODE: Regex = Regex::new(r"(\x00{2,}\x01)").unwrap(); use nom::FindSubstring;
} 'outer: while let Some(pos) = data.find_substring(start_code) {
for unit in START_CODE.split(data) { let mut unit = &data[0..pos];
if !unit.is_empty() { data = &data[pos + start_code.len() ..];
f(unit)?; // 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(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // 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))] #![cfg_attr(all(feature="nightly", test), feature(test))]
use log::{error, info}; use log::{error, info};
use serde::Deserialize; use structopt::StructOpt;
#[cfg(feature = "analytics")] #[cfg(feature = "analytics")]
mod analytics; mod analytics;
@ -74,39 +74,53 @@ mod stream;
mod streamer; mod streamer;
mod web; mod web;
/// Commandline usage string. This is in the particular format expected by the `docopt` crate. #[derive(StructOpt)]
/// Besides being printed on --help or argument parsing error, it's actually parsed to define the #[structopt(name="moonfire-nvr", about="security camera network video recorder")]
/// allowed commandline arguments and their defaults. enum Args {
const USAGE: &'static str = " /// Checks database integrity (like fsck).
Usage: moonfire-nvr <command> [<args>...] Check(cmds::check::Args),
moonfire-nvr (--help | --version)
Options: /// Interactively edits configuration.
-h, --help Show this message. Config(cmds::config::Args),
--version Show the version of moonfire-nvr.
Commands: /// Initializes a database.
check Check database integrity Init(cmds::init::Args),
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
";
/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate. /// Logs in a user, returning the session cookie.
#[derive(Debug, Deserialize)] ///
struct Args { /// This is a privileged command that directly accesses the database. It doesn't check the
arg_command: Option<cmds::Command>, /// 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 { impl Args {
let major = option_env!("CARGO_PKG_VERSION_MAJOR"); fn run(&self) -> Result<(), failure::Error> {
let minor = option_env!("CARGO_PKG_VERSION_MAJOR"); match self {
let patch = option_env!("CARGO_PKG_VERSION_MAJOR"); Args::Check(ref a) => cmds::check::run(a),
match (major, minor, patch) { Args::Config(ref a) => cmds::config::run(a),
(Some(major), Some(minor), Some(patch)) => format!("{}.{}.{}", major, minor, patch), Args::Init(ref a) => cmds::init::run(a),
_ => "".to_owned(), 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() { fn main() {
// Parse commandline arguments. let args = Args::from_args();
// (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 mut h = mylog::Builder::new() let mut h = mylog::Builder::new()
.set_format(::std::env::var("MOONFIRE_FORMAT") .set_format(::std::env::var("MOONFIRE_FORMAT")
.ok() .ok()
@ -136,7 +143,7 @@ fn main() {
.build(); .build();
h.clone().install().unwrap(); 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); error!("{:?}", e);
::std::process::exit(1); ::std::process::exit(1);
} }

View File

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