mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-13 16:03:22 -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(
|
pub fn update_signals(
|
||||||
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
|
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
|
||||||
-> Result<(), Error> {
|
-> Result<(), base::Error> {
|
||||||
self.signal.update_signals(when, signals, states)
|
self.signal.update_signals(when, signals, states)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,7 +255,7 @@ impl State {
|
|||||||
|
|
||||||
pub fn update_signals(
|
pub fn update_signals(
|
||||||
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
|
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
|
||||||
-> Result<(), Error> {
|
-> Result<(), base::Error> {
|
||||||
// Do input validation before any mutation.
|
// Do input validation before any mutation.
|
||||||
self.update_signals_validate(signals, states)?;
|
self.update_signals_validate(signals, states)?;
|
||||||
|
|
||||||
@ -274,7 +274,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper for `update_signals` to do validation.
|
/// 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() {
|
if signals.len() != states.len() {
|
||||||
bail_t!(InvalidArgument, "signals and states must have same length");
|
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 db::auth::SessionHash;
|
||||||
use failure::{Error, format_err};
|
use failure::{Error, format_err};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
|
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::ops::Not;
|
use std::ops::Not;
|
||||||
@ -111,6 +111,29 @@ pub struct Signal<'a> {
|
|||||||
pub short_name: &'a str,
|
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)]
|
#[derive(Default, Serialize)]
|
||||||
#[serde(rename_all="camelCase")]
|
#[serde(rename_all="camelCase")]
|
||||||
pub struct Signals {
|
pub struct Signals {
|
||||||
|
129
src/web.rs
129
src/web.rs
@ -629,7 +629,26 @@ impl ServiceInner {
|
|||||||
Ok(res)
|
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();
|
let mut time = recording::Time::min_value() .. recording::Time::max_value();
|
||||||
if let Some(q) = req.uri().query() {
|
if let Some(q) = req.uri().query() {
|
||||||
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
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
|
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 struct Config<'a> {
|
||||||
pub db: Arc<db::Database>,
|
pub db: Arc<db::Database>,
|
||||||
pub ui_dir: Option<&'a str>,
|
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,
|
fn stream_live_m4s(&self, _req: &Request<::hyper::Body>, uuid: Uuid,
|
||||||
stream_type: db::StreamType) -> ResponseResult {
|
stream_type: db::StreamType) -> ResponseResult {
|
||||||
let stream_id;
|
let stream_id;
|
||||||
@ -905,6 +951,21 @@ impl Service {
|
|||||||
.body(body)
|
.body(body)
|
||||||
.unwrap())
|
.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 {
|
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_))
|
wrap_r(true, self.stream_live_m4s(&req, uuid, type_))
|
||||||
},
|
},
|
||||||
Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
|
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();
|
let s = self.clone();
|
||||||
move |(req, b)| { s.0.login(&req, b) }
|
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();
|
let s = self.clone();
|
||||||
move |(req, b)| { s.0.logout(&req, b) }
|
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())),
|
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user