mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2024-12-25 22:55:55 -05:00
web api glue for updating signals
This is very lightly tested, but it at least sometimes works.
This commit is contained in:
parent
7fe9d34655
commit
6d4b06f7d2
2
db/db.rs
2
db/db.rs
@ -1801,7 +1801,7 @@ impl LockedDatabase {
|
||||
}
|
||||
pub fn update_signals(
|
||||
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
|
||||
-> Result<(), Error> {
|
||||
-> Result<(), base::Error> {
|
||||
self.signal.update_signals(when, signals, states)
|
||||
}
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ impl State {
|
||||
|
||||
pub fn update_signals(
|
||||
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
|
||||
-> Result<(), Error> {
|
||||
-> Result<(), base::Error> {
|
||||
// Do input validation before any mutation.
|
||||
self.update_signals_validate(signals, states)?;
|
||||
|
||||
@ -274,7 +274,7 @@ impl State {
|
||||
}
|
||||
|
||||
/// Helper for `update_signals` to do validation.
|
||||
fn update_signals_validate(&self, signals: &[u32], states: &[u16]) -> Result<(), Error> {
|
||||
fn update_signals_validate(&self, signals: &[u32], states: &[u16]) -> Result<(), base::Error> {
|
||||
if signals.len() != states.len() {
|
||||
bail_t!(InvalidArgument, "signals and states must have same length");
|
||||
}
|
||||
|
25
src/json.rs
25
src/json.rs
@ -30,7 +30,7 @@
|
||||
|
||||
use db::auth::SessionHash;
|
||||
use failure::{Error, format_err};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Not;
|
||||
@ -111,6 +111,29 @@ pub struct Signal<'a> {
|
||||
pub short_name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub enum PostSignalsEndBase {
|
||||
Epoch,
|
||||
Now,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub struct PostSignalsRequest {
|
||||
pub signal_ids: Vec<u32>,
|
||||
pub states: Vec<u16>,
|
||||
pub start_time_90k: Option<i64>,
|
||||
pub end_base: PostSignalsEndBase,
|
||||
pub rel_end_time_90k: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub struct PostSignalsResponse {
|
||||
pub time_90k: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
#[serde(rename_all="camelCase")]
|
||||
pub struct Signals {
|
||||
|
129
src/web.rs
129
src/web.rs
@ -629,7 +629,26 @@ impl ServiceInner {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
|
||||
fn post_signals(&self, req: &Request<hyper::Body>, body: hyper::Chunk) -> ResponseResult {
|
||||
let r: json::PostSignalsRequest = serde_json::from_slice(&body)
|
||||
.map_err(|e| bad_req(e.to_string()))?;
|
||||
let mut l = self.db.lock();
|
||||
let now = recording::Time::new(self.db.clocks().realtime());
|
||||
let start = r.start_time_90k.map(recording::Time).unwrap_or(now);
|
||||
let end = match r.end_base {
|
||||
json::PostSignalsEndBase::Epoch => recording::Time(r.rel_end_time_90k.ok_or_else(
|
||||
|| bad_req("must specify rel_end_time_90k when end_base is epoch"))?),
|
||||
json::PostSignalsEndBase::Now => {
|
||||
now + recording::Duration(r.rel_end_time_90k.unwrap_or(0))
|
||||
},
|
||||
};
|
||||
l.update_signals(start .. end, &r.signal_ids, &r.states).map_err(from_base_error)?;
|
||||
serve_json(req, &json::PostSignalsResponse {
|
||||
time_90k: now.0,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
|
||||
let mut time = recording::Time::min_value() .. recording::Time::max_value();
|
||||
if let Some(q) = req.uri().query() {
|
||||
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
||||
@ -704,6 +723,63 @@ fn extract_sid(req: &Request<hyper::Body>) -> Option<auth::RawSessionId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a future separating the request from its form body.
|
||||
///
|
||||
/// If this is not a `POST` or the body's `Content-Type` is not
|
||||
/// `application/x-www-form-urlencoded`, returns an appropriate error response instead.
|
||||
///
|
||||
/// Use with `and_then` to chain logic which consumes the form body.
|
||||
fn with_form_body(mut req: Request<hyper::Body>)
|
||||
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
|
||||
Error = Response<Body>> +
|
||||
Send + 'static> {
|
||||
if *req.method() != http::method::Method::POST {
|
||||
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST expected")));
|
||||
}
|
||||
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
||||
Some(t) if t == "application/x-www-form-urlencoded" => true,
|
||||
Some(t) if t == "application/x-www-form-urlencoded; charset=UTF-8" => true,
|
||||
_ => false,
|
||||
};
|
||||
if !correct_mime_type {
|
||||
return Box::new(future::err(bad_req(
|
||||
"expected application/x-www-form-urlencoded request body")));
|
||||
}
|
||||
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
||||
Box::new(b.concat2()
|
||||
.map(|b| (req, b))
|
||||
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
|
||||
e))))
|
||||
}
|
||||
|
||||
// TODO: remove redundancy with above. Probably better to just always expect requests in json
|
||||
// format rather than using the form style for login/logout.
|
||||
fn with_json_body(mut req: Request<hyper::Body>)
|
||||
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
|
||||
Error = Response<Body>> +
|
||||
Send + 'static> {
|
||||
if *req.method() != http::method::Method::POST {
|
||||
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST expected")));
|
||||
}
|
||||
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
||||
Some(t) if t == "application/json" => true,
|
||||
Some(t) if t == "application/json; charset=UTF-8" => true,
|
||||
_ => false,
|
||||
};
|
||||
if !correct_mime_type {
|
||||
return Box::new(future::err(bad_req(
|
||||
"expected application/json request body")));
|
||||
}
|
||||
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
||||
Box::new(b.concat2()
|
||||
.map(|b| (req, b))
|
||||
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
|
||||
e))))
|
||||
}
|
||||
|
||||
|
||||
pub struct Config<'a> {
|
||||
pub db: Arc<db::Database>,
|
||||
pub ui_dir: Option<&'a str>,
|
||||
@ -791,36 +867,6 @@ impl Service {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a future separating the request from its form body.
|
||||
///
|
||||
/// If this is not a `POST` or the body's `Content-Type` is not
|
||||
/// `application/x-www-form-urlencoded`, returns an appropriate error response instead.
|
||||
///
|
||||
/// Use with `and_then` to chain logic which consumes the form body.
|
||||
fn with_form_body(&self, mut req: Request<hyper::Body>)
|
||||
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
|
||||
Error = Response<Body>> +
|
||||
Send + 'static> {
|
||||
if *req.method() != http::method::Method::POST {
|
||||
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST expected")));
|
||||
}
|
||||
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
||||
Some(t) if t == "application/x-www-form-urlencoded" => true,
|
||||
Some(t) if t == "application/x-www-form-urlencoded; charset=UTF-8" => true,
|
||||
_ => false,
|
||||
};
|
||||
if !correct_mime_type {
|
||||
return Box::new(future::err(bad_req(
|
||||
"expected application/x-www-form-urlencoded request body")));
|
||||
}
|
||||
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
|
||||
Box::new(b.concat2()
|
||||
.map(|b| (req, b))
|
||||
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
|
||||
e))))
|
||||
}
|
||||
|
||||
fn stream_live_m4s(&self, _req: &Request<::hyper::Body>, uuid: Uuid,
|
||||
stream_type: db::StreamType) -> ResponseResult {
|
||||
let stream_id;
|
||||
@ -905,6 +951,21 @@ impl Service {
|
||||
.body(body)
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
fn signals(&self, req: Request<hyper::Body>)
|
||||
-> Box<dyn Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static> {
|
||||
use http::method::Method;
|
||||
match *req.method() {
|
||||
Method::POST => Box::new(with_json_body(req)
|
||||
.and_then({
|
||||
let s = self.0.clone();
|
||||
move |(req, b)| s.post_signals(&req, b)
|
||||
})),
|
||||
Method::GET | Method::HEAD => Box::new(future::result(self.0.get_signals(&req))),
|
||||
_ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST, GET, or HEAD expected"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ::hyper::service::Service for Service {
|
||||
@ -963,15 +1024,15 @@ impl ::hyper::service::Service for Service {
|
||||
wrap_r(true, self.stream_live_m4s(&req, uuid, type_))
|
||||
},
|
||||
Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
|
||||
Path::Login => wrap(true, self.with_form_body(req).and_then({
|
||||
Path::Login => wrap(true, with_form_body(req).and_then({
|
||||
let s = self.clone();
|
||||
move |(req, b)| { s.0.login(&req, b) }
|
||||
})),
|
||||
Path::Logout => wrap(true, self.with_form_body(req).and_then({
|
||||
Path::Logout => wrap(true, with_form_body(req).and_then({
|
||||
let s = self.clone();
|
||||
move |(req, b)| { s.0.logout(&req, b) }
|
||||
})),
|
||||
Path::Signals => wrap_r(true, self.0.signals(&req)),
|
||||
Path::Signals => wrap(true, self.signals(req)),
|
||||
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user