diff --git a/Cargo.lock b/Cargo.lock index 2738448..806b8a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index c138e6b..f09a9f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "moonfire-nvr" version = "0.1.0" authors = ["Scott Lamb "] +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" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b92d133 --- /dev/null +++ b/build.rs @@ -0,0 +1,43 @@ +// This file is part of Moonfire NVR, a security camera digital video recorder. +// Copyright (C) 2016 Scott Lamb +// +// 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 . + +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(); +} diff --git a/src/db.rs b/src/db.rs index b879256..05b254e 100644 --- a/src/db.rs +++ b/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(&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 { + 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 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>, pub sample_file_bytes: i64, diff --git a/src/main.rs b/src/main.rs index 28b3811..d0501a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; diff --git a/src/mp4.rs b/src/mp4.rs index 60fa38b..386d573 100644 --- a/src/mp4.rs +++ b/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); diff --git a/src/recording.rs b/src/recording.rs index 8497d01..326ca89 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -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 { diff --git a/src/serde_types.in.rs b/src/serde_types.in.rs new file mode 100644 index 0000000..d4408ef --- /dev/null +++ b/src/serde_types.in.rs @@ -0,0 +1,131 @@ +// This file is part of Moonfire NVR, a security camera digital video recorder. +// Copyright (C) 2016 Scott Lamb +// +// 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 . + +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, +} + +/// JSON serialization wrapper for a single camera when processing `/cameras/` and +/// `/cameras//`. 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, + pub max_end_time_90k: Option, + pub total_duration_90k: i64, + pub total_sample_file_bytes: i64, + + #[serde(serialize_with = "Camera::serialize_days")] + pub days: Option<&'a BTreeMap>, +} + +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(days: &Option<&BTreeMap>, + 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(cameras: &BTreeMap, + 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, +} + +#[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, +} diff --git a/src/strutil.rs b/src/strutil.rs new file mode 100644 index 0000000..c4bbbb0 --- /dev/null +++ b/src/strutil.rs @@ -0,0 +1,41 @@ +// This file is part of Moonfire NVR, a security camera digital video recorder. +// Copyright (C) 2016 Scott Lamb +// +// 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 . + +/// 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) } +} diff --git a/src/web.rs b/src/web.rs index ed47aa2..965ffcc 100644 --- a/src/web.rs +++ b/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//" @@ -180,25 +181,6 @@ pub struct Handler { dir: Arc, } -#[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, -} - -impl<'a> ListCameras<'a> { - fn serialize_cameras(cameras: &BTreeMap, - 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, dir: Arc) -> 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, uuid: Uuid) -> Result> { + fn camera_html(&self, db: MutexGuard, query: &str, + uuid: Uuid) -> Result> { + 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 { fpssizebitrate\ \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> { + 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), };