mirror of
				https://github.com/scottlamb/moonfire-nvr.git
				synced 2025-10-29 15:55:01 -04:00 
			
		
		
		
	fully implement json handling as in spec
This is a significant milestone; now the Rust branch matches the C++ branch's features. In the process, I switched from using serde_derive (which requires nightly Rust) to serde_codegen (which does not). It was easier than I thought it'd be. I'm getting close to no longer requiring nightly Rust.
This commit is contained in:
		
							parent
							
								
									678500bc88
								
							
						
					
					
						commit
						1865427f75
					
				
							
								
								
									
										72
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										72
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -22,7 +22,7 @@ dependencies = [ | ||||
|  "rusqlite 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "serde_derive 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "serde_codegen 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "serde_json 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "slog 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "slog-envlogger 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| @ -576,6 +576,8 @@ dependencies = [ | ||||
|  "quote 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "serde_codegen_internals 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "syn 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "syntex 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "syntex_syntax 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| @ -586,14 +588,6 @@ dependencies = [ | ||||
|  "syn 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "serde_derive" | ||||
| version = "0.8.19" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| dependencies = [ | ||||
|  "serde_codegen 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "serde_json" | ||||
| version = "0.8.3" | ||||
| @ -692,6 +686,51 @@ dependencies = [ | ||||
|  "unicode-xid 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "syntex" | ||||
| version = "0.50.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| dependencies = [ | ||||
|  "syntex_errors 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "syntex_syntax 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "syntex_errors" | ||||
| version = "0.50.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| dependencies = [ | ||||
|  "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "syntex_pos 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "unicode-xid 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "syntex_pos" | ||||
| version = "0.50.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| dependencies = [ | ||||
|  "rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "syntex_syntax" | ||||
| version = "0.50.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| dependencies = [ | ||||
|  "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "syntex_errors 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "syntex_pos 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "unicode-xid 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "tempdir" | ||||
| version = "0.3.5" | ||||
| @ -700,6 +739,15 @@ dependencies = [ | ||||
|  "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "term" | ||||
| version = "0.4.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| dependencies = [ | ||||
|  "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
|  "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "thread-id" | ||||
| version = "2.0.0" | ||||
| @ -884,7 +932,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| "checksum serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "58a19c0871c298847e6b68318484685cd51fa5478c0c905095647540031356e5" | ||||
| "checksum serde_codegen 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "ce29a6ae259579707650ec292199b5fed2c0b8e2a4bdc994452d24d1bcf2242a" | ||||
| "checksum serde_codegen_internals 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "59933a62554548c690d2673c5164f0c4a46be7c5731edfd94b0ecb1048940732" | ||||
| "checksum serde_derive 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "a4b541549c4207d3602c9abcc3e31252e91751674264eb85c103bb20197054b4" | ||||
| "checksum serde_json 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1cb6b19e74d9f65b9d03343730b643d729a446b29376785cd65efdff4675e2fc" | ||||
| "checksum slog 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "19138575064905f49832ef72c856d614a78c5b25881f93b48954bf593464b7f5" | ||||
| "checksum slog-envlogger 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfea715bb310c33c8f90e659bce5b95e39851348b9a7e2a77495a069662def78" | ||||
| @ -896,7 +943,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| "checksum solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "172382bac9424588d7840732b250faeeef88942e37b6e35317dce98cafdd75b2" | ||||
| "checksum strsim 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "67f84c44fbb2f91db7fef94554e6b2ac05909c9c0b0bc23bb98d3a1aebfe7f7c" | ||||
| "checksum syn 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94e7d81ecd16d39f16193af05b8d5a0111b9d8d2f3f78f31760f327a247da777" | ||||
| "checksum syntex 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bd253b0d7d787723a33384d426f0ebec7f8edccfaeb2022d0177162bb134da0" | ||||
| "checksum syntex_errors 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)" = "84822a1178204a191239ad844599f8c85c128cf9f4173397def4eb46b55b0aa1" | ||||
| "checksum syntex_pos 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a43abded5057c75bac8555e46ec913ce502efb418267b1ab8e9783897470c7db" | ||||
| "checksum syntex_syntax 0.50.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6ef781e4b60f03431f1b5b59843546ce60ae029a787770cf8e0969ac1fd063a5" | ||||
| "checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" | ||||
| "checksum term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3deff8a2b3b6607d6d7cc32ac25c0b33709453ca9cceac006caac51e963cf94a" | ||||
| "checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" | ||||
| "checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" | ||||
| "checksum time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "3c7ec6d62a20df54e07ab3b78b9a3932972f4b7981de295563686849eb3989af" | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
| name = "moonfire-nvr" | ||||
| version = "0.1.0" | ||||
| authors = ["Scott Lamb <slamb@slamb.org>"] | ||||
| build = "build.rs" | ||||
| 
 | ||||
| [dependencies] | ||||
| byteorder = "0.5" | ||||
| @ -23,7 +24,6 @@ rusqlite = "0.7" | ||||
| rustc-serialize = "0.3" | ||||
| serde = "0.8" | ||||
| serde_json = "0.8" | ||||
| serde_derive = "0.8" | ||||
| slog = "1.2" | ||||
| slog-envlogger = "0.5" | ||||
| slog-stdlog = "1.1" | ||||
| @ -33,6 +33,9 @@ time = "0.1" | ||||
| url = "1.2" | ||||
| uuid = { version = "0.3", features = ["serde", "v4"] } | ||||
| 
 | ||||
| [build-dependencies] | ||||
| serde_codegen = "0.8" | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| tempdir = "0.3" | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										43
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| // This file is part of Moonfire NVR, a security camera digital video recorder.
 | ||||
| // Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
 | ||||
| //
 | ||||
| // 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/>.
 | ||||
| 
 | ||||
| extern crate serde_codegen; | ||||
| 
 | ||||
| use std::env; | ||||
| use std::path::Path; | ||||
| 
 | ||||
| fn main() { | ||||
|     let out_dir = env::var_os("OUT_DIR").unwrap(); | ||||
| 
 | ||||
|     let src = Path::new("src/serde_types.in.rs"); | ||||
|     let dst = Path::new(&out_dir).join("serde_types.rs"); | ||||
| 
 | ||||
|     serde_codegen::expand(&src, &dst).unwrap(); | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/db.rs
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								src/db.rs
									
									
									
									
									
								
							| @ -61,7 +61,6 @@ use lru_cache::LruCache; | ||||
| use openssl::crypto::hash; | ||||
| use recording::{self, TIME_UNITS_PER_SEC}; | ||||
| use rusqlite; | ||||
| use serde::ser::{Serialize, Serializer}; | ||||
| use std::collections::BTreeMap; | ||||
| use std::cell::RefCell; | ||||
| use std::cmp; | ||||
| @ -231,18 +230,26 @@ impl CameraDayKey { | ||||
|         write!(&mut s.0[..], "{}", tm.strftime("%Y-%m-%d")?)?; | ||||
|         Ok(s) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Serialize for CameraDayKey { | ||||
|     /// Serializes as a string, not as the default bytes.
 | ||||
|     /// serde_json will only allow string keys for objects.
 | ||||
|     fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { | ||||
|         serializer.serialize_str(str::from_utf8(&self.0[..]).expect("days are always UTF-8")) | ||||
|     pub fn bounds(&self) -> Range<recording::Time> { | ||||
|         let mut my_tm = time::strptime(self.as_ref(), "%Y-%m-%d").expect("days must be parseable"); | ||||
|         let start = recording::Time(my_tm.to_timespec().sec * recording::TIME_UNITS_PER_SEC); | ||||
|         my_tm.tm_isdst = -1; | ||||
|         my_tm.tm_hour = 0; | ||||
|         my_tm.tm_min = 0; | ||||
|         my_tm.tm_sec = 0; | ||||
|         my_tm.tm_mday += 1; | ||||
|         let end = recording::Time(my_tm.to_timespec().sec * recording::TIME_UNITS_PER_SEC); | ||||
|         start .. end | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl AsRef<str> for CameraDayKey { | ||||
|     fn as_ref(&self) -> &str { str::from_utf8(&self.0[..]).expect("days are always UTF-8") } | ||||
| } | ||||
| 
 | ||||
| /// In-memory state about a particular camera on a particular day.
 | ||||
| #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] | ||||
| #[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||||
| pub struct CameraDayValue { | ||||
|     /// The number of recordings that overlap with this day. Note that `adjust_day` automatically
 | ||||
|     /// prunes days with 0 recordings.
 | ||||
| @ -255,7 +262,7 @@ pub struct CameraDayValue { | ||||
| } | ||||
| 
 | ||||
| /// In-memory state about a camera.
 | ||||
| #[derive(Debug, Serialize)] | ||||
| #[derive(Debug)] | ||||
| pub struct Camera { | ||||
|     pub id: i32, | ||||
|     pub uuid: Uuid, | ||||
| @ -270,7 +277,6 @@ pub struct Camera { | ||||
| 
 | ||||
|     /// The time range of recorded data associated with this camera (minimum start time and maximum
 | ||||
|     /// end time). `None` iff there are no recordings for this camera.
 | ||||
|     #[serde(skip_serializing)] | ||||
|     pub range: Option<Range<recording::Time>>, | ||||
|     pub sample_file_bytes: i64, | ||||
| 
 | ||||
|  | ||||
| @ -52,7 +52,6 @@ extern crate openssl; | ||||
| extern crate regex; | ||||
| extern crate rustc_serialize; | ||||
| extern crate serde; | ||||
| #[macro_use] extern crate serde_derive; | ||||
| extern crate serde_json; | ||||
| extern crate slog; | ||||
| extern crate slog_envlogger; | ||||
| @ -82,6 +81,7 @@ mod recording; | ||||
| mod resource; | ||||
| mod stream; | ||||
| mod streamer; | ||||
| mod strutil; | ||||
| #[cfg(test)] mod testutil; | ||||
| mod web; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										24
									
								
								src/mp4.rs
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								src/mp4.rs
									
									
									
									
									
								
							| @ -99,6 +99,7 @@ use std::io; | ||||
| use std::ops::Range; | ||||
| use std::mem; | ||||
| use std::sync::{Arc, MutexGuard}; | ||||
| use strutil; | ||||
| use time::Timespec; | ||||
| 
 | ||||
| /// This value should be incremented any time a change is made to this file that causes different
 | ||||
| @ -513,18 +514,6 @@ macro_rules! write_length { | ||||
|     }} | ||||
| } | ||||
| 
 | ||||
| /// Returns a hex-encoded version of the input.
 | ||||
| fn hex(raw: &[u8]) -> String { | ||||
|     const HEX_CHARS: [u8; 16] = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', | ||||
|                                  b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f']; | ||||
|     let mut hex = Vec::with_capacity(2 * raw.len()); | ||||
|     for b in raw { | ||||
|         hex.push(HEX_CHARS[((b & 0xf0) >> 4) as usize]); | ||||
|         hex.push(HEX_CHARS[( b & 0x0f      ) as usize]); | ||||
|     } | ||||
|     unsafe { String::from_utf8_unchecked(hex) } | ||||
| } | ||||
| 
 | ||||
| impl Mp4FileBuilder { | ||||
|     pub fn new() -> Self { | ||||
|         Mp4FileBuilder{ | ||||
| @ -669,7 +658,7 @@ impl Mp4FileBuilder { | ||||
|             video_sample_entries: self.video_sample_entries, | ||||
|             initial_sample_byte_pos: initial_sample_byte_pos, | ||||
|             last_modified: header::HttpDate(time::at(Timespec::new(max_end.unix_seconds(), 0))), | ||||
|             etag: header::EntityTag::strong(hex(&etag.finish()?)), | ||||
|             etag: header::EntityTag::strong(strutil::hex(&etag.finish()?)), | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| @ -1202,6 +1191,7 @@ mod tests { | ||||
|     use std::path::Path; | ||||
|     use std::sync::Arc; | ||||
|     use std::str; | ||||
|     use strutil; | ||||
|     use super::*; | ||||
|     use stream::{self, Opener, Stream}; | ||||
|     use testutil::{self, TestDb}; | ||||
| @ -1663,7 +1653,7 @@ mod tests { | ||||
|         // here fails, it can be updated, but the etag must change as well! Otherwise clients may
 | ||||
|         // combine ranges from the new format with ranges from the old format.
 | ||||
|         let sha1 = digest(&mp4); | ||||
|         assert_eq!("1e5331e8371bd97ac3158b3a86494abc87cdc70e", super::hex(&sha1[..])); | ||||
|         assert_eq!("1e5331e8371bd97ac3158b3a86494abc87cdc70e", strutil::hex(&sha1[..])); | ||||
|         const EXPECTED_ETAG: &'static str = "3c48af4dbce2024db07f27a00789b6af774a8c89"; | ||||
|         assert_eq!(Some(&header::EntityTag::strong(EXPECTED_ETAG.to_owned())), mp4.etag()); | ||||
|         drop(db.syncer_channel); | ||||
| @ -1683,7 +1673,7 @@ mod tests { | ||||
|         // here fails, it can be updated, but the etag must change as well! Otherwise clients may
 | ||||
|         // combine ranges from the new format with ranges from the old format.
 | ||||
|         let sha1 = digest(&mp4); | ||||
|         assert_eq!("de382684a471f178e4e3a163762711b0653bfd83", super::hex(&sha1[..])); | ||||
|         assert_eq!("de382684a471f178e4e3a163762711b0653bfd83", strutil::hex(&sha1[..])); | ||||
|         const EXPECTED_ETAG: &'static str = "c24d7af372e5d8f66f4feb6e3a5cd43828392371"; | ||||
|         assert_eq!(Some(&header::EntityTag::strong(EXPECTED_ETAG.to_owned())), mp4.etag()); | ||||
|         drop(db.syncer_channel); | ||||
| @ -1703,7 +1693,7 @@ mod tests { | ||||
|         // here fails, it can be updated, but the etag must change as well! Otherwise clients may
 | ||||
|         // combine ranges from the new format with ranges from the old format.
 | ||||
|         let sha1 = digest(&mp4); | ||||
|         assert_eq!("685e026af44204bc9cc52115c5e17058e9fb7c70", super::hex(&sha1[..])); | ||||
|         assert_eq!("685e026af44204bc9cc52115c5e17058e9fb7c70", strutil::hex(&sha1[..])); | ||||
|         const EXPECTED_ETAG: &'static str = "870e2b3cfef4a988951344b32e53af0d4496894d"; | ||||
|         assert_eq!(Some(&header::EntityTag::strong(EXPECTED_ETAG.to_owned())), mp4.etag()); | ||||
|         drop(db.syncer_channel); | ||||
| @ -1723,7 +1713,7 @@ mod tests { | ||||
|         // here fails, it can be updated, but the etag must change as well! Otherwise clients may
 | ||||
|         // combine ranges from the new format with ranges from the old format.
 | ||||
|         let sha1 = digest(&mp4); | ||||
|         assert_eq!("e0d28ddf08e24575a82657b1ce0b2da73f32fd88", super::hex(&sha1[..])); | ||||
|         assert_eq!("e0d28ddf08e24575a82657b1ce0b2da73f32fd88", strutil::hex(&sha1[..])); | ||||
|         const EXPECTED_ETAG: &'static str = "71c329188a2cd175c8d61492a9789e242af06c05"; | ||||
|         assert_eq!(Some(&header::EntityTag::strong(EXPECTED_ETAG.to_owned())), mp4.etag()); | ||||
|         drop(db.syncer_channel); | ||||
|  | ||||
| @ -46,7 +46,7 @@ pub const DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC; | ||||
| pub const MAX_RECORDING_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC; | ||||
| 
 | ||||
| /// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
 | ||||
| #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] | ||||
| #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] | ||||
| pub struct Time(pub i64); | ||||
| 
 | ||||
| impl Time { | ||||
| @ -86,7 +86,7 @@ impl fmt::Display for Time { | ||||
| 
 | ||||
| /// 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, Eq, Ord, PartialEq, PartialOrd, Serialize)] | ||||
| #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] | ||||
| pub struct Duration(pub i64); | ||||
| 
 | ||||
| impl fmt::Display for Duration { | ||||
|  | ||||
							
								
								
									
										131
									
								
								src/serde_types.in.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/serde_types.in.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | ||||
| // This file is part of Moonfire NVR, a security camera digital video recorder.
 | ||||
| // Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
 | ||||
| //
 | ||||
| // 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/>.
 | ||||
| 
 | ||||
| use db; | ||||
| use serde::ser::Serializer; | ||||
| use std::collections::BTreeMap; | ||||
| use uuid::Uuid; | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct ListCameras<'a> { | ||||
|     // Use a custom serializer which presents the map's values as a sequence.
 | ||||
|     #[serde(serialize_with = "ListCameras::serialize_cameras")] | ||||
|     pub cameras: &'a BTreeMap<i32, db::Camera>, | ||||
| } | ||||
| 
 | ||||
| /// JSON serialization wrapper for a single camera when processing `/cameras/` and
 | ||||
| /// `/cameras/<uuid>/`. See `design/api.md` for details.
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct Camera<'a> { | ||||
|     pub uuid: Uuid, | ||||
|     pub short_name: &'a str, | ||||
|     pub description: &'a str, | ||||
|     pub retain_bytes: i64, | ||||
|     pub min_start_time_90k: Option<i64>, | ||||
|     pub max_end_time_90k: Option<i64>, | ||||
|     pub total_duration_90k: i64, | ||||
|     pub total_sample_file_bytes: i64, | ||||
| 
 | ||||
|     #[serde(serialize_with = "Camera::serialize_days")] | ||||
|     pub days: Option<&'a BTreeMap<db::CameraDayKey, db::CameraDayValue>>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> Camera<'a> { | ||||
|     pub fn new(c: &'a db::Camera, include_days: bool) -> Self { | ||||
|         Camera{ | ||||
|             uuid: c.uuid, | ||||
|             short_name: &c.short_name, | ||||
|             description: &c.description, | ||||
|             retain_bytes: c.retain_bytes, | ||||
|             min_start_time_90k: c.range.as_ref().map(|r| r.start.0), | ||||
|             max_end_time_90k: c.range.as_ref().map(|r| r.end.0), | ||||
|             total_duration_90k: c.duration.0, | ||||
|             total_sample_file_bytes: c.sample_file_bytes, | ||||
|             days: if include_days { Some(&c.days) } else { None }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn serialize_days<S>(days: &Option<&BTreeMap<db::CameraDayKey, db::CameraDayValue>>, | ||||
|                          serializer: &mut S) -> Result<(), S::Error> | ||||
|     where S: Serializer { | ||||
|         let days = match *days { | ||||
|             Some(d) => d, | ||||
|             None => return Ok(()), | ||||
|         }; | ||||
|         let mut state = serializer.serialize_map(Some(days.len()))?; | ||||
|         for (k, v) in days { | ||||
|             serializer.serialize_map_key(&mut state, k.as_ref())?; | ||||
|             let bounds = k.bounds(); | ||||
|             serializer.serialize_map_value(&mut state, &CameraDayValue{ | ||||
|                 start_time_90k: bounds.start.0, | ||||
|                 end_time_90k: bounds.end.0, | ||||
|                 total_duration_90k: v.duration.0, | ||||
|             })?; | ||||
|         } | ||||
|         serializer.serialize_map_end(state) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| struct CameraDayValue { | ||||
|     pub start_time_90k: i64, | ||||
|     pub end_time_90k: i64, | ||||
|     pub total_duration_90k: i64, | ||||
| } | ||||
| 
 | ||||
| impl<'a> ListCameras<'a> { | ||||
|     /// Serializes cameras as a list (rather than a map), wrapping each camera in the
 | ||||
|     /// `ListCamerasCamera` type to tweak the data returned.
 | ||||
|     fn serialize_cameras<S>(cameras: &BTreeMap<i32, db::Camera>, | ||||
|                             serializer: &mut S) -> Result<(), S::Error> | ||||
|     where S: Serializer { | ||||
|         let mut state = serializer.serialize_seq(Some(cameras.len()))?; | ||||
|         for c in cameras.values() { | ||||
|             serializer.serialize_seq_elt(&mut state, &Camera::new(c, false))?; | ||||
|         } | ||||
|         serializer.serialize_seq_end(state) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct ListRecordings { | ||||
|     pub recordings: Vec<Recording>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct Recording { | ||||
|     pub start_time_90k: i64, | ||||
|     pub end_time_90k: i64, | ||||
|     pub sample_file_bytes: i64, | ||||
|     pub video_samples: i64, | ||||
|     pub video_sample_entry_width: u16, | ||||
|     pub video_sample_entry_height: u16, | ||||
|     pub video_sample_entry_sha1: String, | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/strutil.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/strutil.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| // This file is part of Moonfire NVR, a security camera digital video recorder.
 | ||||
| // Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
 | ||||
| //
 | ||||
| // 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/>.
 | ||||
| 
 | ||||
| /// Returns a hex-encoded version of the input.
 | ||||
| pub fn hex(raw: &[u8]) -> String { | ||||
|     const HEX_CHARS: [u8; 16] = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', | ||||
|                                  b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f']; | ||||
|     let mut hex = Vec::with_capacity(2 * raw.len()); | ||||
|     for b in raw { | ||||
|         hex.push(HEX_CHARS[((b & 0xf0) >> 4) as usize]); | ||||
|         hex.push(HEX_CHARS[( b & 0x0f      ) as usize]); | ||||
|     } | ||||
|     unsafe { String::from_utf8_unchecked(hex) } | ||||
| } | ||||
							
								
								
									
										92
									
								
								src/web.rs
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								src/web.rs
									
									
									
									
									
								
							| @ -42,12 +42,11 @@ use mp4; | ||||
| use recording; | ||||
| use resource; | ||||
| use serde_json; | ||||
| use serde::ser::Serializer; | ||||
| use std::collections::BTreeMap; | ||||
| use std::fmt; | ||||
| use std::io::Write; | ||||
| use std::result; | ||||
| use std::ops::Range; | ||||
| use std::sync::{Arc,MutexGuard}; | ||||
| use strutil; | ||||
| use time; | ||||
| use url::form_urlencoded; | ||||
| use uuid::Uuid; | ||||
| @ -60,6 +59,8 @@ lazy_static! { | ||||
|     static ref HTML: mime::Mime = mime!(Text/Html); | ||||
| } | ||||
| 
 | ||||
| mod json { include!(concat!(env!("OUT_DIR"), "/serde_types.rs")); } | ||||
| 
 | ||||
| enum Path { | ||||
|     CamerasList,              // "/" or "/cameras/"
 | ||||
|     Camera(Uuid),             // "/cameras/<uuid>/"
 | ||||
| @ -180,25 +181,6 @@ pub struct Handler { | ||||
|     dir: Arc<SampleFileDir>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Serialize)] | ||||
| struct ListCameras<'a> { | ||||
|     // Use a custom serializer which presents the map's values as a sequence.
 | ||||
|     #[serde(serialize_with = "ListCameras::serialize_cameras")] | ||||
|     cameras: &'a BTreeMap<i32, db::Camera>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> ListCameras<'a> { | ||||
|     fn serialize_cameras<S>(cameras: &BTreeMap<i32, db::Camera>, | ||||
|                             serializer: &mut S) -> result::Result<(), S::Error> | ||||
|     where S: Serializer { | ||||
|         let mut state = serializer.serialize_seq(Some(cameras.len()))?; | ||||
|         for c in cameras.values() { | ||||
|             serializer.serialize_seq_elt(&mut state, c)?; | ||||
|         } | ||||
|         serializer.serialize_seq_end(state) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Handler { | ||||
|     pub fn new(db: Arc<db::Database>, dir: Arc<SampleFileDir>) -> Self { | ||||
|         Handler{db: db, dir: dir} | ||||
| @ -215,7 +197,7 @@ impl Handler { | ||||
|         let buf = { | ||||
|             let db = self.db.lock(); | ||||
|             if json { | ||||
|                 serde_json::to_vec(&ListCameras{cameras: db.cameras_by_id()})? | ||||
|                 serde_json::to_vec(&json::ListCameras{cameras: db.cameras_by_id()})? | ||||
|             } else { | ||||
|                 self.list_cameras_html(db)? | ||||
|             } | ||||
| @ -259,16 +241,17 @@ impl Handler { | ||||
|         Ok(buf) | ||||
|     } | ||||
| 
 | ||||
|     fn camera(&self, uuid: Uuid, req: &server::Request, mut res: server::Response) -> Result<()> { | ||||
|     fn camera(&self, uuid: Uuid, query: &str, req: &server::Request, mut res: server::Response) | ||||
|               -> Result<()> { | ||||
|         let json = is_json(req); | ||||
|         let buf = { | ||||
|             let db = self.db.lock(); | ||||
|             if json { | ||||
|                 let camera = db.get_camera(uuid) | ||||
|                                .ok_or_else(|| Error::new("no such camera".to_owned()))?; | ||||
|                 serde_json::to_vec(&camera)? | ||||
|                 serde_json::to_vec(&json::Camera::new(camera, true))? | ||||
|             } else { | ||||
|                 self.camera_html(db, uuid)? | ||||
|                 self.camera_html(db, query, uuid)? | ||||
|             } | ||||
|         }; | ||||
|         res.headers_mut().set(header::ContentType(if json { JSON.clone() } else { HTML.clone() })); | ||||
| @ -276,7 +259,9 @@ impl Handler { | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn camera_html(&self, db: MutexGuard<db::LockedDatabase>, uuid: Uuid) -> Result<Vec<u8>> { | ||||
|     fn camera_html(&self, db: MutexGuard<db::LockedDatabase>, query: &str, | ||||
|                    uuid: Uuid) -> Result<Vec<u8>> { | ||||
|         let r = Handler::get_optional_range(query)?; | ||||
|         let camera = db.get_camera(uuid) | ||||
|                        .ok_or_else(|| Error::new("no such camera".to_owned()))?; | ||||
|         let mut buf = Vec::new(); | ||||
| @ -299,7 +284,6 @@ impl Handler { | ||||
|             <th>fps</th><th>size</th><th>bitrate</th>\ | ||||
|             </tr>\n",
 | ||||
|             HtmlEscaped(&camera.short_name), HtmlEscaped(&camera.description))?; | ||||
|         let r = recording::Time(i64::min_value()) .. recording::Time(i64::max_value()); | ||||
| 
 | ||||
|         // Rather than listing each 60-second recording, generate a HTML row for aggregated .mp4
 | ||||
|         // files of up to FORCE_SPLIT_DURATION each, provided there is no gap or change in video
 | ||||
| @ -324,10 +308,36 @@ impl Handler { | ||||
|         Ok(buf) | ||||
|     } | ||||
| 
 | ||||
|     fn camera_recordings(&self, _uuid: Uuid, _req: &server::Request, | ||||
|     fn camera_recordings(&self, uuid: Uuid, query: &str, req: &server::Request, | ||||
|                          mut res: server::Response) -> Result<()> { | ||||
|         *res.status_mut() = status::StatusCode::NotImplemented; | ||||
|         res.send(b"not implemented")?; | ||||
|         let r = Handler::get_optional_range(query)?; | ||||
|         if !is_json(req) { | ||||
|             *res.status_mut() = status::StatusCode::NotAcceptable; | ||||
|             res.send(b"only available for JSON requests")?; | ||||
|             return Ok(()); | ||||
|         } | ||||
|         let mut out = json::ListRecordings{recordings: Vec::new()}; | ||||
|         { | ||||
|             let db = self.db.lock(); | ||||
|             let camera = db.get_camera(uuid) | ||||
|                            .ok_or_else(|| Error::new("no such camera".to_owned()))?; | ||||
|             db.list_aggregated_recordings(camera.id, &r, recording::Duration(i64::max_value()), | ||||
|                                           |row| { | ||||
|                 out.recordings.push(json::Recording{ | ||||
|                     start_time_90k: row.range.start.0, | ||||
|                     end_time_90k: row.range.end.0, | ||||
|                     sample_file_bytes: row.sample_file_bytes, | ||||
|                     video_samples: row.video_samples, | ||||
|                     video_sample_entry_width: row.video_sample_entry.width, | ||||
|                     video_sample_entry_height: row.video_sample_entry.height, | ||||
|                     video_sample_entry_sha1: strutil::hex(&row.video_sample_entry.sha1), | ||||
|                 }); | ||||
|                 Ok(()) | ||||
|             })?; | ||||
|         } | ||||
|         let buf = serde_json::to_vec(&out)?; | ||||
|         res.headers_mut().set(header::ContentType(JSON.clone())); | ||||
|         res.send(&buf)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
| @ -409,6 +419,22 @@ impl Handler { | ||||
|         resource::serve(&mp4, req, res)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Parses optional `start_time_90k` and `end_time_90k` query parameters, defaulting to the
 | ||||
|     /// full range of possible values.
 | ||||
|     fn get_optional_range(query: &str) -> Result<Range<recording::Time>> { | ||||
|         let mut start = i64::min_value(); | ||||
|         let mut end = i64::max_value(); | ||||
|         for (key, value) in form_urlencoded::parse(query.as_bytes()) { | ||||
|             let (key, value) = (key.borrow(), value.borrow()); | ||||
|             match key { | ||||
|                 "start_time_90k" => start = i64::from_str(value)?, | ||||
|                 "end_time_90k" => end = i64::from_str(value)?, | ||||
|                 _ => {}, | ||||
|             } | ||||
|         }; | ||||
|         Ok(recording::Time(start) .. recording::Time(end)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl server::Handler for Handler { | ||||
| @ -416,8 +442,8 @@ impl server::Handler for Handler { | ||||
|         let (path, query) = get_path_and_query(&req.uri); | ||||
|         let res = match decode_path(path) { | ||||
|             Path::CamerasList => self.list_cameras(&req, res), | ||||
|             Path::Camera(uuid) => self.camera(uuid, &req, res), | ||||
|             Path::CameraRecordings(uuid) => self.camera_recordings(uuid, &req, res), | ||||
|             Path::Camera(uuid) => self.camera(uuid, query, &req, res), | ||||
|             Path::CameraRecordings(uuid) => self.camera_recordings(uuid, query, &req, res), | ||||
|             Path::CameraViewMp4(uuid) => self.camera_view_mp4(uuid, query, &req, res), | ||||
|             Path::NotFound => self.not_found(res), | ||||
|         }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user