mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-02-03 01:46:01 -05:00
Merge branch 'master' into new-schema
This commit is contained in:
commit
618d0d71be
154
Cargo.lock
generated
154
Cargo.lock
generated
@ -15,15 +15,6 @@ dependencies = [
|
||||
"const-random",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.9.0"
|
||||
@ -299,6 +290,7 @@ dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"strsim 0.8.0",
|
||||
"term_size",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
@ -533,18 +525,6 @@ dependencies = [
|
||||
"winapi 0.3.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docopt"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
"strsim 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.4"
|
||||
@ -1075,6 +1055,19 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lexical-core"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890"
|
||||
dependencies = [
|
||||
"arrayvec 0.4.12",
|
||||
"cfg-if",
|
||||
"rustc_version",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.66"
|
||||
@ -1258,8 +1251,8 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"nom 5.1.1",
|
||||
"parking_lot",
|
||||
"regex",
|
||||
"time 0.1.42",
|
||||
]
|
||||
|
||||
@ -1288,7 +1281,6 @@ dependencies = [
|
||||
"prettydiff",
|
||||
"protobuf",
|
||||
"protobuf-codegen-pure",
|
||||
"regex",
|
||||
"ring",
|
||||
"rusqlite",
|
||||
"smallvec 1.1.0",
|
||||
@ -1319,7 +1311,6 @@ dependencies = [
|
||||
"bytes",
|
||||
"cstr",
|
||||
"cursive",
|
||||
"docopt",
|
||||
"failure",
|
||||
"fnv",
|
||||
"futures",
|
||||
@ -1338,16 +1329,17 @@ dependencies = [
|
||||
"moonfire-tflite",
|
||||
"mylog",
|
||||
"nix",
|
||||
"nom 5.1.1",
|
||||
"parking_lot",
|
||||
"protobuf",
|
||||
"reffers",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec 1.1.0",
|
||||
"structopt 0.3.13",
|
||||
"tempdir",
|
||||
"time 0.1.42",
|
||||
"tokio",
|
||||
@ -1445,6 +1437,17 @@ dependencies = [
|
||||
"version_check 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6"
|
||||
dependencies = [
|
||||
"lexical-core",
|
||||
"memchr",
|
||||
"version_check 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.2.1"
|
||||
@ -1672,7 +1675,7 @@ checksum = "5240be0c9ea1bc7887819a36264cb9475eb71c58749808e5b989c8c1fdc67acf"
|
||||
dependencies = [
|
||||
"ansi_term 0.9.0",
|
||||
"prettytable-rs",
|
||||
"structopt",
|
||||
"structopt 0.2.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1689,6 +1692,32 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2 1.0.8",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.14",
|
||||
"version_check 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.8",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.14",
|
||||
"syn-mid",
|
||||
"version_check 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.11"
|
||||
@ -1912,18 +1941,6 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.8"
|
||||
@ -1933,12 +1950,6 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.2"
|
||||
@ -2331,6 +2342,12 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
@ -2350,7 +2367,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"structopt-derive",
|
||||
"structopt-derive 0.2.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6da2e8d107dfd7b74df5ef4d205c6aebee0706c647f6bc6a2d5789905c00fb"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"lazy_static",
|
||||
"structopt-derive 0.4.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2365,6 +2393,19 @@ dependencies = [
|
||||
"syn 0.15.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt-derive"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a489c87c08fbaf12e386665109dd13470dcc9c4583ea3e10dd2b4523e5ebd9ac"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2 1.0.8",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "1.0.0"
|
||||
@ -2393,6 +2434,17 @@ dependencies = [
|
||||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn-mid"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.8",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.12.3"
|
||||
@ -2457,18 +2509,10 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"term_size",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.42"
|
||||
@ -2866,7 +2910,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"nom 4.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -28,7 +28,6 @@ byteorder = "1.0"
|
||||
cstr = "0.1.7"
|
||||
cursive = "0.14.0"
|
||||
db = { package = "moonfire-db", path = "db" }
|
||||
docopt = "1.0"
|
||||
failure = "0.1.1"
|
||||
ffmpeg = { package = "moonfire-ffmpeg", git = "https://github.com/scottlamb/moonfire-ffmpeg" }
|
||||
futures = "0.3"
|
||||
@ -42,18 +41,19 @@ libc = "0.2"
|
||||
log = { version = "0.4", features = ["release_max_level_info"] }
|
||||
memchr = "2.0.2"
|
||||
memmap = "0.7"
|
||||
moonfire-tflite = { git = "https://github.com/scottlamb/moonfire-tflite", features = ["edgetpu"], optional = true }
|
||||
mylog = { git = "https://github.com/scottlamb/mylog" }
|
||||
nix = "0.16.1"
|
||||
nom = "5.1.1"
|
||||
parking_lot = { version = "0.9", features = [] }
|
||||
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
|
||||
reffers = "0.6.0"
|
||||
regex = "1.0"
|
||||
ring = "0.14.6"
|
||||
rusqlite = "0.21.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
smallvec = "1.0"
|
||||
moonfire-tflite = { git = "https://github.com/scottlamb/moonfire-tflite", features = ["edgetpu"], optional = true }
|
||||
structopt = { version = "0.3.13", features = ["default", "wrap_help"] }
|
||||
time = "0.1"
|
||||
tokio = { version = "0.2.0", features = ["blocking", "macros", "rt-threaded", "signal"] }
|
||||
tokio-tungstenite = "0.10.1"
|
||||
|
@ -17,5 +17,5 @@ lazy_static = "1.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
parking_lot = { version = "0.9", features = [] }
|
||||
regex = "1.0"
|
||||
nom = "5.1.1"
|
||||
time = "0.1"
|
||||
|
@ -29,6 +29,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
pub mod clock;
|
||||
pub mod time;
|
||||
mod error;
|
||||
pub mod strutil;
|
||||
|
||||
|
@ -28,10 +28,13 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use nom::IResult;
|
||||
use nom::branch::alt;
|
||||
use nom::bytes::complete::{tag, take_while1};
|
||||
use nom::character::complete::space0;
|
||||
use nom::combinator::{map, map_res, opt};
|
||||
use nom::sequence::{delimited, tuple};
|
||||
use std::fmt::Write as _;
|
||||
use std::str::FromStr as _;
|
||||
|
||||
static MULTIPLIERS: [(char, u64); 4] = [
|
||||
// (suffix character, power of 2)
|
||||
@ -58,32 +61,33 @@ pub fn encode_size(mut raw: i64) -> String {
|
||||
encoded
|
||||
}
|
||||
|
||||
fn decode_sizepart(input: &str) -> IResult<&str, i64> {
|
||||
map(
|
||||
tuple((
|
||||
map_res(take_while1(|c: char| c.is_ascii_digit()),
|
||||
|input: &str| i64::from_str_radix(input, 10)),
|
||||
opt(alt((
|
||||
nom::combinator::value(1<<40, tag("T")),
|
||||
nom::combinator::value(1<<30, tag("G")),
|
||||
nom::combinator::value(1<<20, tag("M")),
|
||||
nom::combinator::value(1<<10, tag("K"))
|
||||
)))
|
||||
)),
|
||||
|(n, opt_unit)| n * opt_unit.unwrap_or(1)
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn decode_size_internal(input: &str) -> IResult<&str, i64> {
|
||||
nom::multi::fold_many1(
|
||||
delimited(space0, decode_sizepart, space0),
|
||||
0,
|
||||
|sum, i| sum + i)(input)
|
||||
}
|
||||
|
||||
/// Decodes a human-readable size as output by encode_size.
|
||||
pub fn decode_size(encoded: &str) -> Result<i64, ()> {
|
||||
let mut decoded = 0i64;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"\s*([0-9]+)([TGMK])?,?\s*").unwrap();
|
||||
}
|
||||
let mut last_pos = 0;
|
||||
for cap in RE.captures_iter(encoded) {
|
||||
let whole_cap = cap.get(0).unwrap();
|
||||
if whole_cap.start() > last_pos {
|
||||
return Err(());
|
||||
}
|
||||
last_pos = whole_cap.end();
|
||||
let mut piece = i64::from_str(&cap[1]).map_err(|_| ())?;
|
||||
if let Some(m) = cap.get(2) {
|
||||
let m = m.as_str().as_bytes()[0] as char;
|
||||
for &(some_m, n) in &MULTIPLIERS {
|
||||
if some_m == m {
|
||||
piece *= 1i64<<n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
decoded += piece;
|
||||
}
|
||||
if last_pos < encoded.len() {
|
||||
let (remaining, decoded) = decode_size_internal(encoded).map_err(|_e| ())?;
|
||||
if !remaining.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
Ok(decoded)
|
||||
@ -130,6 +134,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20);
|
||||
assert_eq!(super::decode_size("100M 42").unwrap(), (100i64 << 20) + 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
329
base/time.rs
Normal file
329
base/time.rs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
@ -31,7 +31,6 @@ odds = { version = "0.3.1", features = ["std-vec"] }
|
||||
parking_lot = { version = "0.9", features = [] }
|
||||
prettydiff = "0.3.1"
|
||||
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
|
||||
regex = "1.0"
|
||||
ring = "0.14.6"
|
||||
rusqlite = "0.21.0"
|
||||
smallvec = "1.0"
|
||||
|
27
db/auth.rs
27
db/auth.rs
@ -42,6 +42,7 @@ use rusqlite::{Connection, Transaction, params};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
lazy_static! {
|
||||
@ -57,7 +58,7 @@ pub(crate) fn set_test_config() {
|
||||
Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2)));
|
||||
}
|
||||
|
||||
enum UserFlags {
|
||||
enum UserFlag {
|
||||
Disabled = 1,
|
||||
}
|
||||
|
||||
@ -91,7 +92,7 @@ impl User {
|
||||
}
|
||||
|
||||
pub fn has_password(&self) -> bool { self.password_hash.is_some() }
|
||||
fn disabled(&self) -> bool { (self.flags & UserFlags::Disabled as i32) != 0 }
|
||||
fn disabled(&self) -> bool { (self.flags & UserFlag::Disabled as i32) != 0 }
|
||||
}
|
||||
|
||||
/// A change to a user.
|
||||
@ -132,7 +133,7 @@ impl UserChange {
|
||||
}
|
||||
|
||||
pub fn disable(&mut self) {
|
||||
self.flags |= UserFlags::Disabled as i32;
|
||||
self.flags |= UserFlag::Disabled as i32;
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,13 +195,29 @@ impl rusqlite::types::FromSql for FromSqlIpAddr {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SessionFlags {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum SessionFlag {
|
||||
HttpOnly = 1,
|
||||
Secure = 2,
|
||||
SameSite = 4,
|
||||
SameSiteStrict = 8,
|
||||
}
|
||||
|
||||
impl FromStr for SessionFlag {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"http-only" => Ok(Self::HttpOnly),
|
||||
"secure" => Ok(Self::Secure),
|
||||
"same-site" => Ok(Self::SameSite),
|
||||
"same-site-strict" => Ok(Self::SameSiteStrict),
|
||||
_ => bail!("No such session flag {:?}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum RevocationReason {
|
||||
LoggedOut = 1,
|
||||
@ -210,7 +227,7 @@ pub enum RevocationReason {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Session {
|
||||
user_id: i32,
|
||||
flags: i32, // bitmask of SessionFlags enum values
|
||||
flags: i32, // bitmask of SessionFlag enum values
|
||||
domain: Option<Vec<u8>>,
|
||||
description: Option<String>,
|
||||
seed: Seed,
|
||||
|
10
db/dir.rs
10
db/dir.rs
@ -41,7 +41,7 @@ use log::warn;
|
||||
use protobuf::Message;
|
||||
use nix::{NixPath, fcntl::{FlockArg, OFlag}, sys::stat::Mode};
|
||||
use nix::sys::statvfs::Statvfs;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ffi::CStr;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
@ -104,16 +104,14 @@ impl Drop for Fd {
|
||||
|
||||
impl Fd {
|
||||
/// Opens the given path as a directory.
|
||||
pub fn open(path: &str, mkdir: bool) -> Result<Fd, nix::Error> {
|
||||
let cstring = CString::new(path).map_err(|_| nix::Error::InvalidPath)?;
|
||||
pub fn open<P: ?Sized + NixPath>(path: &P, mkdir: bool) -> Result<Fd, nix::Error> {
|
||||
if mkdir {
|
||||
match nix::unistd::mkdir(cstring.as_c_str(), nix::sys::stat::Mode::S_IRWXU) {
|
||||
match nix::unistd::mkdir(path, nix::sys::stat::Mode::S_IRWXU) {
|
||||
Ok(()) | Err(nix::Error::Sys(nix::errno::Errno::EEXIST)) => {},
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
let fd = nix::fcntl::open(cstring.as_c_str(), OFlag::O_DIRECTORY | OFlag::O_RDONLY,
|
||||
Mode::empty())?;
|
||||
let fd = nix::fcntl::open(path, OFlag::O_DIRECTORY | OFlag::O_RDONLY, Mode::empty())?;
|
||||
Ok(Fd(fd))
|
||||
}
|
||||
|
||||
|
238
db/recording.rs
238
db/recording.rs
@ -30,199 +30,17 @@
|
||||
|
||||
use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32};
|
||||
use crate::db;
|
||||
use failure::{Error, bail, format_err};
|
||||
use lazy_static::lazy_static;
|
||||
use failure::{Error, bail};
|
||||
use log::trace;
|
||||
use regex::Regex;
|
||||
use std::ops;
|
||||
use std::fmt;
|
||||
use std::ops::Range;
|
||||
use std::str::FromStr;
|
||||
use time;
|
||||
|
||||
pub const TIME_UNITS_PER_SEC: i64 = 90000;
|
||||
pub use base::time::TIME_UNITS_PER_SEC;
|
||||
|
||||
pub const DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC;
|
||||
pub const MAX_RECORDING_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC;
|
||||
|
||||
/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
|
||||
#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Time(pub i64);
|
||||
|
||||
impl Time {
|
||||
pub fn new(tm: time::Timespec) -> Self {
|
||||
Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000)
|
||||
}
|
||||
|
||||
pub const fn min_value() -> Self { Time(i64::min_value()) }
|
||||
pub const fn max_value() -> Self { Time(i64::max_value()) }
|
||||
|
||||
/// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string.
|
||||
///
|
||||
/// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds.
|
||||
///
|
||||
/// The latter is a string such as `2006-01-02T15:04:05`, followed by an optional 90,000ths of
|
||||
/// a second such as `:00001`, followed by an optional time zone offset such as `Z` or
|
||||
/// `-07:00`. A missing fraction is assumed to be 0. A missing time zone offset implies the
|
||||
/// local time zone.
|
||||
pub fn parse(s: &str) -> Result<Self, Error> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r#"(?x)
|
||||
^
|
||||
([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})
|
||||
(?::([0-9]{5}))?
|
||||
(Z|[+-]([0-9]{2}):([0-9]{2}))?
|
||||
$"#).unwrap();
|
||||
}
|
||||
|
||||
// First try parsing as 90,000ths of a second since epoch.
|
||||
match i64::from_str(s) {
|
||||
Ok(i) => return Ok(Time(i)),
|
||||
Err(_) => {},
|
||||
}
|
||||
|
||||
// If that failed, parse as a time string or bust.
|
||||
let c = RE.captures(s).ok_or_else(|| format_err!("unparseable time {:?}", s))?;
|
||||
let mut tm = time::Tm{
|
||||
tm_sec: i32::from_str(c.get(6).unwrap().as_str()).unwrap(),
|
||||
tm_min: i32::from_str(c.get(5).unwrap().as_str()).unwrap(),
|
||||
tm_hour: i32::from_str(c.get(4).unwrap().as_str()).unwrap(),
|
||||
tm_mday: i32::from_str(c.get(3).unwrap().as_str()).unwrap(),
|
||||
tm_mon: i32::from_str(c.get(2).unwrap().as_str()).unwrap(),
|
||||
tm_year: i32::from_str(c.get(1).unwrap().as_str()).unwrap(),
|
||||
tm_wday: 0,
|
||||
tm_yday: 0,
|
||||
tm_isdst: -1,
|
||||
tm_utcoff: 0,
|
||||
tm_nsec: 0,
|
||||
};
|
||||
if tm.tm_mon == 0 {
|
||||
bail!("time {:?} has month 0", s);
|
||||
}
|
||||
tm.tm_mon -= 1;
|
||||
if tm.tm_year < 1900 {
|
||||
bail!("time {:?} has year before 1900", s);
|
||||
}
|
||||
tm.tm_year -= 1900;
|
||||
|
||||
// The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0,
|
||||
// mktime() otherwise. If a zone is specified, use the timegm path and a manual offset.
|
||||
// If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the
|
||||
// chrono crate's lead and just use 0 or 1 to choose between these functions.
|
||||
let sec = if let Some(zone) = c.get(8) {
|
||||
tm.to_timespec().sec + if zone.as_str() == "Z" {
|
||||
0
|
||||
} else {
|
||||
let off = i64::from_str(c.get(9).unwrap().as_str()).unwrap() * 3600 +
|
||||
i64::from_str(c.get(10).unwrap().as_str()).unwrap() * 60;
|
||||
if zone.as_str().as_bytes()[0] == b'-' { off } else { -off }
|
||||
}
|
||||
} else {
|
||||
tm.tm_utcoff = 1;
|
||||
tm.to_timespec().sec
|
||||
};
|
||||
let fraction = if let Some(f) = c.get(7) { i64::from_str(f.as_str()).unwrap() } else { 0 };
|
||||
Ok(Time(sec * TIME_UNITS_PER_SEC + fraction))
|
||||
}
|
||||
|
||||
/// Convert to unix seconds by floor method (rounding down).
|
||||
pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC }
|
||||
}
|
||||
|
||||
impl ops::Sub for Time {
|
||||
type Output = Duration;
|
||||
fn sub(self, rhs: Time) -> Duration { Duration(self.0 - rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::AddAssign<Duration> for Time {
|
||||
fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
|
||||
}
|
||||
|
||||
impl ops::Add<Duration> for Time {
|
||||
type Output = Time;
|
||||
fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub<Duration> for Time {
|
||||
type Output = Time;
|
||||
fn sub(self, rhs: Duration) -> Time { Time(self.0 - rhs.0) }
|
||||
}
|
||||
|
||||
impl fmt::Debug for Time {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// Write both the raw and display forms.
|
||||
write!(f, "{} /* {} */", self.0, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Time {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let tm = time::at(time::Timespec{sec: self.0 / TIME_UNITS_PER_SEC, nsec: 0});
|
||||
let zone_minutes = tm.tm_utcoff.abs() / 60;
|
||||
write!(f, "{}:{:05}{}{:02}:{:02}", tm.strftime("%FT%T").or_else(|_| Err(fmt::Error))?,
|
||||
self.0 % TIME_UNITS_PER_SEC,
|
||||
if tm.tm_utcoff > 0 { '+' } else { '-' }, zone_minutes / 60, zone_minutes % 60)
|
||||
}
|
||||
}
|
||||
|
||||
/// A duration specified in 1/90,000ths of a second.
|
||||
/// Durations are typically non-negative, but a `db::CameraDayValue::duration` may be negative.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Duration(pub i64);
|
||||
|
||||
impl Duration {
|
||||
pub fn to_tm_duration(&self) -> time::Duration {
|
||||
time::Duration::nanoseconds(self.0 * 100000 / 9)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Duration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut seconds = self.0 / TIME_UNITS_PER_SEC;
|
||||
const MINUTE_IN_SECONDS: i64 = 60;
|
||||
const HOUR_IN_SECONDS: i64 = 60 * MINUTE_IN_SECONDS;
|
||||
const DAY_IN_SECONDS: i64 = 24 * HOUR_IN_SECONDS;
|
||||
let days = seconds / DAY_IN_SECONDS;
|
||||
seconds %= DAY_IN_SECONDS;
|
||||
let hours = seconds / HOUR_IN_SECONDS;
|
||||
seconds %= HOUR_IN_SECONDS;
|
||||
let minutes = seconds / MINUTE_IN_SECONDS;
|
||||
seconds %= MINUTE_IN_SECONDS;
|
||||
let mut have_written = if days > 0 {
|
||||
write!(f, "{} day{}", days, if days == 1 { "" } else { "s" })?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if hours > 0 {
|
||||
write!(f, "{}{} hour{}", if have_written { " " } else { "" },
|
||||
hours, if hours == 1 { "" } else { "s" })?;
|
||||
have_written = true;
|
||||
}
|
||||
if minutes > 0 {
|
||||
write!(f, "{}{} minute{}", if have_written { " " } else { "" },
|
||||
minutes, if minutes == 1 { "" } else { "s" })?;
|
||||
have_written = true;
|
||||
}
|
||||
if seconds > 0 || !have_written {
|
||||
write!(f, "{}{} second{}", if have_written { " " } else { "" },
|
||||
seconds, if seconds == 1 { "" } else { "s" })?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Duration {
|
||||
type Output = Duration;
|
||||
fn add(self, rhs: Duration) -> Duration { Duration(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::AddAssign for Duration {
|
||||
fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
|
||||
}
|
||||
|
||||
impl ops::SubAssign for Duration {
|
||||
fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 }
|
||||
}
|
||||
pub use base::time::Time;
|
||||
pub use base::time::Duration;
|
||||
|
||||
/// An iterator through a sample index.
|
||||
/// Initially invalid; call `next()` before each read.
|
||||
@ -533,52 +351,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::testutil::{self, TestDb};
|
||||
|
||||
#[test]
|
||||
fn test_parse_time() {
|
||||
testutil::init();
|
||||
let tests = &[
|
||||
("2006-01-02T15:04:05-07:00", 102261550050000),
|
||||
("2006-01-02T15:04:05:00001-07:00", 102261550050001),
|
||||
("2006-01-02T15:04:05-08:00", 102261874050000),
|
||||
("2006-01-02T15:04:05", 102261874050000), // implied -08:00
|
||||
("2006-01-02T15:04:05:00001", 102261874050001), // implied -08:00
|
||||
("2006-01-02T15:04:05-00:00", 102259282050000),
|
||||
("2006-01-02T15:04:05Z", 102259282050000),
|
||||
("102261550050000", 102261550050000),
|
||||
];
|
||||
for test in tests {
|
||||
assert_eq!(test.1, Time::parse(test.0).unwrap().0, "parsing {}", test.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_time() {
|
||||
testutil::init();
|
||||
assert_eq!("2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_duration() {
|
||||
testutil::init();
|
||||
let tests = &[
|
||||
// (output, seconds)
|
||||
("0 seconds", 0),
|
||||
("1 second", 1),
|
||||
("1 minute", 60),
|
||||
("1 minute 1 second", 61),
|
||||
("2 minutes", 120),
|
||||
("1 hour", 3600),
|
||||
("1 hour 1 minute", 3660),
|
||||
("2 hours", 7200),
|
||||
("1 day", 86400),
|
||||
("1 day 1 hour", 86400 + 3600),
|
||||
("2 days", 2 * 86400),
|
||||
];
|
||||
for test in tests {
|
||||
assert_eq!(test.0, format!("{}", Duration(test.1 * TIME_UNITS_PER_SEC)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests encoding the example from design/schema.md.
|
||||
#[test]
|
||||
fn test_encode_example() {
|
||||
|
@ -53,9 +53,9 @@ const UPGRADE_NOTES: &'static str =
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Args<'a> {
|
||||
pub flag_sample_file_dir: Option<&'a str>,
|
||||
pub flag_preset_journal: &'a str,
|
||||
pub flag_no_vacuum: bool,
|
||||
pub sample_file_dir: Option<&'a std::path::Path>,
|
||||
pub preset_journal: &'a str,
|
||||
pub no_vacuum: bool,
|
||||
}
|
||||
|
||||
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
|
||||
@ -88,7 +88,7 @@ fn upgrade(args: &Args, target_ver: i32, conn: &mut rusqlite::Connection) -> Res
|
||||
bail!("Database is at negative version {}!", old_ver);
|
||||
}
|
||||
info!("Upgrading database from version {} to version {}...", old_ver, target_ver);
|
||||
set_journal_mode(&conn, args.flag_preset_journal)?;
|
||||
set_journal_mode(&conn, args.preset_journal)?;
|
||||
for ver in old_ver .. target_ver {
|
||||
info!("...from version {} to version {}", ver, ver + 1);
|
||||
let tx = conn.transaction()?;
|
||||
@ -120,7 +120,7 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> {
|
||||
// WAL is the preferred journal mode for normal operation; it reduces the number of syncs
|
||||
// without compromising safety.
|
||||
set_journal_mode(&conn, "wal")?;
|
||||
if !args.flag_no_vacuum {
|
||||
if !args.no_vacuum {
|
||||
info!("...vacuuming database after upgrade.");
|
||||
conn.execute_batch(r#"
|
||||
pragma page_size = 16384;
|
||||
@ -159,7 +159,7 @@ impl NixPath for UuidPath {
|
||||
mod tests {
|
||||
use crate::compare;
|
||||
use crate::testutil;
|
||||
use failure::{ResultExt, format_err};
|
||||
use failure::ResultExt;
|
||||
use fnv::FnvHashMap;
|
||||
use super::*;
|
||||
|
||||
@ -209,7 +209,7 @@ mod tests {
|
||||
fn upgrade_and_compare() -> Result<(), Error> {
|
||||
testutil::init();
|
||||
let tmpdir = tempdir::TempDir::new("moonfire-nvr-test")?;
|
||||
let path = tmpdir.path().to_str().ok_or_else(|| format_err!("invalid UTF-8"))?.to_owned();
|
||||
//let path = tmpdir.path().to_str().ok_or_else(|| format_err!("invalid UTF-8"))?.to_owned();
|
||||
let mut upgraded = new_conn()?;
|
||||
upgraded.execute_batch(include_str!("v0.sql"))?;
|
||||
upgraded.execute_batch(r#"
|
||||
@ -252,9 +252,9 @@ mod tests {
|
||||
(5, Some(include_str!("v5.sql"))),
|
||||
(6, Some(include_str!("../schema.sql")))] {
|
||||
upgrade(&Args {
|
||||
flag_sample_file_dir: Some(&path),
|
||||
flag_preset_journal: "delete",
|
||||
flag_no_vacuum: false,
|
||||
sample_file_dir: Some(&tmpdir.path()),
|
||||
preset_journal: "delete",
|
||||
no_vacuum: false,
|
||||
}, *ver, &mut upgraded).context(format!("upgrading to version {}", ver))?;
|
||||
if let Some(f) = fresh_sql {
|
||||
compare(&upgraded, *ver, f)?;
|
||||
|
@ -42,7 +42,7 @@ use uuid::Uuid;
|
||||
|
||||
pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||
let sample_file_path =
|
||||
args.flag_sample_file_dir
|
||||
args.sample_file_dir
|
||||
.ok_or_else(|| format_err!("--sample-file-dir required when upgrading from \
|
||||
schema version 1 to 2."))?;
|
||||
|
||||
@ -122,6 +122,9 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
||||
}
|
||||
dir::write_meta(d.as_raw_fd(), &meta)?;
|
||||
|
||||
let sample_file_path = sample_file_path.to_str()
|
||||
.ok_or_else(|| format_err!("sample file dir {} is not a valid string",
|
||||
sample_file_path.display()))?;
|
||||
tx.execute(r#"
|
||||
insert into sample_file_dir (path, uuid, last_complete_open_id)
|
||||
values (?, ?, ?)
|
||||
@ -293,7 +296,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
||||
/// * optional: reserved sample file uuids.
|
||||
/// * optional: meta and meta-tmp from half-completed update attempts.
|
||||
/// * forbidden: anything else.
|
||||
fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir,
|
||||
fn verify_dir_contents(sample_file_path: &std::path::Path, dir: &mut nix::dir::Dir,
|
||||
tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||
// Build a hash of the uuids found in the directory.
|
||||
let n: i64 = tx.query_row(r#"
|
||||
@ -337,7 +340,7 @@ fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir,
|
||||
while let Some(row) = rows.next()? {
|
||||
let uuid: crate::db::FromSqlUuid = row.get(0)?;
|
||||
if !files.remove(&uuid.0) {
|
||||
bail!("{} is missing from dir {}!", uuid.0, sample_file_path);
|
||||
bail!("{} is missing from dir {}!", uuid.0, sample_file_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -360,7 +363,7 @@ fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir,
|
||||
|
||||
if !files.is_empty() {
|
||||
bail!("{} unexpected sample file uuids in dir {}: {:?}!",
|
||||
files.len(), sample_file_path, files);
|
||||
files.len(), sample_file_path.display(), files);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2018 The Moonfire NVR Authors
|
||||
// Copyright (C) 2018-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
@ -32,36 +32,25 @@
|
||||
|
||||
use db::check;
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Checks database integrity.
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
|
||||
Usage:
|
||||
|
||||
moonfire-nvr check [options]
|
||||
moonfire-nvr check --help
|
||||
|
||||
Options:
|
||||
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--compare-lens Compare sample file lengths on disk to the database.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_compare_lens: bool,
|
||||
/// Compare sample file lengths on disk to the database.
|
||||
#[structopt(long)]
|
||||
compare_lens: bool,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
// TODO: ReadOnly should be sufficient but seems to fail.
|
||||
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
check::run(&conn, &check::Options {
|
||||
compare_lens: args.flag_compare_lens,
|
||||
compare_lens: args.compare_lens,
|
||||
})
|
||||
}
|
||||
|
@ -38,36 +38,24 @@ use cursive::Cursive;
|
||||
use cursive::views;
|
||||
use db;
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use structopt::StructOpt;
|
||||
|
||||
mod cameras;
|
||||
mod dirs;
|
||||
mod users;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Interactive configuration editor.
|
||||
|
||||
Usage:
|
||||
|
||||
moonfire-nvr config [options]
|
||||
moonfire-nvr config --help
|
||||
|
||||
Options:
|
||||
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
let clocks = clock::RealClocks {};
|
||||
let db = Arc::new(db::Database::new(clocks, conn, true)?);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
@ -30,31 +30,19 @@
|
||||
|
||||
use failure::Error;
|
||||
use log::info;
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Initializes a database.
|
||||
|
||||
Usage:
|
||||
|
||||
moonfire-nvr init [options]
|
||||
moonfire-nvr init --help
|
||||
|
||||
Options:
|
||||
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::Create)?;
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::Create)?;
|
||||
|
||||
// Check if the database has already been initialized.
|
||||
let cur_ver = db::get_schema_version(&conn)?;
|
||||
|
@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2019 The Moonfire NVR Authors
|
||||
// Copyright (C) 2019-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
@ -31,92 +31,74 @@
|
||||
//! Subcommand to login a user (without requiring a password).
|
||||
|
||||
use base::clock::{self, Clocks};
|
||||
use db::auth::SessionFlags;
|
||||
use failure::{Error, ResultExt, bail, format_err};
|
||||
use serde::Deserialize;
|
||||
use db::auth::SessionFlag;
|
||||
use failure::{Error, format_err};
|
||||
use std::os::unix::fs::OpenOptionsExt as _;
|
||||
use std::io::Write as _;
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Logs in a user, returning the session cookie.
|
||||
#[derive(Debug, Default, StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
|
||||
This is a privileged command that directly accesses the database. It doesn't
|
||||
check the user's password and even can be used to create sessions with
|
||||
permissions the user doesn't have.
|
||||
/// Create a session with the given permissions.
|
||||
///
|
||||
/// If unspecified, uses user's default permissions.
|
||||
#[structopt(long, value_name="perms",
|
||||
parse(try_from_str = protobuf::text_format::parse_from_str))]
|
||||
permissions: Option<db::Permissions>,
|
||||
|
||||
Usage:
|
||||
/// Restrict this cookie to the given domain.
|
||||
#[structopt(long)]
|
||||
domain: Option<String>,
|
||||
|
||||
moonfire-nvr login [options] <username>
|
||||
moonfire-nvr login --help
|
||||
/// Write the cookie to a new curl-compatible cookie-jar file.
|
||||
///
|
||||
/// ---domain must be specified. This file can be used later with curl's --cookie flag.
|
||||
#[structopt(long, requires("domain"), value_name="path")]
|
||||
curl_cookie_jar: Option<PathBuf>,
|
||||
|
||||
Options:
|
||||
/// Set the given db::auth::SessionFlags.
|
||||
#[structopt(long, default_value="http-only,secure,same-site,same-site-strict",
|
||||
value_name="flags", use_delimiter=true)]
|
||||
session_flags: Vec<SessionFlag>,
|
||||
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database. This
|
||||
is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--permissions=PERMISSIONS
|
||||
Create a session with the given permissions. If
|
||||
unspecified, uses user's default permissions.
|
||||
--domain=DOMAIN The domain this cookie lives on. Optional.
|
||||
--curl-cookie-jar=FILE
|
||||
Writes the cookie to a new curl-compatible cookie-jar
|
||||
file. --domain must be specified. This can be used later
|
||||
with curl's --cookie flag.
|
||||
--session-flags=FLAGS
|
||||
Set the given db::auth::SessionFlags.
|
||||
[default: http-only,secure,same-site,same-site-strict]
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_permissions: Option<String>,
|
||||
flag_domain: Option<String>,
|
||||
flag_curl_cookie_jar: Option<PathBuf>,
|
||||
flag_session_flags: String,
|
||||
arg_username: String,
|
||||
/// Create the session for this username.
|
||||
username: String,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let clocks = clock::RealClocks {};
|
||||
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
let db = std::sync::Arc::new(db::Database::new(clocks.clone(), conn, true).unwrap());
|
||||
let mut l = db.lock();
|
||||
let u = l.get_user(&args.arg_username)
|
||||
.ok_or_else(|| format_err!("no such user {:?}", &args.arg_username))?;
|
||||
let permissions = match args.flag_permissions {
|
||||
None => u.permissions.clone(),
|
||||
Some(s) => protobuf::text_format::parse_from_str(&s)
|
||||
.context("unable to parse --permissions")?
|
||||
};
|
||||
let u = l.get_user(&args.username)
|
||||
.ok_or_else(|| format_err!("no such user {:?}", &args.username))?;
|
||||
let permissions = args.permissions.as_ref().unwrap_or(&u.permissions).clone();
|
||||
let creation = db::auth::Request {
|
||||
when_sec: Some(db.clocks().realtime().sec),
|
||||
user_agent: None,
|
||||
addr: None,
|
||||
};
|
||||
let mut flags = 0;
|
||||
for f in args.flag_session_flags.split(',') {
|
||||
flags |= match f {
|
||||
"http-only" => SessionFlags::HttpOnly,
|
||||
"secure" => SessionFlags::Secure,
|
||||
"same-site" => SessionFlags::SameSite,
|
||||
"same-site-strict" => SessionFlags::SameSiteStrict,
|
||||
_ => bail!("unknown session flag {:?}", f),
|
||||
} as i32;
|
||||
for f in &args.session_flags {
|
||||
flags |= *f as i32;
|
||||
}
|
||||
let uid = u.id;
|
||||
drop(u);
|
||||
let (sid, _) = l.make_session(creation, uid,
|
||||
args.flag_domain.as_ref().map(|d| d.as_bytes().to_owned()),
|
||||
args.domain.as_ref().map(|d| d.as_bytes().to_owned()),
|
||||
flags, permissions)?;
|
||||
let mut encoded = [0u8; 64];
|
||||
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
|
||||
let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8");
|
||||
|
||||
if let Some(ref p) = args.flag_curl_cookie_jar {
|
||||
let d = args.flag_domain.as_ref()
|
||||
if let Some(ref p) = args.curl_cookie_jar {
|
||||
let d = args.domain.as_ref()
|
||||
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?;
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
@ -139,11 +121,11 @@ pub fn run() -> Result<(), Error> {
|
||||
|
||||
fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
|
||||
format!("{httponly}{domain}\t{tailmatch}\t{path}\t{secure}\t{expires}\t{name}\t{value}",
|
||||
httponly=if (flags & SessionFlags::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" },
|
||||
httponly=if (flags & SessionFlag::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" },
|
||||
domain=domain,
|
||||
tailmatch="FALSE",
|
||||
path="/",
|
||||
secure=if (flags & SessionFlags::Secure as i32) != 0 { "TRUE" } else { "FALSE" },
|
||||
secure=if (flags & SessionFlag::Secure as i32) != 0 { "TRUE" } else { "FALSE" },
|
||||
expires="9223372036854775807", // 64-bit CURL_OFF_T_MAX, never expires
|
||||
name="s",
|
||||
value=cookie)
|
||||
@ -153,24 +135,10 @@ fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_args() {
|
||||
let args: Args = docopt::Docopt::new(USAGE).unwrap()
|
||||
.argv(&["nvr", "login", "--curl-cookie-jar=foo.txt", "slamb"])
|
||||
.deserialize().unwrap();
|
||||
assert_eq!(args, Args {
|
||||
flag_db_dir: "/var/lib/moonfire-nvr/db".to_owned(),
|
||||
flag_curl_cookie_jar: Some(PathBuf::from("foo.txt")),
|
||||
flag_session_flags: "http-only,secure,same-site,same-site-strict".to_owned(),
|
||||
arg_username: "slamb".to_owned(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_curl_cookie() {
|
||||
assert_eq!(curl_cookie("o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q",
|
||||
SessionFlags::HttpOnly as i32, "localhost"),
|
||||
SessionFlag::HttpOnly as i32, "localhost"),
|
||||
"#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\
|
||||
o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q");
|
||||
}
|
||||
|
@ -29,48 +29,19 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use db::dir;
|
||||
use docopt;
|
||||
use failure::{Error, Fail};
|
||||
use nix::fcntl::FlockArg;
|
||||
use rusqlite;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
mod check;
|
||||
mod config;
|
||||
mod login;
|
||||
mod init;
|
||||
mod run;
|
||||
mod sql;
|
||||
mod ts;
|
||||
mod upgrade;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub enum Command {
|
||||
Check,
|
||||
Config,
|
||||
Login,
|
||||
Init,
|
||||
Run,
|
||||
Sql,
|
||||
Ts,
|
||||
Upgrade,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
match *self {
|
||||
Command::Check => check::run(),
|
||||
Command::Config => config::run(),
|
||||
Command::Login => login::run(),
|
||||
Command::Init => init::run(),
|
||||
Command::Run => run::run(),
|
||||
Command::Sql => sql::run(),
|
||||
Command::Ts => ts::run(),
|
||||
Command::Upgrade => upgrade::run(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod check;
|
||||
pub mod config;
|
||||
pub mod login;
|
||||
pub mod init;
|
||||
pub mod run;
|
||||
pub mod sql;
|
||||
pub mod ts;
|
||||
pub mod upgrade;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum OpenMode {
|
||||
@ -81,10 +52,10 @@ enum OpenMode {
|
||||
|
||||
/// Locks the directory without opening the database.
|
||||
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is.
|
||||
fn open_dir(db_dir: &str, mode: OpenMode) -> Result<dir::Fd, Error> {
|
||||
fn open_dir(db_dir: &Path, mode: OpenMode) -> Result<dir::Fd, Error> {
|
||||
let dir = dir::Fd::open(db_dir, mode == OpenMode::Create)?;
|
||||
let ro = mode == OpenMode::ReadOnly;
|
||||
dir.lock(if ro { FlockArg::LockExclusiveNonblock } else { FlockArg::LockSharedNonblock })
|
||||
dir.lock(if ro { FlockArg::LockSharedNonblock } else { FlockArg::LockExclusiveNonblock })
|
||||
.map_err(|e| e.context(format!("db dir {:?} already in use; can't get {} lock",
|
||||
db_dir, if ro { "shared" } else { "exclusive" })))?;
|
||||
Ok(dir)
|
||||
@ -92,10 +63,10 @@ fn open_dir(db_dir: &str, mode: OpenMode) -> Result<dir::Fd, Error> {
|
||||
|
||||
/// Locks and opens the database.
|
||||
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is.
|
||||
fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> {
|
||||
fn open_conn(db_dir: &Path, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> {
|
||||
let dir = open_dir(db_dir, mode)?;
|
||||
let conn = rusqlite::Connection::open_with_flags(
|
||||
Path::new(&db_dir).join("db"),
|
||||
db_dir.join("db"),
|
||||
match mode {
|
||||
OpenMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
|
||||
OpenMode::ReadWrite => rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE,
|
||||
@ -108,9 +79,3 @@ fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connect
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX)?;
|
||||
Ok((dir, conn))
|
||||
}
|
||||
|
||||
fn parse_args<'a, T>(usage: &str) -> Result<T, Error> where T: ::serde::Deserialize<'a> {
|
||||
Ok(docopt::Docopt::new(usage)
|
||||
.and_then(|d| d.deserialize())
|
||||
.unwrap_or_else(|e| e.exit()))
|
||||
}
|
||||
|
120
src/cmds/run.rs
120
src/cmds/run.rs
@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
@ -33,19 +33,66 @@ use crate::stream;
|
||||
use crate::streamer;
|
||||
use crate::web;
|
||||
use db::{dir, writer};
|
||||
use failure::{Error, ResultExt, bail};
|
||||
use failure::{Error, bail};
|
||||
use fnv::FnvHashMap;
|
||||
use futures::future::FutureExt;
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use log::{info, warn};
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use structopt::StructOpt;
|
||||
use tokio;
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
|
||||
/// Directory holding user interface files (.html, .js, etc).
|
||||
#[structopt(default_value = "/usr/local/lib/moonfire-nvr/ui", value_name="path",
|
||||
parse(from_os_str))]
|
||||
ui_dir: std::path::PathBuf,
|
||||
|
||||
/// Bind address for unencrypted HTTP server.
|
||||
#[structopt(long, default_value = "0.0.0.0:8080", parse(try_from_str))]
|
||||
http_addr: std::net::SocketAddr,
|
||||
|
||||
/// Open the database in read-only mode and disables recording.
|
||||
///
|
||||
/// Note this is incompatible with authentication, so you'll likely want to specify
|
||||
/// --allow_unauthenticated_permissions.
|
||||
#[structopt(long)]
|
||||
read_only: bool,
|
||||
|
||||
/// Allow unauthenticated access to the web interface, with the given permissions (may be
|
||||
/// empty). Should be a text Permissions protobuf such as "view_videos: true".
|
||||
///
|
||||
/// Note that even an empty string allows some basic access that would be rejected if the
|
||||
/// argument were omitted.
|
||||
#[structopt(long, parse(try_from_str = protobuf::text_format::parse_from_str))]
|
||||
allow_unauthenticated_permissions: Option<db::Permissions>,
|
||||
|
||||
/// Trust X-Real-IP: and X-Forwarded-Proto: headers on the incoming request.
|
||||
///
|
||||
/// Set this only after ensuring your proxy server is configured to set them and that no
|
||||
/// untrusted requests bypass the proxy server. You may want to specify
|
||||
/// --http-addr=127.0.0.1:8080.
|
||||
#[structopt(long)]
|
||||
trust_forward_hdrs: bool,
|
||||
|
||||
/// Perform object detection on SUB streams.
|
||||
///
|
||||
/// Note: requires compilation with --feature=analytics.
|
||||
#[structopt(long)]
|
||||
object_detection: bool,
|
||||
}
|
||||
|
||||
// These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles).
|
||||
// They seem to be correct for Linux and macOS at least.
|
||||
const LOCALTIME_PATH: &'static str = "/etc/localtime";
|
||||
@ -55,47 +102,6 @@ const ZONEINFO_PATHS: [&'static str; 2] = [
|
||||
"/var/db/timezone/zoneinfo/" // macOS High Sierra
|
||||
];
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Usage: moonfire-nvr run [options]
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--ui-dir=DIR Set the directory with the user interface files
|
||||
(.html, .js, etc).
|
||||
[default: /usr/local/lib/moonfire-nvr/ui]
|
||||
--http-addr=ADDR Set the bind address for the unencrypted HTTP server.
|
||||
[default: 0.0.0.0:8080]
|
||||
--read-only Forces read-only mode / disables recording.
|
||||
--allow-unauthenticated-permissions=PERMISSIONS
|
||||
Allow unauthenticated access to the web interface,
|
||||
with the given permissions (may be empty).
|
||||
PERMISSIONS should be a text Permissions protobuf
|
||||
such as "view_videos: true". NOTE: even an empty
|
||||
string allows some basic access that would be
|
||||
rejected if the argument were omitted.
|
||||
--trust-forward-hdrs Trust X-Real-IP: and X-Forwarded-Proto: headers on
|
||||
the incoming request. Set this only after ensuring
|
||||
your proxy server is configured to set them and that
|
||||
no untrusted requests bypass the proxy server.
|
||||
You may want to specify --http-addr=127.0.0.1:8080.
|
||||
--object-detection Perform object detection on SUB streams.
|
||||
Note: requires compilation with --feature=analytics.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_http_addr: String,
|
||||
flag_ui_dir: String,
|
||||
flag_read_only: bool,
|
||||
flag_allow_unauthenticated_permissions: Option<String>,
|
||||
flag_trust_forward_hdrs: bool,
|
||||
flag_object_detection: bool,
|
||||
}
|
||||
|
||||
fn trim_zoneinfo(p: &str) -> &str {
|
||||
for zp in &ZONEINFO_PATHS {
|
||||
if p.starts_with(zp) {
|
||||
@ -170,16 +176,15 @@ struct Syncer {
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
pub async fn run(args: &Args) -> Result<(), Error> {
|
||||
let clocks = clock::RealClocks {};
|
||||
let (_db_dir, conn) = super::open_conn(
|
||||
&args.flag_db_dir,
|
||||
if args.flag_read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?;
|
||||
let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.flag_read_only).unwrap());
|
||||
&args.db_dir,
|
||||
if args.read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?;
|
||||
let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.read_only).unwrap());
|
||||
info!("Database is loaded.");
|
||||
|
||||
let object_detector = match args.flag_object_detection {
|
||||
let object_detector = match args.object_detection {
|
||||
false => None,
|
||||
true => Some(crate::analytics::ObjectDetector::new()?),
|
||||
};
|
||||
@ -194,22 +199,18 @@ pub async fn run() -> Result<(), Error> {
|
||||
|
||||
let time_zone_name = resolve_zone()?;
|
||||
info!("Resolved timezone: {}", &time_zone_name);
|
||||
let allow_unauthenticated_permissions = args.flag_allow_unauthenticated_permissions
|
||||
.map(|s| protobuf::text_format::parse_from_str(&s))
|
||||
.transpose()
|
||||
.context("Unable to parse --allow-unauthenticated-permissions")?;
|
||||
let s = web::Service::new(web::Config {
|
||||
db: db.clone(),
|
||||
ui_dir: Some(&args.flag_ui_dir),
|
||||
allow_unauthenticated_permissions,
|
||||
trust_forward_hdrs: args.flag_trust_forward_hdrs,
|
||||
ui_dir: Some(&args.ui_dir),
|
||||
allow_unauthenticated_permissions: args.allow_unauthenticated_permissions.clone(),
|
||||
trust_forward_hdrs: args.trust_forward_hdrs,
|
||||
time_zone_name,
|
||||
})?;
|
||||
|
||||
// Start a streamer for each stream.
|
||||
let shutdown_streamers = Arc::new(AtomicBool::new(false));
|
||||
let mut streamers = Vec::new();
|
||||
let syncers = if !args.flag_read_only {
|
||||
let syncers = if !args.read_only {
|
||||
let l = db.lock();
|
||||
let mut dirs = FnvHashMap::with_capacity_and_hasher(
|
||||
l.sample_file_dirs_by_id().len(), Default::default());
|
||||
@ -280,14 +281,13 @@ pub async fn run() -> Result<(), Error> {
|
||||
} else { None };
|
||||
|
||||
// Start the web interface.
|
||||
let addr = args.flag_http_addr.parse().unwrap();
|
||||
let make_svc = make_service_fn(move |_conn| {
|
||||
futures::future::ok::<_, std::convert::Infallible>(service_fn({
|
||||
let mut s = s.clone();
|
||||
move |req| Pin::from(s.serve(req))
|
||||
}))
|
||||
});
|
||||
let server = ::hyper::server::Server::bind(&addr)
|
||||
let server = ::hyper::server::Server::bind(&args.http_addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(make_svc);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2019 The Moonfire NVR Authors
|
||||
// Copyright (C) 2019-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
@ -31,45 +31,43 @@
|
||||
//! Subcommand to run a SQLite shell.
|
||||
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use super::OpenMode;
|
||||
use structopt::StructOpt;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Runs a SQLite shell on the Moonfire NVR database with locking.
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
|
||||
Usage:
|
||||
/// Opens the database in read-only mode and locks it only for shared access.
|
||||
///
|
||||
/// This can be run simultaneously with "moonfire-nvr run --read-only".
|
||||
#[structopt(long)]
|
||||
read_only: bool,
|
||||
|
||||
moonfire-nvr sql [options] [--] [<arg>...]
|
||||
moonfire-nvr sql --help
|
||||
|
||||
Positional arguments will be passed to sqlite3. Use the -- separator to pass
|
||||
sqlite3 options, as in "moonfire-nvr sql -- -line 'select username from user'".
|
||||
|
||||
Options:
|
||||
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--read-only Accesses the database in read-only mode.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_read_only: bool,
|
||||
arg_arg: Vec<String>,
|
||||
/// Arguments to pass to sqlite3.
|
||||
///
|
||||
/// Use the -- separator to pass sqlite3 options, as in
|
||||
/// "moonfire-nvr sql -- -line 'select username from user'".
|
||||
#[structopt(parse(from_os_str))]
|
||||
arg: Vec<OsString>,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
|
||||
let mode = if args.flag_read_only { OpenMode::ReadWrite } else { OpenMode::ReadOnly };
|
||||
let _db_dir = super::open_dir(&args.flag_db_dir, mode)?;
|
||||
let mut db = format!("file:{}/db", &args.flag_db_dir);
|
||||
if args.flag_read_only {
|
||||
db.push_str("?mode=ro");
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let mode = if args.read_only { OpenMode::ReadOnly } else { OpenMode::ReadWrite };
|
||||
let _db_dir = super::open_dir(&args.db_dir, mode)?;
|
||||
let mut db = OsString::new();
|
||||
db.push("file:");
|
||||
db.push(&args.db_dir);
|
||||
db.push("/db");
|
||||
if args.read_only {
|
||||
db.push("?mode=ro");
|
||||
}
|
||||
Command::new("sqlite3").arg(&db).args(&args.arg_arg).status()?;
|
||||
Command::new("sqlite3").arg(&db).args(&args.arg).status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
@ -29,21 +29,22 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Usage: moonfire-nvr ts <ts>...
|
||||
moonfire-nvr ts --help
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
arg_ts: Vec<String>,
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Timestamp(s) to translate.
|
||||
///
|
||||
/// May be either an integer or an RFC-3339-like string:
|
||||
/// YYYY-mm-dd[THH:MM[:SS[:FFFFF]]][{Z,{+,-,}HH:MM}].
|
||||
///
|
||||
/// Eg: 142913484000000, 2020-04-26, 2020-04-26T12:00:00:00000-07:00.
|
||||
#[structopt(required = true)]
|
||||
timestamps: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let arg: Args = super::parse_args(&USAGE)?;
|
||||
for timestamp in &arg.arg_ts {
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
for timestamp in &args.timestamps {
|
||||
let t = db::recording::Time::parse(timestamp)?;
|
||||
println!("{} == {}", t, t.0);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
@ -33,45 +33,38 @@
|
||||
/// See `guide/schema.md` for more information.
|
||||
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Upgrade to the latest database schema.
|
||||
|
||||
Usage: moonfire-nvr upgrade [options]
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--sample-file-dir=DIR When upgrading from schema version 1 to 2, the sample file directory.
|
||||
This is typically on a hard drive.
|
||||
--preset-journal=MODE Resets the SQLite journal_mode to the specified mode
|
||||
prior to the upgrade. The default, delete, is
|
||||
recommended. off is very dangerous but may be
|
||||
desirable in some circumstances. See guide/schema.md
|
||||
for more information. The journal mode will be reset
|
||||
to wal after the upgrade.
|
||||
[default: delete]
|
||||
--no-vacuum Skips the normal post-upgrade vacuum operation.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_sample_file_dir: Option<String>,
|
||||
flag_preset_journal: String,
|
||||
flag_no_vacuum: bool,
|
||||
#[structopt(long,
|
||||
help = "Directory holding the SQLite3 index database.",
|
||||
default_value = "/var/lib/moonfire-nvr/db",
|
||||
parse(from_os_str))]
|
||||
db_dir: std::path::PathBuf,
|
||||
|
||||
#[structopt(help = "When upgrading from schema version 1 to 2, the sample file directory.",
|
||||
long, parse(from_os_str))]
|
||||
sample_file_dir: Option<std::path::PathBuf>,
|
||||
|
||||
#[structopt(help = "Resets the SQLite journal_mode to the specified mode prior to the \
|
||||
upgrade. The default, delete, is recommended. off is very dangerous \
|
||||
but may be desirable in some circumstances. See guide/schema.md for \
|
||||
more information. The journal mode will be reset to wal after the \
|
||||
upgrade.",
|
||||
long, default_value = "delete")]
|
||||
preset_journal: String,
|
||||
|
||||
#[structopt(help = "Skips the normal post-upgrade vacuum operation.", long)]
|
||||
no_vacuum: bool,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
|
||||
db::upgrade::run(&db::upgrade::Args {
|
||||
flag_sample_file_dir: args.flag_sample_file_dir.as_ref().map(|s| s.as_str()),
|
||||
flag_preset_journal: &args.flag_preset_journal,
|
||||
flag_no_vacuum: args.flag_no_vacuum,
|
||||
sample_file_dir: args.sample_file_dir.as_ref().map(std::path::PathBuf::as_path),
|
||||
preset_journal: &args.preset_journal,
|
||||
no_vacuum: args.no_vacuum,
|
||||
}, &mut conn)
|
||||
}
|
||||
|
29
src/h264.rs
29
src/h264.rs
@ -42,8 +42,6 @@
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||
use failure::{Error, bail, format_err};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::bytes::Regex;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
// See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element categories, and NAL unit
|
||||
@ -91,15 +89,28 @@ fn default_pixel_aspect_ratio(width: u16, height: u16) -> (u16, u16) {
|
||||
///
|
||||
/// TODO: detect invalid byte streams. For example, several 0x00s not followed by a 0x01, a stream
|
||||
/// stream not starting with 0x00 0x00 0x00 0x01, or an empty NAL unit.
|
||||
fn decode_h264_annex_b<'a, F>(data: &'a [u8], mut f: F) -> Result<(), Error>
|
||||
fn decode_h264_annex_b<'a, F>(mut data: &'a [u8], mut f: F) -> Result<(), Error>
|
||||
where F: FnMut(&'a [u8]) -> Result<(), Error> {
|
||||
lazy_static! {
|
||||
static ref START_CODE: Regex = Regex::new(r"(\x00{2,}\x01)").unwrap();
|
||||
}
|
||||
for unit in START_CODE.split(data) {
|
||||
if !unit.is_empty() {
|
||||
f(unit)?;
|
||||
let start_code = &b"\x00\x00\x01"[..];
|
||||
use nom::FindSubstring;
|
||||
'outer: while let Some(pos) = data.find_substring(start_code) {
|
||||
let mut unit = &data[0..pos];
|
||||
data = &data[pos + start_code.len() ..];
|
||||
// Have zero or more bytes that end in a start code. Strip out any trailing 0x00s and
|
||||
// process the unit if there's anything left.
|
||||
loop {
|
||||
match unit.last() {
|
||||
None => continue 'outer,
|
||||
Some(b) if *b == 0 => { unit = &unit[..unit.len()-1]; },
|
||||
Some(_) => break,
|
||||
}
|
||||
}
|
||||
f(unit)?;
|
||||
}
|
||||
|
||||
// No remaining start codes; likely a unit left.
|
||||
if !data.is_empty() {
|
||||
f(data)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
85
src/main.rs
85
src/main.rs
@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
@ -31,7 +31,7 @@
|
||||
#![cfg_attr(all(feature="nightly", test), feature(test))]
|
||||
|
||||
use log::{error, info};
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[cfg(feature = "analytics")]
|
||||
mod analytics;
|
||||
@ -74,39 +74,53 @@ mod stream;
|
||||
mod streamer;
|
||||
mod web;
|
||||
|
||||
/// Commandline usage string. This is in the particular format expected by the `docopt` crate.
|
||||
/// Besides being printed on --help or argument parsing error, it's actually parsed to define the
|
||||
/// allowed commandline arguments and their defaults.
|
||||
const USAGE: &'static str = "
|
||||
Usage: moonfire-nvr <command> [<args>...]
|
||||
moonfire-nvr (--help | --version)
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(name="moonfire-nvr", about="security camera network video recorder")]
|
||||
enum Args {
|
||||
/// Checks database integrity (like fsck).
|
||||
Check(cmds::check::Args),
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--version Show the version of moonfire-nvr.
|
||||
/// Interactively edits configuration.
|
||||
Config(cmds::config::Args),
|
||||
|
||||
Commands:
|
||||
check Check database integrity
|
||||
init Initialize a database
|
||||
run Run the daemon: record from cameras and serve HTTP
|
||||
shell Start an interactive shell to modify the database
|
||||
ts Translate human-readable and numeric timestamps
|
||||
upgrade Upgrade the database to the latest schema
|
||||
";
|
||||
/// Initializes a database.
|
||||
Init(cmds::init::Args),
|
||||
|
||||
/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
arg_command: Option<cmds::Command>,
|
||||
/// Logs in a user, returning the session cookie.
|
||||
///
|
||||
/// This is a privileged command that directly accesses the database. It doesn't check the
|
||||
/// user's password and even can be used to create sessions with permissions the user doesn't
|
||||
/// have.
|
||||
Login(cmds::login::Args),
|
||||
|
||||
/// Runs the server, saving recordings and allowing web access.
|
||||
Run(cmds::run::Args),
|
||||
|
||||
/// Runs a SQLite3 shell on Moonfire NVR's index database.
|
||||
///
|
||||
/// Note this locks the database to prevent simultaneous access with a running server. The
|
||||
/// server maintains cached state which could be invalidated otherwise.
|
||||
Sql(cmds::sql::Args),
|
||||
|
||||
/// Translates between integer and human-readable timestamps.
|
||||
Ts(cmds::ts::Args),
|
||||
|
||||
/// Upgrades to the latest database schema.
|
||||
Upgrade(cmds::upgrade::Args),
|
||||
}
|
||||
|
||||
fn version() -> String {
|
||||
let major = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||
let minor = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||
let patch = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||
match (major, minor, patch) {
|
||||
(Some(major), Some(minor), Some(patch)) => format!("{}.{}.{}", major, minor, patch),
|
||||
_ => "".to_owned(),
|
||||
impl Args {
|
||||
fn run(&self) -> Result<(), failure::Error> {
|
||||
match self {
|
||||
Args::Check(ref a) => cmds::check::run(a),
|
||||
Args::Config(ref a) => cmds::config::run(a),
|
||||
Args::Init(ref a) => cmds::init::run(a),
|
||||
Args::Login(ref a) => cmds::login::run(a),
|
||||
Args::Run(ref a) => cmds::run::run(a),
|
||||
Args::Sql(ref a) => cmds::sql::run(a),
|
||||
Args::Ts(ref a) => cmds::ts::run(a),
|
||||
Args::Upgrade(ref a) => cmds::upgrade::run(a),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,14 +133,7 @@ fn parse_fmt<S: AsRef<str>>(fmt: S) -> Option<mylog::Format> {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Parse commandline arguments.
|
||||
// (Note this differs from cmds::parse_args in that it specifies options_first.)
|
||||
let args: Args = docopt::Docopt::new(USAGE)
|
||||
.and_then(|d| d.options_first(true)
|
||||
.version(Some(version()))
|
||||
.deserialize())
|
||||
.unwrap_or_else(|e| e.exit());
|
||||
|
||||
let args = Args::from_args();
|
||||
let mut h = mylog::Builder::new()
|
||||
.set_format(::std::env::var("MOONFIRE_FORMAT")
|
||||
.ok()
|
||||
@ -136,7 +143,7 @@ fn main() {
|
||||
.build();
|
||||
h.clone().install().unwrap();
|
||||
|
||||
if let Err(e) = { let _a = h.r#async(); args.arg_command.unwrap().run() } {
|
||||
if let Err(e) = { let _a = h.r#async(); args.run() } {
|
||||
error!("{:?}", e);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
|
123
src/web.rs
123
src/web.rs
@ -34,7 +34,6 @@ use bytes::Bytes;
|
||||
use crate::body::{Body, BoxedError};
|
||||
use crate::json;
|
||||
use crate::mp4;
|
||||
use base64;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use core::borrow::Borrow;
|
||||
use core::str::FromStr;
|
||||
@ -46,12 +45,12 @@ use futures::sink::SinkExt;
|
||||
use futures::future::{self, Future, TryFutureExt};
|
||||
use futures::stream::StreamExt;
|
||||
use http::{Request, Response, status::StatusCode};
|
||||
use http_serve;
|
||||
use http::header::{self, HeaderValue};
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, info, warn};
|
||||
use regex::Regex;
|
||||
use serde_json;
|
||||
use nom::IResult;
|
||||
use nom::bytes::complete::{take_while1, tag};
|
||||
use nom::combinator::{all_consuming, map, map_res, opt};
|
||||
use nom::sequence::{preceded, tuple};
|
||||
use std::collections::HashMap;
|
||||
use std::cmp;
|
||||
use std::fs;
|
||||
@ -64,14 +63,6 @@ use tokio_tungstenite::tungstenite;
|
||||
use url::form_urlencoded;
|
||||
use uuid::Uuid;
|
||||
|
||||
lazy_static! {
|
||||
/// Regex used to parse the `s` query parameter to `view.mp4`.
|
||||
/// As described in `design/api.md`, this is of the form
|
||||
/// `START_ID[-END_ID][@OPEN_ID][.[REL_START_TIME]-[REL_END_TIME]]`.
|
||||
static ref SEGMENTS_RE: Regex =
|
||||
Regex::new(r"^(\d+)(-\d+)?(@\d+)?(?:\.(\d+)?-(\d+)?)?$").unwrap();
|
||||
}
|
||||
|
||||
type BoxedFuture = Box<dyn Future<Output = Result<Response<Body>, BoxedError>> +
|
||||
Sync + Send + 'static>;
|
||||
|
||||
@ -204,41 +195,48 @@ struct Segments {
|
||||
end_time: Option<i64>,
|
||||
}
|
||||
|
||||
fn num<'a, T: FromStr>() -> impl Fn(&'a str) -> IResult<&'a str, T> {
|
||||
map_res(take_while1(|c: char| c.is_ascii_digit()), FromStr::from_str)
|
||||
}
|
||||
|
||||
impl Segments {
|
||||
pub fn parse(input: &str) -> Result<Segments, ()> {
|
||||
let caps = SEGMENTS_RE.captures(input).ok_or(())?;
|
||||
let ids_start = i32::from_str(caps.get(1).unwrap().as_str()).map_err(|_| ())?;
|
||||
let ids_end = match caps.get(2) {
|
||||
Some(m) => i32::from_str(&m.as_str()[1..]).map_err(|_| ())?,
|
||||
None => ids_start,
|
||||
} + 1;
|
||||
let open_id = match caps.get(3) {
|
||||
Some(m) => Some(u32::from_str(&m.as_str()[1..]).map_err(|_| ())?),
|
||||
None => None,
|
||||
};
|
||||
if ids_start < 0 || ids_end <= ids_start {
|
||||
/// Parses the `s` query parameter to `view.mp4` as described in `design/api.md`.
|
||||
/// Doesn't do any validation.
|
||||
fn parse(i: &str) -> IResult<&str, Segments> {
|
||||
// Parse START_ID[-END_ID] into Range<i32>.
|
||||
// Note that END_ID is inclusive, but Ranges are half-open.
|
||||
let (i, ids) = map(tuple((num::<i32>(), opt(preceded(tag("-"), num::<i32>())))),
|
||||
|(start, end)| start .. end.unwrap_or(start) + 1)(i)?;
|
||||
|
||||
// Parse [@OPEN_ID] into Option<u32>.
|
||||
let (i, open_id) = opt(preceded(tag("@"), num::<u32>()))(i)?;
|
||||
|
||||
// Parse [.[REL_START_TIME]-[REL_END_TIME]] into (i64, Option<i64>).
|
||||
let (i, (start_time, end_time)) = map(
|
||||
opt(preceded(tag("."), tuple((opt(num::<i64>()), tag("-"), opt(num::<i64>()))))),
|
||||
|t| {
|
||||
t.map(|(s, _, e)| (s.unwrap_or(0), e))
|
||||
.unwrap_or((0, None))
|
||||
})(i)?;
|
||||
|
||||
Ok((i, Segments { ids, open_id, start_time, end_time, }))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Segments {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (_, s) = all_consuming(Segments::parse)(s).map_err(|_| ())?;
|
||||
if s.ids.end <= s.ids.start {
|
||||
return Err(());
|
||||
}
|
||||
let start_time = caps.get(4).map_or(Ok(0), |m| i64::from_str(m.as_str())).map_err(|_| ())?;
|
||||
if start_time < 0 {
|
||||
return Err(());
|
||||
if let Some(e) = s.end_time {
|
||||
if e < s.start_time {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
let end_time = match caps.get(5) {
|
||||
Some(v) => {
|
||||
let e = i64::from_str(v.as_str()).map_err(|_| ())?;
|
||||
if e <= start_time {
|
||||
return Err(());
|
||||
}
|
||||
Some(e)
|
||||
},
|
||||
None => None
|
||||
};
|
||||
Ok(Segments {
|
||||
ids: ids_start .. ids_end,
|
||||
open_id,
|
||||
start_time,
|
||||
end_time,
|
||||
})
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -421,7 +419,7 @@ impl ServiceInner {
|
||||
let (key, value) = (key.borrow(), value.borrow());
|
||||
match key {
|
||||
"s" => {
|
||||
let s = Segments::parse(value).map_err(
|
||||
let s = Segments::from_str(value).map_err(
|
||||
|()| plain_response(StatusCode::BAD_REQUEST,
|
||||
format!("invalid s parameter: {}", value)))?;
|
||||
debug!("stream_view_mp4: appending s={:?}", s);
|
||||
@ -587,10 +585,10 @@ impl ServiceInner {
|
||||
}.to_owned();
|
||||
let mut l = self.db.lock();
|
||||
let is_secure = self.is_secure(req);
|
||||
let flags = (auth::SessionFlags::HttpOnly as i32) |
|
||||
(auth::SessionFlags::SameSite as i32) |
|
||||
(auth::SessionFlags::SameSiteStrict as i32) |
|
||||
if is_secure { auth::SessionFlags::Secure as i32 } else { 0 };
|
||||
let flags = (auth::SessionFlag::HttpOnly as i32) |
|
||||
(auth::SessionFlag::SameSite as i32) |
|
||||
(auth::SessionFlag::SameSiteStrict as i32) |
|
||||
if is_secure { auth::SessionFlag::Secure as i32 } else { 0 };
|
||||
let (sid, _) = l.login_by_password(authreq, &r.username, r.password, Some(domain),
|
||||
flags)
|
||||
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
||||
@ -796,7 +794,7 @@ async fn with_json_body(mut req: Request<hyper::Body>)
|
||||
|
||||
pub struct Config<'a> {
|
||||
pub db: Arc<db::Database>,
|
||||
pub ui_dir: Option<&'a str>,
|
||||
pub ui_dir: Option<&'a std::path::Path>,
|
||||
pub trust_forward_hdrs: bool,
|
||||
pub time_zone_name: String,
|
||||
pub allow_unauthenticated_permissions: Option<db::Permissions>,
|
||||
@ -839,12 +837,12 @@ impl Service {
|
||||
})))
|
||||
}
|
||||
|
||||
fn fill_ui_files(dir: &str, files: &mut HashMap<String, UiFile>) {
|
||||
fn fill_ui_files(dir: &std::path::Path, files: &mut HashMap<String, UiFile>) {
|
||||
let r = match fs::read_dir(dir) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!("Unable to search --ui-dir={}; will serve no static files. Error was: {}",
|
||||
dir, e);
|
||||
dir.display(), e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@ -1075,6 +1073,7 @@ mod tests {
|
||||
use futures::future::FutureExt;
|
||||
use log::info;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use super::Segments;
|
||||
|
||||
struct Server {
|
||||
@ -1221,25 +1220,25 @@ mod tests {
|
||||
fn test_segments() {
|
||||
testutil::init();
|
||||
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: None},
|
||||
Segments::parse("1").unwrap());
|
||||
Segments::from_str("1").unwrap());
|
||||
assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 0, end_time: None},
|
||||
Segments::parse("1@42").unwrap());
|
||||
Segments::from_str("1@42").unwrap());
|
||||
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: None},
|
||||
Segments::parse("1.26-").unwrap());
|
||||
Segments::from_str("1.26-").unwrap());
|
||||
assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 26, end_time: None},
|
||||
Segments::parse("1@42.26-").unwrap());
|
||||
Segments::from_str("1@42.26-").unwrap());
|
||||
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: Some(42)},
|
||||
Segments::parse("1.-42").unwrap());
|
||||
Segments::from_str("1.-42").unwrap());
|
||||
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: Some(42)},
|
||||
Segments::parse("1.26-42").unwrap());
|
||||
Segments::from_str("1.26-42").unwrap());
|
||||
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: None},
|
||||
Segments::parse("1-5").unwrap());
|
||||
Segments::from_str("1-5").unwrap());
|
||||
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: None},
|
||||
Segments::parse("1-5.26-").unwrap());
|
||||
Segments::from_str("1-5.26-").unwrap());
|
||||
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: Some(42)},
|
||||
Segments::parse("1-5.-42").unwrap());
|
||||
Segments::from_str("1-5.-42").unwrap());
|
||||
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: Some(42)},
|
||||
Segments::parse("1-5.26-42").unwrap());
|
||||
Segments::from_str("1-5.26-42").unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
Loading…
x
Reference in New Issue
Block a user