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