mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-13 07:53:22 -05: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…
Reference in New Issue
Block a user