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:
Scott Lamb 2016-12-08 21:28:50 -08:00
parent 678500bc88
commit 1865427f75
10 changed files with 366 additions and 74 deletions

72
Cargo.lock generated
View File

@ -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"

View File

@ -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
View 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();
}

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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
View 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
View 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) }
}

View File

@ -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),
};