mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-28 21:18:11 -05:00
new "moonfire-nvr config" subcommand
This is a ncurses-based user interface for configuration. This fills a major usability gap: the system can be configured without manual SQL commands.
This commit is contained in:
270
src/cmds/config/cameras.rs
Normal file
270
src/cmds/config/cameras.rs
Normal file
@@ -0,0 +1,270 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2017 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 cursive;
|
||||
|
||||
use self::cursive::Cursive;
|
||||
use self::cursive::traits::{Boxable, Identifiable, Finder};
|
||||
use self::cursive::views;
|
||||
use db;
|
||||
use dir;
|
||||
use error::Error;
|
||||
use std::sync::Arc;
|
||||
use stream::{self, Opener, Stream};
|
||||
use super::{decode_size, encode_size};
|
||||
|
||||
/// Builds a `CameraChange` from an active `edit_camera_dialog`.
|
||||
fn get_change(siv: &mut Cursive) -> db::CameraChange {
|
||||
db::CameraChange{
|
||||
short_name: siv.find_id::<views::EditView>("short_name")
|
||||
.unwrap().get_content().as_str().into(),
|
||||
description: siv.find_id::<views::TextArea>("description").unwrap().get_content().into(),
|
||||
host: siv.find_id::<views::EditView>("host").unwrap().get_content().as_str().into(),
|
||||
username: siv.find_id::<views::EditView>("username").unwrap().get_content().as_str().into(),
|
||||
password: siv.find_id::<views::EditView>("password").unwrap().get_content().as_str().into(),
|
||||
main_rtsp_path: siv.find_id::<views::EditView>("main_rtsp_path")
|
||||
.unwrap().get_content().as_str().into(),
|
||||
sub_rtsp_path: siv.find_id::<views::EditView>("sub_rtsp_path")
|
||||
.unwrap().get_content().as_str().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
|
||||
id: Option<i32>) {
|
||||
let change = get_change(siv);
|
||||
siv.pop_layer(); // get rid of the add/edit camera dialog.
|
||||
|
||||
let result = {
|
||||
let mut l = db.lock();
|
||||
if let Some(id) = id {
|
||||
l.update_camera(id, change)
|
||||
} else {
|
||||
l.add_camera(change).map(|_| ())
|
||||
}
|
||||
};
|
||||
if let Err(e) = result {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to add camera: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
} else {
|
||||
// Recreate the "Edit cameras" dialog from scratch; it's easier than adding the new entry.
|
||||
siv.pop_layer();
|
||||
add_dialog(db, dir, siv);
|
||||
}
|
||||
}
|
||||
|
||||
fn press_test_inner(url: &str) -> Result<String, Error> {
|
||||
let stream = stream::FFMPEG.open(stream::Source::Rtsp(url))?;
|
||||
let extra_data = stream.get_extra_data()?;
|
||||
Ok(format!("{}x{} video stream", extra_data.width, extra_data.height))
|
||||
}
|
||||
|
||||
fn press_test(siv: &mut Cursive, c: &db::CameraChange, stream: &str, path: &str) {
|
||||
let url = format!("rtsp://{}:{}@{}{}", c.username, c.password, c.host, path);
|
||||
let description = match press_test_inner(&url) {
|
||||
Err(e) => {
|
||||
siv.add_layer(
|
||||
views::Dialog::text(format!("{} stream at {}:\n\n{}", stream, url, e))
|
||||
.title("Stream test failed")
|
||||
.dismiss_button("Back"));
|
||||
return;
|
||||
},
|
||||
Ok(d) => d,
|
||||
};
|
||||
siv.add_layer(views::Dialog::text(format!("{} stream at {}:\n\n{}", stream, url, description))
|
||||
.title("Stream test succeeded")
|
||||
.dismiss_button("Back"));
|
||||
}
|
||||
|
||||
fn press_delete(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, id: i32,
|
||||
name: String, to_delete: i64) {
|
||||
let dialog = if to_delete > 0 {
|
||||
let prompt = format!("Camera {} has recorded video. Please confirm the amount \
|
||||
of data to delete by typing it back:\n\n{}", name,
|
||||
encode_size(to_delete));
|
||||
views::Dialog::around(
|
||||
views::LinearLayout::vertical()
|
||||
.child(views::TextView::new(prompt))
|
||||
.child(views::DummyView)
|
||||
.child(views::EditView::new().on_submit({
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |siv, _| confirm_deletion(siv, &db, &dir, id, to_delete)
|
||||
}).with_id("confirm")))
|
||||
.button("Delete", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |siv| confirm_deletion(siv, &db, &dir, id, to_delete)
|
||||
})
|
||||
} else {
|
||||
views::Dialog::text(format!("Delete camera {}? This camera has no recorded video.", name))
|
||||
.button("Delete", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| actually_delete(s, &db, &dir, id)
|
||||
})
|
||||
}.title("Delete camera").dismiss_button("Cancel");
|
||||
siv.add_layer(dialog);
|
||||
}
|
||||
|
||||
fn confirm_deletion(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
|
||||
id: i32, to_delete: i64) {
|
||||
let typed = siv.find_id::<views::EditView>("confirm").unwrap().get_content();
|
||||
if decode_size(typed.as_str()).ok() == Some(to_delete) {
|
||||
siv.pop_layer(); // deletion confirmation dialog
|
||||
if let Err(e) = dir::lower_retention(dir.clone(),
|
||||
&[dir::NewLimit{camera_id: id, limit: 0}]) {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to delete recordings: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
return;
|
||||
}
|
||||
actually_delete(siv, db, dir, id);
|
||||
} else {
|
||||
siv.add_layer(views::Dialog::text("Please confirm amount.")
|
||||
.title("Try again")
|
||||
.dismiss_button("Back"));
|
||||
}
|
||||
}
|
||||
|
||||
fn actually_delete(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
|
||||
id: i32) {
|
||||
info!("actually_delete call");
|
||||
siv.pop_layer(); // get rid of the add/edit camera dialog.
|
||||
let result = {
|
||||
let mut l = db.lock();
|
||||
l.delete_camera(id)
|
||||
};
|
||||
if let Err(e) = result {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to delete camera: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
} else {
|
||||
// Recreate the "Edit cameras" dialog from scratch; it's easier than adding the new entry.
|
||||
siv.pop_layer();
|
||||
add_dialog(db, dir, siv);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds or updates a camera.
|
||||
/// (The former if `item` is None; the latter otherwise.)
|
||||
fn edit_camera_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive,
|
||||
item: &Option<i32>) {
|
||||
let list = views::ListView::new()
|
||||
.child("id", views::TextView::new(match *item {
|
||||
None => "<new>".to_string(),
|
||||
Some(id) => id.to_string(),
|
||||
}))
|
||||
.child("uuid", views::TextView::new("<new>").with_id("uuid"))
|
||||
.child("short name", views::EditView::new().with_id("short_name"))
|
||||
.child("host", views::EditView::new().with_id("host"))
|
||||
.child("username", views::EditView::new().with_id("username"))
|
||||
.child("password", views::EditView::new().with_id("password"))
|
||||
.child("main_rtsp_path", views::LinearLayout::horizontal()
|
||||
.child(views::EditView::new().with_id("main_rtsp_path").full_width())
|
||||
.child(views::DummyView)
|
||||
.child(views::Button::new("Test", |siv| {
|
||||
let c = get_change(siv);
|
||||
press_test(siv, &c, "main", &c.main_rtsp_path)
|
||||
})))
|
||||
.child("sub_rtsp_path", views::LinearLayout::horizontal()
|
||||
.child(views::EditView::new().with_id("sub_rtsp_path").full_width())
|
||||
.child(views::DummyView)
|
||||
.child(views::Button::new("Test", |siv| {
|
||||
let c = get_change(siv);
|
||||
press_test(siv, &c, "sub", &c.sub_rtsp_path)
|
||||
})))
|
||||
.min_height(8);
|
||||
let layout = views::LinearLayout::vertical()
|
||||
.child(list)
|
||||
.child(views::TextView::new("description"))
|
||||
.child(views::TextArea::new().with_id("description").min_height(3))
|
||||
.full_width();
|
||||
let mut dialog = views::Dialog::around(layout);
|
||||
let dialog = if let Some(id) = *item {
|
||||
let l = db.lock();
|
||||
let camera = l.cameras_by_id().get(&id).expect("missing camera");
|
||||
dialog.find_id::<views::TextView>("uuid")
|
||||
.expect("missing TextView")
|
||||
.set_content(camera.uuid.to_string());
|
||||
let bytes = camera.sample_file_bytes;
|
||||
let name = camera.short_name.clone();
|
||||
for &(view_id, content) in &[("short_name", &camera.short_name),
|
||||
("host", &camera.host),
|
||||
("username", &camera.username),
|
||||
("password", &camera.password),
|
||||
("main_rtsp_path", &camera.main_rtsp_path),
|
||||
("sub_rtsp_path", &camera.sub_rtsp_path)] {
|
||||
dialog.find_id::<views::EditView>(view_id)
|
||||
.expect("missing EditView")
|
||||
.set_content(content.to_string());
|
||||
}
|
||||
dialog.find_id::<views::TextArea>("description")
|
||||
.expect("missing TextArea")
|
||||
.set_content(camera.description.to_string());
|
||||
dialog.title("Edit camera")
|
||||
.button("Edit", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| press_edit(s, &db, &dir, Some(id))
|
||||
})
|
||||
.button("Delete", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| press_delete(s, &db, &dir, id, name.clone(), bytes)
|
||||
})
|
||||
} else {
|
||||
dialog.title("Add camera")
|
||||
.button("Add", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| press_edit(s, &db, &dir, None)
|
||||
})
|
||||
};
|
||||
siv.add_layer(dialog.dismiss_button("Cancel"));
|
||||
}
|
||||
|
||||
pub fn add_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive) {
|
||||
siv.add_layer(views::Dialog::around(
|
||||
views::SelectView::new()
|
||||
.on_submit({
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |siv, item| edit_camera_dialog(&db, &dir, siv, item)
|
||||
})
|
||||
.item("<new camera>".to_string(), None)
|
||||
.with_all(db.lock()
|
||||
.cameras_by_id()
|
||||
.iter()
|
||||
.map(|(&id, camera)| (format!("{}: {}", id, camera.short_name), Some(id))))
|
||||
.full_width())
|
||||
.dismiss_button("Done")
|
||||
.title("Edit cameras"));
|
||||
}
|
||||
163
src/cmds/config/mod.rs
Normal file
163
src/cmds/config/mod.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2017 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/>.
|
||||
|
||||
//! Text-based configuration interface.
|
||||
//!
|
||||
//! This code is a bit messy, but it's essentially a prototype. Eventually Moonfire NVR's
|
||||
//! configuration will likely be almost entirely done through a web-based UI.
|
||||
|
||||
extern crate cursive;
|
||||
|
||||
use self::cursive::Cursive;
|
||||
use self::cursive::views;
|
||||
use db;
|
||||
use dir;
|
||||
use error::Error;
|
||||
use regex::Regex;
|
||||
use std::sync::Arc;
|
||||
use std::fmt::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod cameras;
|
||||
mod retention;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Interactive configuration editor.
|
||||
|
||||
Usage:
|
||||
|
||||
moonfire-nvr config [options]
|
||||
moonfire-nvr config --help
|
||||
|
||||
Options:
|
||||
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--sample-file-dir=DIR Set the directory holding video data.
|
||||
This is typically on a hard drive.
|
||||
[default: /var/lib/moonfire-nvr/sample]
|
||||
"#;
|
||||
|
||||
static MULTIPLIERS: [(char, u64); 4] = [
|
||||
// (suffix character, power of 2)
|
||||
('T', 40),
|
||||
('G', 30),
|
||||
('M', 20),
|
||||
('K', 10),
|
||||
];
|
||||
|
||||
fn encode_size(mut raw: i64) -> String {
|
||||
let mut encoded = String::new();
|
||||
for &(c, n) in &MULTIPLIERS {
|
||||
if raw >= 1i64<<n {
|
||||
write!(&mut encoded, "{}{} ", raw >> n, c).unwrap();
|
||||
raw &= (1i64 << n) - 1;
|
||||
}
|
||||
}
|
||||
if raw > 0 || encoded.len() == 0 {
|
||||
write!(&mut encoded, "{}", raw).unwrap();
|
||||
} else {
|
||||
encoded.pop(); // remove trailing space.
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
fn decode_size(encoded: &str) -> Result<i64, ()> {
|
||||
let mut decoded = 0i64;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"\s*([0-9]+)([TGMK])?,?\s*").unwrap();
|
||||
}
|
||||
let mut last_pos = 0;
|
||||
for cap in RE.captures_iter(encoded) {
|
||||
let whole_cap = cap.get(0).unwrap();
|
||||
if whole_cap.start() > last_pos {
|
||||
return Err(());
|
||||
}
|
||||
last_pos = whole_cap.end();
|
||||
let mut piece = i64::from_str(&cap[1]).map_err(|_| ())?;
|
||||
if let Some(m) = cap.get(2) {
|
||||
let m = m.as_str().as_bytes()[0] as char;
|
||||
for &(some_m, n) in &MULTIPLIERS {
|
||||
if some_m == m {
|
||||
piece *= 1i64<<n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
decoded += piece;
|
||||
}
|
||||
if last_pos < encoded.len() {
|
||||
return Err(());
|
||||
}
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_sample_file_dir: String,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
super::install_logger(false);
|
||||
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
let db = Arc::new(db::Database::new(conn)?);
|
||||
//let dir = Arc::new(dir::Fd::open(&args.flag_sample_file_dir)?);
|
||||
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone())?;
|
||||
|
||||
let mut siv = Cursive::new();
|
||||
//siv.add_global_callback('q', |s| s.quit());
|
||||
|
||||
siv.add_layer(views::Dialog::around(
|
||||
views::SelectView::<fn(&Arc<db::Database>, &Arc<dir::SampleFileDir>, &mut Cursive)>::new()
|
||||
.on_submit({
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |siv, item| item(&db, &dir, siv)
|
||||
})
|
||||
.item("Edit cameras".to_string(), cameras::add_dialog)
|
||||
.item("Edit retention".to_string(), retention::add_dialog))
|
||||
.button("Quit", |siv| siv.quit())
|
||||
.title("Main menu"));
|
||||
|
||||
siv.run();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20);
|
||||
}
|
||||
}
|
||||
265
src/cmds/config/retention.rs
Normal file
265
src/cmds/config/retention.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2017 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 cursive;
|
||||
|
||||
use self::cursive::Cursive;
|
||||
use self::cursive::traits::{Boxable, Identifiable};
|
||||
use self::cursive::views;
|
||||
use db;
|
||||
use dir;
|
||||
use error::Error;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use super::{decode_size, encode_size};
|
||||
|
||||
struct Camera {
|
||||
label: String,
|
||||
used: i64,
|
||||
retain: Option<i64>, // None if unparseable
|
||||
}
|
||||
|
||||
struct Model {
|
||||
db: Arc<db::Database>,
|
||||
dir: Arc<dir::SampleFileDir>,
|
||||
fs_capacity: i64,
|
||||
total_used: i64,
|
||||
total_retain: i64,
|
||||
errors: isize,
|
||||
cameras: BTreeMap<i32, Camera>,
|
||||
}
|
||||
|
||||
/// Updates the limits in the database. Doesn't delete excess data (if any).
|
||||
fn update_limits_inner(model: &Model) -> Result<(), Error> {
|
||||
let mut db = model.db.lock();
|
||||
let mut tx = db.tx()?;
|
||||
for (&id, camera) in &model.cameras {
|
||||
tx.update_retention(id, camera.retain.unwrap())?;
|
||||
}
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
fn update_limits(model: &Model, siv: &mut Cursive) {
|
||||
if let Err(e) = update_limits_inner(model) {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to update limits: {}", e))
|
||||
.dismiss_button("Back")
|
||||
.title("Error"));
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_limit(model: &RefCell<Model>, siv: &mut Cursive, id: i32, content: &str) {
|
||||
info!("on_edit called for id {}", id);
|
||||
let mut model = model.borrow_mut();
|
||||
let model: &mut Model = &mut *model;
|
||||
let camera = model.cameras.get_mut(&id).unwrap();
|
||||
let new_value = decode_size(content).ok();
|
||||
let delta = new_value.unwrap_or(0) - camera.retain.unwrap_or(0);
|
||||
let old_errors = model.errors;
|
||||
if delta != 0 {
|
||||
let prev_over = model.total_retain > model.fs_capacity;
|
||||
model.total_retain += delta;
|
||||
siv.find_id::<views::TextView>("total_retain")
|
||||
.unwrap()
|
||||
.set_content(encode_size(model.total_retain));
|
||||
let now_over = model.total_retain > model.fs_capacity;
|
||||
info!("now_over: {}", now_over);
|
||||
if now_over != prev_over {
|
||||
model.errors += if now_over { 1 } else { -1 };
|
||||
siv.find_id::<views::TextView>("total_ok")
|
||||
.unwrap()
|
||||
.set_content(if now_over { "*" } else { " " });
|
||||
}
|
||||
}
|
||||
if new_value.is_none() != camera.retain.is_none() {
|
||||
model.errors += if new_value.is_none() { 1 } else { -1 };
|
||||
siv.find_id::<views::TextView>(&format!("{}_ok", id))
|
||||
.unwrap()
|
||||
.set_content(if new_value.is_none() { "*" } else { " " });
|
||||
}
|
||||
camera.retain = new_value;
|
||||
info!("model.errors = {}", model.errors);
|
||||
if (model.errors == 0) != (old_errors == 0) {
|
||||
info!("toggling change state: errors={}", model.errors);
|
||||
siv.find_id::<views::Button>("change")
|
||||
.unwrap()
|
||||
.set_enabled(model.errors == 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_deletion(model: &RefCell<Model>, siv: &mut Cursive, to_delete: i64) {
|
||||
let typed = siv.find_id::<views::EditView>("confirm")
|
||||
.unwrap()
|
||||
.get_content();
|
||||
info!("confirm, typed: {} vs expected: {}", typed.as_str(), to_delete);
|
||||
if decode_size(typed.as_str()).ok() == Some(to_delete) {
|
||||
actually_delete(model, siv);
|
||||
} else {
|
||||
siv.add_layer(views::Dialog::text("Please confirm amount.")
|
||||
.title("Try again")
|
||||
.dismiss_button("Back"));
|
||||
}
|
||||
}
|
||||
|
||||
fn actually_delete(model: &RefCell<Model>, siv: &mut Cursive) {
|
||||
let model = &*model.borrow();
|
||||
let new_limits: Vec<_> =
|
||||
model.cameras.iter()
|
||||
.map(|(&id, c)| dir::NewLimit{camera_id: id, limit: c.retain.unwrap()})
|
||||
.collect();
|
||||
siv.pop_layer(); // deletion confirmation
|
||||
siv.pop_layer(); // retention dialog
|
||||
if let Err(e) = dir::lower_retention(model.dir.clone(), &new_limits[..]) {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to delete excess video: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
} else {
|
||||
update_limits(model, siv);
|
||||
}
|
||||
}
|
||||
|
||||
fn press_change(model: &Rc<RefCell<Model>>, siv: &mut Cursive) {
|
||||
if model.borrow().errors > 0 {
|
||||
return;
|
||||
}
|
||||
let to_delete = model.borrow().cameras.values().map(
|
||||
|c| ::std::cmp::max(c.used - c.retain.unwrap(), 0)).sum();
|
||||
info!("change press, to_delete={}", to_delete);
|
||||
if to_delete > 0 {
|
||||
let prompt = format!("Some cameras' usage exceeds new limit. Please confirm the amount \
|
||||
of data to delete by typing it back:\n\n{}", encode_size(to_delete));
|
||||
let dialog = views::Dialog::around(
|
||||
views::LinearLayout::vertical()
|
||||
.child(views::TextView::new(prompt))
|
||||
.child(views::DummyView)
|
||||
.child(views::EditView::new().on_submit({
|
||||
let model = model.clone();
|
||||
move |siv, _| confirm_deletion(&model, siv, to_delete)
|
||||
}).with_id("confirm")))
|
||||
.button("Confirm", {
|
||||
let model = model.clone();
|
||||
move |siv| confirm_deletion(&model, siv, to_delete)
|
||||
})
|
||||
.dismiss_button("Cancel")
|
||||
.title("Confirm deletion");
|
||||
siv.add_layer(dialog);
|
||||
} else {
|
||||
siv.screen_mut().pop_layer();
|
||||
update_limits(&model.borrow(), siv);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive) {
|
||||
let model = {
|
||||
let mut cameras = BTreeMap::new();
|
||||
let mut total_used = 0;
|
||||
let mut total_retain = 0;
|
||||
{
|
||||
let db = db.lock();
|
||||
for (&id, camera) in db.cameras_by_id() {
|
||||
cameras.insert(id, Camera{
|
||||
label: format!("{}: {}", id, camera.short_name),
|
||||
used: camera.sample_file_bytes,
|
||||
retain: Some(camera.retain_bytes),
|
||||
});
|
||||
total_used += camera.sample_file_bytes;
|
||||
total_retain += camera.retain_bytes;
|
||||
}
|
||||
}
|
||||
let stat = dir.statfs().unwrap();
|
||||
let fs_capacity = (stat.f_bsize * (stat.f_blocks - stat.f_bfree + stat.f_bavail)) as i64 -
|
||||
total_used;
|
||||
Rc::new(RefCell::new(Model{
|
||||
dir: dir.clone(),
|
||||
db: db.clone(),
|
||||
fs_capacity: fs_capacity,
|
||||
total_used: total_used,
|
||||
total_retain: total_retain,
|
||||
errors: (total_retain > fs_capacity) as isize,
|
||||
cameras: cameras,
|
||||
}))
|
||||
};
|
||||
|
||||
let mut list = views::ListView::new();
|
||||
list.add_child(
|
||||
"camera",
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new("usage").fixed_width(25))
|
||||
.child(views::TextView::new("limit").fixed_width(25)));
|
||||
for (&id, camera) in &model.borrow().cameras {
|
||||
list.add_child(
|
||||
&camera.label,
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new(encode_size(camera.used)).fixed_width(25))
|
||||
.child(views::EditView::new()
|
||||
.content(encode_size(camera.retain.unwrap()))
|
||||
.on_edit({
|
||||
let model = model.clone();
|
||||
move |siv, content, _pos| edit_limit(&model, siv, id, content)
|
||||
})
|
||||
.on_submit({
|
||||
let model = model.clone();
|
||||
move |siv, _| press_change(&model, siv)
|
||||
})
|
||||
.fixed_width(25))
|
||||
.child(views::TextView::new("").with_id(&format!("{}_ok", id)).fixed_width(1)));
|
||||
}
|
||||
let over = model.borrow().total_retain > model.borrow().fs_capacity;
|
||||
list.add_child(
|
||||
"total",
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new(encode_size(model.borrow().total_used)).fixed_width(25))
|
||||
.child(views::TextView::new(encode_size(model.borrow().total_retain))
|
||||
.with_id("total_retain").fixed_width(25))
|
||||
.child(views::TextView::new(if over { "*" } else { " " }).with_id("total_ok")));
|
||||
list.add_child(
|
||||
"filesystem",
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new("").fixed_width(25))
|
||||
.child(views::TextView::new(encode_size(model.borrow().fs_capacity)).fixed_width(25)));
|
||||
let mut change_button = views::Button::new("Change", {
|
||||
let model = model.clone();
|
||||
move |siv| press_change(&model, siv)
|
||||
});
|
||||
change_button.set_enabled(!over);
|
||||
let mut buttons = views::LinearLayout::horizontal()
|
||||
.child(views::DummyView.full_width());
|
||||
buttons.add_child(change_button.with_id("change"));
|
||||
buttons.add_child(views::DummyView);
|
||||
buttons.add_child(views::Button::new("Cancel", |siv| siv.screen_mut().pop_layer()));
|
||||
siv.add_layer(
|
||||
views::Dialog::around(
|
||||
views::LinearLayout::vertical()
|
||||
.child(list)
|
||||
.child(views::DummyView)
|
||||
.child(buttons))
|
||||
.title("Edit retention"));
|
||||
}
|
||||
@@ -40,6 +40,7 @@ use slog_term;
|
||||
use std::path::Path;
|
||||
|
||||
mod check;
|
||||
mod config;
|
||||
mod init;
|
||||
mod run;
|
||||
mod ts;
|
||||
@@ -48,6 +49,7 @@ mod upgrade;
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
pub enum Command {
|
||||
Check,
|
||||
Config,
|
||||
Init,
|
||||
Run,
|
||||
Ts,
|
||||
@@ -58,6 +60,7 @@ impl Command {
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
match *self {
|
||||
Command::Check => check::run(),
|
||||
Command::Config => config::run(),
|
||||
Command::Init => init::run(),
|
||||
Command::Run => run::run(),
|
||||
Command::Ts => ts::run(),
|
||||
@@ -71,7 +74,7 @@ impl Command {
|
||||
/// Sync logging should be preferred for other modes because async apparently is never flushed
|
||||
/// before the program exits, and partial output from these tools is very confusing.
|
||||
fn install_logger(async: bool) {
|
||||
let drain = slog_term::StreamerBuilder::new();
|
||||
let drain = slog_term::StreamerBuilder::new().stderr();
|
||||
let drain = slog_envlogger::new(if async { drain.async() } else { drain }.full().build());
|
||||
slog_stdlog::set_logger(slog::Logger::root(drain.ignore_err(), None)).unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user