switch from docopt to structopt
A couple reasons for this: * the docopt crate is "unlikely to see significant future evolution", and the wider docopt project is "mostly unmaintained at this point". clap/structopt is more full-featured, has more natural subcommand support, etc. * it may allow me to shrink the binary (#70). This change alone seems to be a slight regression, but it's a step toward getting rid of regex, which is pretty large. And I feel less ridiculous now that I don't have two parsing crates anyway; prettydiff was pulling in structopt. There are some behavior changes here: * misc --help output changes and such as you'd expect from switching argument-parsing libraries * I properly used PathBuf and OsString for stuff that theoretically could be non-UTF-8. I haven't tested that it actually made any difference. I'm also still storing the sample file dirname as "text" in the database to avoid causing a diff when not doing a schema change.
This commit is contained in:
parent
066c086050
commit
e8eb764b90
|
@ -275,6 +275,7 @@ dependencies = [
|
|||
"atty",
|
||||
"bitflags",
|
||||
"strsim 0.8.0",
|
||||
"term_size",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
|
@ -509,18 +510,6 @@ dependencies = [
|
|||
"winapi 0.3.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docopt"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
"strsim 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.4"
|
||||
|
@ -1282,7 +1271,6 @@ dependencies = [
|
|||
"bytes",
|
||||
"cstr",
|
||||
"cursive",
|
||||
"docopt",
|
||||
"failure",
|
||||
"fnv",
|
||||
"futures",
|
||||
|
@ -1310,6 +1298,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"smallvec 1.1.0",
|
||||
"structopt 0.3.13",
|
||||
"tempdir",
|
||||
"time 0.1.42",
|
||||
"tokio",
|
||||
|
@ -1624,7 +1613,7 @@ checksum = "5240be0c9ea1bc7887819a36264cb9475eb71c58749808e5b989c8c1fdc67acf"
|
|||
dependencies = [
|
||||
"ansi_term 0.9.0",
|
||||
"prettytable-rs",
|
||||
"structopt",
|
||||
"structopt 0.2.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1641,6 +1630,32 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2 1.0.8",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.14",
|
||||
"version_check 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.8",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.14",
|
||||
"syn-mid",
|
||||
"version_check 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.11"
|
||||
|
@ -2302,7 +2317,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"structopt-derive",
|
||||
"structopt-derive 0.2.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6da2e8d107dfd7b74df5ef4d205c6aebee0706c647f6bc6a2d5789905c00fb"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"lazy_static",
|
||||
"structopt-derive 0.4.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2317,6 +2343,19 @@ dependencies = [
|
|||
"syn 0.15.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt-derive"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a489c87c08fbaf12e386665109dd13470dcc9c4583ea3e10dd2b4523e5ebd9ac"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2 1.0.8",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "1.0.0"
|
||||
|
@ -2345,6 +2384,17 @@ dependencies = [
|
|||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn-mid"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.8",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.12.3"
|
||||
|
@ -2409,6 +2459,7 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"term_size",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
|
|
|
@ -25,7 +25,10 @@ byteorder = "1.0"
|
|||
cstr = "0.1.7"
|
||||
cursive = "0.14.0"
|
||||
db = { package = "moonfire-db", path = "db" }
|
||||
docopt = "1.0"
|
||||
#structopt = "0.3.13"
|
||||
structopt = { version = "0.3.13", features = ["default", "wrap_help"] }
|
||||
# default = ["suggestions", "color", "vec_map", "derive", "std", "cargo"]
|
||||
|
||||
failure = "0.1.1"
|
||||
ffmpeg = { package = "moonfire-ffmpeg", path = "ffmpeg" }
|
||||
futures = "0.3"
|
||||
|
|
27
db/auth.rs
27
db/auth.rs
|
@ -42,6 +42,7 @@ use rusqlite::{Connection, Transaction, params};
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -57,7 +58,7 @@ pub(crate) fn set_test_config() {
|
|||
Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2)));
|
||||
}
|
||||
|
||||
enum UserFlags {
|
||||
enum UserFlag {
|
||||
Disabled = 1,
|
||||
}
|
||||
|
||||
|
@ -91,7 +92,7 @@ impl User {
|
|||
}
|
||||
|
||||
pub fn has_password(&self) -> bool { self.password_hash.is_some() }
|
||||
fn disabled(&self) -> bool { (self.flags & UserFlags::Disabled as i32) != 0 }
|
||||
fn disabled(&self) -> bool { (self.flags & UserFlag::Disabled as i32) != 0 }
|
||||
}
|
||||
|
||||
/// A change to a user.
|
||||
|
@ -132,7 +133,7 @@ impl UserChange {
|
|||
}
|
||||
|
||||
pub fn disable(&mut self) {
|
||||
self.flags |= UserFlags::Disabled as i32;
|
||||
self.flags |= UserFlag::Disabled as i32;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,13 +195,29 @@ impl rusqlite::types::FromSql for FromSqlIpAddr {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum SessionFlags {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum SessionFlag {
|
||||
HttpOnly = 1,
|
||||
Secure = 2,
|
||||
SameSite = 4,
|
||||
SameSiteStrict = 8,
|
||||
}
|
||||
|
||||
impl FromStr for SessionFlag {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"http-only" => Ok(Self::HttpOnly),
|
||||
"secure" => Ok(Self::Secure),
|
||||
"same-site" => Ok(Self::SameSite),
|
||||
"same-site-strict" => Ok(Self::SameSiteStrict),
|
||||
_ => bail!("No such session flag {:?}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum RevocationReason {
|
||||
LoggedOut = 1,
|
||||
|
@ -209,7 +226,7 @@ pub enum RevocationReason {
|
|||
#[derive(Debug, Default)]
|
||||
pub struct Session {
|
||||
user_id: i32,
|
||||
flags: i32, // bitmask of SessionFlags enum values
|
||||
flags: i32, // bitmask of SessionFlag enum values
|
||||
domain: Option<Vec<u8>>,
|
||||
description: Option<String>,
|
||||
seed: Seed,
|
||||
|
|
10
db/dir.rs
10
db/dir.rs
|
@ -41,7 +41,7 @@ use log::warn;
|
|||
use protobuf::Message;
|
||||
use nix::{NixPath, fcntl::{FlockArg, OFlag}, sys::stat::Mode};
|
||||
use nix::sys::statvfs::Statvfs;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ffi::CStr;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
@ -104,16 +104,14 @@ impl Drop for Fd {
|
|||
|
||||
impl Fd {
|
||||
/// Opens the given path as a directory.
|
||||
pub fn open(path: &str, mkdir: bool) -> Result<Fd, nix::Error> {
|
||||
let cstring = CString::new(path).map_err(|_| nix::Error::InvalidPath)?;
|
||||
pub fn open<P: ?Sized + NixPath>(path: &P, mkdir: bool) -> Result<Fd, nix::Error> {
|
||||
if mkdir {
|
||||
match nix::unistd::mkdir(cstring.as_c_str(), nix::sys::stat::Mode::S_IRWXU) {
|
||||
match nix::unistd::mkdir(path, nix::sys::stat::Mode::S_IRWXU) {
|
||||
Ok(()) | Err(nix::Error::Sys(nix::errno::Errno::EEXIST)) => {},
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
let fd = nix::fcntl::open(cstring.as_c_str(), OFlag::O_DIRECTORY | OFlag::O_RDONLY,
|
||||
Mode::empty())?;
|
||||
let fd = nix::fcntl::open(path, OFlag::O_DIRECTORY | OFlag::O_RDONLY, Mode::empty())?;
|
||||
Ok(Fd(fd))
|
||||
}
|
||||
|
||||
|
|
|
@ -52,9 +52,9 @@ const UPGRADE_NOTES: &'static str =
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Args<'a> {
|
||||
pub flag_sample_file_dir: Option<&'a str>,
|
||||
pub flag_preset_journal: &'a str,
|
||||
pub flag_no_vacuum: bool,
|
||||
pub sample_file_dir: Option<&'a std::path::Path>,
|
||||
pub preset_journal: &'a str,
|
||||
pub no_vacuum: bool,
|
||||
}
|
||||
|
||||
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
|
||||
|
@ -86,7 +86,7 @@ fn upgrade(args: &Args, target_ver: i32, conn: &mut rusqlite::Connection) -> Res
|
|||
bail!("Database is at negative version {}!", old_ver);
|
||||
}
|
||||
info!("Upgrading database from version {} to version {}...", old_ver, target_ver);
|
||||
set_journal_mode(&conn, args.flag_preset_journal)?;
|
||||
set_journal_mode(&conn, args.preset_journal)?;
|
||||
for ver in old_ver .. target_ver {
|
||||
info!("...from version {} to version {}", ver, ver + 1);
|
||||
let tx = conn.transaction()?;
|
||||
|
@ -118,7 +118,7 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> {
|
|||
// WAL is the preferred journal mode for normal operation; it reduces the number of syncs
|
||||
// without compromising safety.
|
||||
set_journal_mode(&conn, "wal")?;
|
||||
if !args.flag_no_vacuum {
|
||||
if !args.no_vacuum {
|
||||
info!("...vacuuming database after upgrade.");
|
||||
conn.execute_batch(r#"
|
||||
pragma page_size = 16384;
|
||||
|
@ -157,7 +157,7 @@ impl NixPath for UuidPath {
|
|||
mod tests {
|
||||
use crate::compare;
|
||||
use crate::testutil;
|
||||
use failure::{ResultExt, format_err};
|
||||
use failure::ResultExt;
|
||||
use super::*;
|
||||
|
||||
fn new_conn() -> Result<rusqlite::Connection, Error> {
|
||||
|
@ -183,7 +183,7 @@ mod tests {
|
|||
fn upgrade_and_compare() -> Result<(), Error> {
|
||||
testutil::init();
|
||||
let tmpdir = tempdir::TempDir::new("moonfire-nvr-test")?;
|
||||
let path = tmpdir.path().to_str().ok_or_else(|| format_err!("invalid UTF-8"))?.to_owned();
|
||||
//let path = tmpdir.path().to_str().ok_or_else(|| format_err!("invalid UTF-8"))?.to_owned();
|
||||
let mut upgraded = new_conn()?;
|
||||
upgraded.execute_batch(include_str!("v0.sql"))?;
|
||||
upgraded.execute_batch(r#"
|
||||
|
@ -218,9 +218,9 @@ mod tests {
|
|||
(4, None), // transitional; don't compare schemas.
|
||||
(5, Some(include_str!("../schema.sql")))] {
|
||||
upgrade(&Args {
|
||||
flag_sample_file_dir: Some(&path),
|
||||
flag_preset_journal: "delete",
|
||||
flag_no_vacuum: false,
|
||||
sample_file_dir: Some(&tmpdir.path()),
|
||||
preset_journal: "delete",
|
||||
no_vacuum: false,
|
||||
}, *ver, &mut upgraded).context(format!("upgrading to version {}", ver))?;
|
||||
if let Some(f) = fresh_sql {
|
||||
compare(&upgraded, *ver, f)?;
|
||||
|
|
|
@ -42,7 +42,7 @@ use uuid::Uuid;
|
|||
|
||||
pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||
let sample_file_path =
|
||||
args.flag_sample_file_dir
|
||||
args.sample_file_dir
|
||||
.ok_or_else(|| format_err!("--sample-file-dir required when upgrading from \
|
||||
schema version 1 to 2."))?;
|
||||
|
||||
|
@ -122,6 +122,9 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
|||
}
|
||||
dir::write_meta(d.as_raw_fd(), &meta)?;
|
||||
|
||||
let sample_file_path = sample_file_path.to_str()
|
||||
.ok_or_else(|| format_err!("sample file dir {} is not a valid string",
|
||||
sample_file_path.display()))?;
|
||||
tx.execute(r#"
|
||||
insert into sample_file_dir (path, uuid, last_complete_open_id)
|
||||
values (?, ?, ?)
|
||||
|
@ -293,7 +296,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
|||
/// * optional: reserved sample file uuids.
|
||||
/// * optional: meta and meta-tmp from half-completed update attempts.
|
||||
/// * forbidden: anything else.
|
||||
fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir,
|
||||
fn verify_dir_contents(sample_file_path: &std::path::Path, dir: &mut nix::dir::Dir,
|
||||
tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||
// Build a hash of the uuids found in the directory.
|
||||
let n: i64 = tx.query_row(r#"
|
||||
|
@ -337,7 +340,7 @@ fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir,
|
|||
while let Some(row) = rows.next()? {
|
||||
let uuid: crate::db::FromSqlUuid = row.get(0)?;
|
||||
if !files.remove(&uuid.0) {
|
||||
bail!("{} is missing from dir {}!", uuid.0, sample_file_path);
|
||||
bail!("{} is missing from dir {}!", uuid.0, sample_file_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -360,7 +363,7 @@ fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir,
|
|||
|
||||
if !files.is_empty() {
|
||||
bail!("{} unexpected sample file uuids in dir {}: {:?}!",
|
||||
files.len(), sample_file_path, files);
|
||||
files.len(), sample_file_path.display(), files);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2018 The Moonfire NVR Authors
|
||||
// Copyright (C) 2018-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// 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
|
||||
|
@ -32,36 +32,25 @@
|
|||
|
||||
use db::check;
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Checks database integrity.
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
|
||||
Usage:
|
||||
|
||||
moonfire-nvr check [options]
|
||||
moonfire-nvr check --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]
|
||||
--compare-lens Compare sample file lengths on disk to the database.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_compare_lens: bool,
|
||||
/// Compare sample file lengths on disk to the database.
|
||||
#[structopt(long)]
|
||||
compare_lens: bool,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
// TODO: ReadOnly should be sufficient but seems to fail.
|
||||
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
check::run(&conn, &check::Options {
|
||||
compare_lens: args.flag_compare_lens,
|
||||
compare_lens: args.compare_lens,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -38,36 +38,24 @@ use cursive::Cursive;
|
|||
use cursive::views;
|
||||
use db;
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use structopt::StructOpt;
|
||||
|
||||
mod cameras;
|
||||
mod dirs;
|
||||
mod users;
|
||||
|
||||
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]
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
let clocks = clock::RealClocks {};
|
||||
let db = Arc::new(db::Database::new(clocks, conn, true)?);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// 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
|
||||
|
@ -30,31 +30,19 @@
|
|||
|
||||
use failure::Error;
|
||||
use log::info;
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Initializes a database.
|
||||
|
||||
Usage:
|
||||
|
||||
moonfire-nvr init [options]
|
||||
moonfire-nvr init --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]
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::Create)?;
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::Create)?;
|
||||
|
||||
// Check if the database has already been initialized.
|
||||
let cur_ver = db::get_schema_version(&conn)?;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2019 The Moonfire NVR Authors
|
||||
// Copyright (C) 2019-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// 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
|
||||
|
@ -31,92 +31,74 @@
|
|||
//! Subcommand to login a user (without requiring a password).
|
||||
|
||||
use base::clock::{self, Clocks};
|
||||
use db::auth::SessionFlags;
|
||||
use failure::{Error, ResultExt, bail, format_err};
|
||||
use serde::Deserialize;
|
||||
use db::auth::SessionFlag;
|
||||
use failure::{Error, format_err};
|
||||
use std::os::unix::fs::OpenOptionsExt as _;
|
||||
use std::io::Write as _;
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Logs in a user, returning the session cookie.
|
||||
#[derive(Debug, Default, StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
|
||||
This is a privileged command that directly accesses the database. It doesn't
|
||||
check the user's password and even can be used to create sessions with
|
||||
permissions the user doesn't have.
|
||||
/// Create a session with the given permissions.
|
||||
///
|
||||
/// If unspecified, uses user's default permissions.
|
||||
#[structopt(long, value_name="perms",
|
||||
parse(try_from_str = protobuf::text_format::parse_from_str))]
|
||||
permissions: Option<db::Permissions>,
|
||||
|
||||
Usage:
|
||||
/// Restrict this cookie to the given domain.
|
||||
#[structopt(long)]
|
||||
domain: Option<String>,
|
||||
|
||||
moonfire-nvr login [options] <username>
|
||||
moonfire-nvr login --help
|
||||
/// Write the cookie to a new curl-compatible cookie-jar file.
|
||||
///
|
||||
/// ---domain must be specified. This file can be used later with curl's --cookie flag.
|
||||
#[structopt(long, requires("domain"), value_name="path")]
|
||||
curl_cookie_jar: Option<PathBuf>,
|
||||
|
||||
Options:
|
||||
/// Set the given db::auth::SessionFlags.
|
||||
#[structopt(long, default_value="http-only,secure,same-site,same-site-strict",
|
||||
value_name="flags", use_delimiter=true)]
|
||||
session_flags: Vec<SessionFlag>,
|
||||
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database. This
|
||||
is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--permissions=PERMISSIONS
|
||||
Create a session with the given permissions. If
|
||||
unspecified, uses user's default permissions.
|
||||
--domain=DOMAIN The domain this cookie lives on. Optional.
|
||||
--curl-cookie-jar=FILE
|
||||
Writes the cookie to a new curl-compatible cookie-jar
|
||||
file. --domain must be specified. This can be used later
|
||||
with curl's --cookie flag.
|
||||
--session-flags=FLAGS
|
||||
Set the given db::auth::SessionFlags.
|
||||
[default: http-only,secure,same-site,same-site-strict]
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_permissions: Option<String>,
|
||||
flag_domain: Option<String>,
|
||||
flag_curl_cookie_jar: Option<PathBuf>,
|
||||
flag_session_flags: String,
|
||||
arg_username: String,
|
||||
/// Create the session for this username.
|
||||
username: String,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let clocks = clock::RealClocks {};
|
||||
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
let db = std::sync::Arc::new(db::Database::new(clocks.clone(), conn, true).unwrap());
|
||||
let mut l = db.lock();
|
||||
let u = l.get_user(&args.arg_username)
|
||||
.ok_or_else(|| format_err!("no such user {:?}", &args.arg_username))?;
|
||||
let permissions = match args.flag_permissions {
|
||||
None => u.permissions.clone(),
|
||||
Some(s) => protobuf::text_format::parse_from_str(&s)
|
||||
.context("unable to parse --permissions")?
|
||||
};
|
||||
let u = l.get_user(&args.username)
|
||||
.ok_or_else(|| format_err!("no such user {:?}", &args.username))?;
|
||||
let permissions = args.permissions.as_ref().unwrap_or(&u.permissions).clone();
|
||||
let creation = db::auth::Request {
|
||||
when_sec: Some(db.clocks().realtime().sec),
|
||||
user_agent: None,
|
||||
addr: None,
|
||||
};
|
||||
let mut flags = 0;
|
||||
for f in args.flag_session_flags.split(',') {
|
||||
flags |= match f {
|
||||
"http-only" => SessionFlags::HttpOnly,
|
||||
"secure" => SessionFlags::Secure,
|
||||
"same-site" => SessionFlags::SameSite,
|
||||
"same-site-strict" => SessionFlags::SameSiteStrict,
|
||||
_ => bail!("unknown session flag {:?}", f),
|
||||
} as i32;
|
||||
for f in &args.session_flags {
|
||||
flags |= *f as i32;
|
||||
}
|
||||
let uid = u.id;
|
||||
drop(u);
|
||||
let (sid, _) = l.make_session(creation, uid,
|
||||
args.flag_domain.as_ref().map(|d| d.as_bytes().to_owned()),
|
||||
args.domain.as_ref().map(|d| d.as_bytes().to_owned()),
|
||||
flags, permissions)?;
|
||||
let mut encoded = [0u8; 64];
|
||||
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
|
||||
let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8");
|
||||
|
||||
if let Some(ref p) = args.flag_curl_cookie_jar {
|
||||
let d = args.flag_domain.as_ref()
|
||||
if let Some(ref p) = args.curl_cookie_jar {
|
||||
let d = args.domain.as_ref()
|
||||
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?;
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
|
@ -139,11 +121,11 @@ pub fn run() -> Result<(), Error> {
|
|||
|
||||
fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
|
||||
format!("{httponly}{domain}\t{tailmatch}\t{path}\t{secure}\t{expires}\t{name}\t{value}",
|
||||
httponly=if (flags & SessionFlags::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" },
|
||||
httponly=if (flags & SessionFlag::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" },
|
||||
domain=domain,
|
||||
tailmatch="FALSE",
|
||||
path="/",
|
||||
secure=if (flags & SessionFlags::Secure as i32) != 0 { "TRUE" } else { "FALSE" },
|
||||
secure=if (flags & SessionFlag::Secure as i32) != 0 { "TRUE" } else { "FALSE" },
|
||||
expires="9223372036854775807", // 64-bit CURL_OFF_T_MAX, never expires
|
||||
name="s",
|
||||
value=cookie)
|
||||
|
@ -153,24 +135,10 @@ fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_args() {
|
||||
let args: Args = docopt::Docopt::new(USAGE).unwrap()
|
||||
.argv(&["nvr", "login", "--curl-cookie-jar=foo.txt", "slamb"])
|
||||
.deserialize().unwrap();
|
||||
assert_eq!(args, Args {
|
||||
flag_db_dir: "/var/lib/moonfire-nvr/db".to_owned(),
|
||||
flag_curl_cookie_jar: Some(PathBuf::from("foo.txt")),
|
||||
flag_session_flags: "http-only,secure,same-site,same-site-strict".to_owned(),
|
||||
arg_username: "slamb".to_owned(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_curl_cookie() {
|
||||
assert_eq!(curl_cookie("o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q",
|
||||
SessionFlags::HttpOnly as i32, "localhost"),
|
||||
SessionFlag::HttpOnly as i32, "localhost"),
|
||||
"#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\
|
||||
o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q");
|
||||
}
|
||||
|
|
|
@ -29,48 +29,19 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use db::dir;
|
||||
use docopt;
|
||||
use failure::{Error, Fail};
|
||||
use nix::fcntl::FlockArg;
|
||||
use rusqlite;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
mod check;
|
||||
mod config;
|
||||
mod login;
|
||||
mod init;
|
||||
mod run;
|
||||
mod sql;
|
||||
mod ts;
|
||||
mod upgrade;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub enum Command {
|
||||
Check,
|
||||
Config,
|
||||
Login,
|
||||
Init,
|
||||
Run,
|
||||
Sql,
|
||||
Ts,
|
||||
Upgrade,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
match *self {
|
||||
Command::Check => check::run(),
|
||||
Command::Config => config::run(),
|
||||
Command::Login => login::run(),
|
||||
Command::Init => init::run(),
|
||||
Command::Run => run::run(),
|
||||
Command::Sql => sql::run(),
|
||||
Command::Ts => ts::run(),
|
||||
Command::Upgrade => upgrade::run(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod check;
|
||||
pub mod config;
|
||||
pub mod login;
|
||||
pub mod init;
|
||||
pub mod run;
|
||||
pub mod sql;
|
||||
pub mod ts;
|
||||
pub mod upgrade;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum OpenMode {
|
||||
|
@ -81,10 +52,10 @@ enum OpenMode {
|
|||
|
||||
/// Locks the directory without opening the database.
|
||||
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is.
|
||||
fn open_dir(db_dir: &str, mode: OpenMode) -> Result<dir::Fd, Error> {
|
||||
fn open_dir(db_dir: &Path, mode: OpenMode) -> Result<dir::Fd, Error> {
|
||||
let dir = dir::Fd::open(db_dir, mode == OpenMode::Create)?;
|
||||
let ro = mode == OpenMode::ReadOnly;
|
||||
dir.lock(if ro { FlockArg::LockExclusiveNonblock } else { FlockArg::LockSharedNonblock })
|
||||
dir.lock(if ro { FlockArg::LockSharedNonblock } else { FlockArg::LockExclusiveNonblock })
|
||||
.map_err(|e| e.context(format!("db dir {:?} already in use; can't get {} lock",
|
||||
db_dir, if ro { "shared" } else { "exclusive" })))?;
|
||||
Ok(dir)
|
||||
|
@ -92,10 +63,10 @@ fn open_dir(db_dir: &str, mode: OpenMode) -> Result<dir::Fd, Error> {
|
|||
|
||||
/// Locks and opens the database.
|
||||
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is.
|
||||
fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> {
|
||||
fn open_conn(db_dir: &Path, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> {
|
||||
let dir = open_dir(db_dir, mode)?;
|
||||
let conn = rusqlite::Connection::open_with_flags(
|
||||
Path::new(&db_dir).join("db"),
|
||||
db_dir.join("db"),
|
||||
match mode {
|
||||
OpenMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
|
||||
OpenMode::ReadWrite => rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE,
|
||||
|
@ -108,9 +79,3 @@ fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connect
|
|||
rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX)?;
|
||||
Ok((dir, conn))
|
||||
}
|
||||
|
||||
fn parse_args<'a, T>(usage: &str) -> Result<T, Error> where T: ::serde::Deserialize<'a> {
|
||||
Ok(docopt::Docopt::new(usage)
|
||||
.and_then(|d| d.deserialize())
|
||||
.unwrap_or_else(|e| e.exit()))
|
||||
}
|
||||
|
|
109
src/cmds/run.rs
109
src/cmds/run.rs
|
@ -1,5 +1,5 @@
|
|||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// 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
|
||||
|
@ -33,19 +33,60 @@ use crate::stream;
|
|||
use crate::streamer;
|
||||
use crate::web;
|
||||
use db::{dir, writer};
|
||||
use failure::{Error, ResultExt, bail};
|
||||
use failure::{Error, bail};
|
||||
use fnv::FnvHashMap;
|
||||
use futures::future::FutureExt;
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use log::{info, warn};
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use structopt::StructOpt;
|
||||
use tokio;
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
|
||||
/// Directory holding user interface files (.html, .js, etc).
|
||||
#[structopt(default_value = "/usr/local/lib/moonfire-nvr/ui", value_name="path",
|
||||
parse(from_os_str))]
|
||||
ui_dir: std::path::PathBuf,
|
||||
|
||||
/// Bind address for unencrypted HTTP server.
|
||||
#[structopt(long, default_value = "0.0.0.0:8080", parse(try_from_str))]
|
||||
http_addr: std::net::SocketAddr,
|
||||
|
||||
/// Open the database in read-only mode and disables recording.
|
||||
///
|
||||
/// Note this is incompatible with authentication, so you'll likely want to specify
|
||||
/// --allow_unauthenticated_permissions.
|
||||
#[structopt(long)]
|
||||
read_only: bool,
|
||||
|
||||
/// Allow unauthenticated access to the web interface, with the given permissions (may be
|
||||
/// empty). Should be a text Permissions protobuf such as "view_videos: true".
|
||||
///
|
||||
/// Note that even an empty string allows some basic access that would be rejected if the
|
||||
/// argument were omitted.
|
||||
#[structopt(long, parse(try_from_str = protobuf::text_format::parse_from_str))]
|
||||
allow_unauthenticated_permissions: Option<db::Permissions>,
|
||||
|
||||
/// Trust X-Real-IP: and X-Forwarded-Proto: headers on the incoming request.
|
||||
///
|
||||
/// Set this only after ensuring your proxy server is configured to set them and that no
|
||||
/// untrusted requests bypass the proxy server. You may want to specify
|
||||
/// --http-addr=127.0.0.1:8080.
|
||||
#[structopt(long)]
|
||||
trust_forward_hdrs: bool,
|
||||
}
|
||||
|
||||
// These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles).
|
||||
// They seem to be correct for Linux and macOS at least.
|
||||
const LOCALTIME_PATH: &'static str = "/etc/localtime";
|
||||
|
@ -55,44 +96,6 @@ const ZONEINFO_PATHS: [&'static str; 2] = [
|
|||
"/var/db/timezone/zoneinfo/" // macOS High Sierra
|
||||
];
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Usage: moonfire-nvr run [options]
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--ui-dir=DIR Set the directory with the user interface files
|
||||
(.html, .js, etc).
|
||||
[default: /usr/local/lib/moonfire-nvr/ui]
|
||||
--http-addr=ADDR Set the bind address for the unencrypted HTTP server.
|
||||
[default: 0.0.0.0:8080]
|
||||
--read-only Forces read-only mode / disables recording.
|
||||
--allow-unauthenticated-permissions=PERMISSIONS
|
||||
Allow unauthenticated access to the web interface,
|
||||
with the given permissions (may be empty).
|
||||
PERMISSIONS should be a text Permissions protobuf
|
||||
such as "view_videos: true". NOTE: even an empty
|
||||
string allows some basic access that would be
|
||||
rejected if the argument were omitted.
|
||||
--trust-forward-hdrs Trust X-Real-IP: and X-Forwarded-Proto: headers on
|
||||
the incoming request. Set this only after ensuring
|
||||
your proxy server is configured to set them and that
|
||||
no untrusted requests bypass the proxy server.
|
||||
You may want to specify --http-addr=127.0.0.1:8080.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_http_addr: String,
|
||||
flag_ui_dir: String,
|
||||
flag_read_only: bool,
|
||||
flag_allow_unauthenticated_permissions: Option<String>,
|
||||
flag_trust_forward_hdrs: bool,
|
||||
}
|
||||
|
||||
fn trim_zoneinfo(p: &str) -> &str {
|
||||
for zp in &ZONEINFO_PATHS {
|
||||
if p.starts_with(zp) {
|
||||
|
@ -167,13 +170,12 @@ struct Syncer {
|
|||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
pub async fn run(args: &Args) -> Result<(), Error> {
|
||||
let clocks = clock::RealClocks {};
|
||||
let (_db_dir, conn) = super::open_conn(
|
||||
&args.flag_db_dir,
|
||||
if args.flag_read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?;
|
||||
let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.flag_read_only).unwrap());
|
||||
&args.db_dir,
|
||||
if args.read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?;
|
||||
let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.read_only).unwrap());
|
||||
info!("Database is loaded.");
|
||||
|
||||
{
|
||||
|
@ -186,22 +188,18 @@ pub async fn run() -> Result<(), Error> {
|
|||
|
||||
let time_zone_name = resolve_zone()?;
|
||||
info!("Resolved timezone: {}", &time_zone_name);
|
||||
let allow_unauthenticated_permissions = args.flag_allow_unauthenticated_permissions
|
||||
.map(|s| protobuf::text_format::parse_from_str(&s))
|
||||
.transpose()
|
||||
.context("Unable to parse --allow-unauthenticated-permissions")?;
|
||||
let s = web::Service::new(web::Config {
|
||||
db: db.clone(),
|
||||
ui_dir: Some(&args.flag_ui_dir),
|
||||
allow_unauthenticated_permissions,
|
||||
trust_forward_hdrs: args.flag_trust_forward_hdrs,
|
||||
ui_dir: Some(&args.ui_dir),
|
||||
allow_unauthenticated_permissions: args.allow_unauthenticated_permissions.clone(),
|
||||
trust_forward_hdrs: args.trust_forward_hdrs,
|
||||
time_zone_name,
|
||||
})?;
|
||||
|
||||
// Start a streamer for each stream.
|
||||
let shutdown_streamers = Arc::new(AtomicBool::new(false));
|
||||
let mut streamers = Vec::new();
|
||||
let syncers = if !args.flag_read_only {
|
||||
let syncers = if !args.read_only {
|
||||
let l = db.lock();
|
||||
let mut dirs = FnvHashMap::with_capacity_and_hasher(
|
||||
l.sample_file_dirs_by_id().len(), Default::default());
|
||||
|
@ -267,14 +265,13 @@ pub async fn run() -> Result<(), Error> {
|
|||
} else { None };
|
||||
|
||||
// Start the web interface.
|
||||
let addr = args.flag_http_addr.parse().unwrap();
|
||||
let make_svc = make_service_fn(move |_conn| {
|
||||
futures::future::ok::<_, std::convert::Infallible>(service_fn({
|
||||
let mut s = s.clone();
|
||||
move |req| Pin::from(s.serve(req))
|
||||
}))
|
||||
});
|
||||
let server = ::hyper::server::Server::bind(&addr)
|
||||
let server = ::hyper::server::Server::bind(&args.http_addr)
|
||||
.tcp_nodelay(true)
|
||||
.serve(make_svc);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2019 The Moonfire NVR Authors
|
||||
// Copyright (C) 2019-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// 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
|
||||
|
@ -31,45 +31,43 @@
|
|||
//! Subcommand to run a SQLite shell.
|
||||
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use super::OpenMode;
|
||||
use structopt::StructOpt;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Runs a SQLite shell on the Moonfire NVR database with locking.
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Directory holding the SQLite3 index database.
|
||||
#[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
|
||||
parse(from_os_str))]
|
||||
db_dir: PathBuf,
|
||||
|
||||
Usage:
|
||||
/// Opens the database in read-only mode and locks it only for shared access.
|
||||
///
|
||||
/// This can be run simultaneously with "moonfire-nvr run --read-only".
|
||||
#[structopt(long)]
|
||||
read_only: bool,
|
||||
|
||||
moonfire-nvr sql [options] [--] [<arg>...]
|
||||
moonfire-nvr sql --help
|
||||
|
||||
Positional arguments will be passed to sqlite3. Use the -- separator to pass
|
||||
sqlite3 options, as in "moonfire-nvr sql -- -line 'select username from user'".
|
||||
|
||||
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]
|
||||
--read-only Accesses the database in read-only mode.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_read_only: bool,
|
||||
arg_arg: Vec<String>,
|
||||
/// Arguments to pass to sqlite3.
|
||||
///
|
||||
/// Use the -- separator to pass sqlite3 options, as in
|
||||
/// "moonfire-nvr sql -- -line 'select username from user'".
|
||||
#[structopt(parse(from_os_str))]
|
||||
arg: Vec<OsString>,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
|
||||
let mode = if args.flag_read_only { OpenMode::ReadWrite } else { OpenMode::ReadOnly };
|
||||
let _db_dir = super::open_dir(&args.flag_db_dir, mode)?;
|
||||
let mut db = format!("file:{}/db", &args.flag_db_dir);
|
||||
if args.flag_read_only {
|
||||
db.push_str("?mode=ro");
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let mode = if args.read_only { OpenMode::ReadOnly } else { OpenMode::ReadWrite };
|
||||
let _db_dir = super::open_dir(&args.db_dir, mode)?;
|
||||
let mut db = OsString::new();
|
||||
db.push("file:");
|
||||
db.push(&args.db_dir);
|
||||
db.push("/db");
|
||||
if args.read_only {
|
||||
db.push("?mode=ro");
|
||||
}
|
||||
Command::new("sqlite3").arg(&db).args(&args.arg_arg).status()?;
|
||||
Command::new("sqlite3").arg(&db).args(&args.arg).status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// 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
|
||||
|
@ -29,21 +29,22 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Usage: moonfire-nvr ts <ts>...
|
||||
moonfire-nvr ts --help
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
arg_ts: Vec<String>,
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
/// Timestamp(s) to translate.
|
||||
///
|
||||
/// May be either an integer or an RFC-3339-like string:
|
||||
/// YYYY-mm-dd[THH:MM[:SS[:FFFFF]]][{Z,{+,-,}HH:MM}].
|
||||
///
|
||||
/// Eg: 142913484000000, 2020-04-26, 2020-04-26T12:00:00:00000-07:00.
|
||||
#[structopt(required = true)]
|
||||
timestamps: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let arg: Args = super::parse_args(&USAGE)?;
|
||||
for timestamp in &arg.arg_ts {
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
for timestamp in &args.timestamps {
|
||||
let t = db::recording::Time::parse(timestamp)?;
|
||||
println!("{} == {}", t, t.0);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// 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
|
||||
|
@ -33,45 +33,38 @@
|
|||
/// See `guide/schema.md` for more information.
|
||||
|
||||
use failure::Error;
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Upgrade to the latest database schema.
|
||||
|
||||
Usage: moonfire-nvr upgrade [options]
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--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 When upgrading from schema version 1 to 2, the sample file directory.
|
||||
This is typically on a hard drive.
|
||||
--preset-journal=MODE Resets the SQLite journal_mode to the specified mode
|
||||
prior to the upgrade. The default, delete, is
|
||||
recommended. off is very dangerous but may be
|
||||
desirable in some circumstances. See guide/schema.md
|
||||
for more information. The journal mode will be reset
|
||||
to wal after the upgrade.
|
||||
[default: delete]
|
||||
--no-vacuum Skips the normal post-upgrade vacuum operation.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_sample_file_dir: Option<String>,
|
||||
flag_preset_journal: String,
|
||||
flag_no_vacuum: bool,
|
||||
#[structopt(long,
|
||||
help = "Directory holding the SQLite3 index database.",
|
||||
default_value = "/var/lib/moonfire-nvr/db",
|
||||
parse(from_os_str))]
|
||||
db_dir: std::path::PathBuf,
|
||||
|
||||
#[structopt(help = "When upgrading from schema version 1 to 2, the sample file directory.",
|
||||
long, parse(from_os_str))]
|
||||
sample_file_dir: Option<std::path::PathBuf>,
|
||||
|
||||
#[structopt(help = "Resets the SQLite journal_mode to the specified mode prior to the \
|
||||
upgrade. The default, delete, is recommended. off is very dangerous \
|
||||
but may be desirable in some circumstances. See guide/schema.md for \
|
||||
more information. The journal mode will be reset to wal after the \
|
||||
upgrade.",
|
||||
long, default_value = "delete")]
|
||||
preset_journal: String,
|
||||
|
||||
#[structopt(help = "Skips the normal post-upgrade vacuum operation.", long)]
|
||||
no_vacuum: bool,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
|
||||
|
||||
db::upgrade::run(&db::upgrade::Args {
|
||||
flag_sample_file_dir: args.flag_sample_file_dir.as_ref().map(|s| s.as_str()),
|
||||
flag_preset_journal: &args.flag_preset_journal,
|
||||
flag_no_vacuum: args.flag_no_vacuum,
|
||||
sample_file_dir: args.sample_file_dir.as_ref().map(std::path::PathBuf::as_path),
|
||||
preset_journal: &args.preset_journal,
|
||||
no_vacuum: args.no_vacuum,
|
||||
}, &mut conn)
|
||||
}
|
||||
|
|
85
src/main.rs
85
src/main.rs
|
@ -1,5 +1,5 @@
|
|||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2016 The Moonfire NVR Authors
|
||||
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||
//
|
||||
// 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
|
||||
|
@ -31,7 +31,7 @@
|
|||
#![cfg_attr(all(feature="nightly", test), feature(test))]
|
||||
|
||||
use log::{error, info};
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
mod body;
|
||||
mod cmds;
|
||||
|
@ -43,39 +43,53 @@ mod stream;
|
|||
mod streamer;
|
||||
mod web;
|
||||
|
||||
/// Commandline usage string. This is in the particular format expected by the `docopt` crate.
|
||||
/// Besides being printed on --help or argument parsing error, it's actually parsed to define the
|
||||
/// allowed commandline arguments and their defaults.
|
||||
const USAGE: &'static str = "
|
||||
Usage: moonfire-nvr <command> [<args>...]
|
||||
moonfire-nvr (--help | --version)
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(name="moonfire-nvr", about="security camera network video recorder")]
|
||||
enum Args {
|
||||
/// Checks database integrity (like fsck).
|
||||
Check(cmds::check::Args),
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--version Show the version of moonfire-nvr.
|
||||
/// Interactively edits configuration.
|
||||
Config(cmds::config::Args),
|
||||
|
||||
Commands:
|
||||
check Check database integrity
|
||||
init Initialize a database
|
||||
run Run the daemon: record from cameras and serve HTTP
|
||||
shell Start an interactive shell to modify the database
|
||||
ts Translate human-readable and numeric timestamps
|
||||
upgrade Upgrade the database to the latest schema
|
||||
";
|
||||
/// Initializes a database.
|
||||
Init(cmds::init::Args),
|
||||
|
||||
/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Args {
|
||||
arg_command: Option<cmds::Command>,
|
||||
/// Logs in a user, returning the session cookie.
|
||||
///
|
||||
/// This is a privileged command that directly accesses the database. It doesn't check the
|
||||
/// user's password and even can be used to create sessions with permissions the user doesn't
|
||||
/// have.
|
||||
Login(cmds::login::Args),
|
||||
|
||||
/// Runs the server, saving recordings and allowing web access.
|
||||
Run(cmds::run::Args),
|
||||
|
||||
/// Runs a SQLite3 shell on Moonfire NVR's index database.
|
||||
///
|
||||
/// Note this locks the database to prevent simultaneous access with a running server. The
|
||||
/// server maintains cached state which could be invalidated otherwise.
|
||||
Sql(cmds::sql::Args),
|
||||
|
||||
/// Translates between integer and human-readable timestamps.
|
||||
Ts(cmds::ts::Args),
|
||||
|
||||
/// Upgrades to the latest database schema.
|
||||
Upgrade(cmds::upgrade::Args),
|
||||
}
|
||||
|
||||
fn version() -> String {
|
||||
let major = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||
let minor = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||
let patch = option_env!("CARGO_PKG_VERSION_MAJOR");
|
||||
match (major, minor, patch) {
|
||||
(Some(major), Some(minor), Some(patch)) => format!("{}.{}.{}", major, minor, patch),
|
||||
_ => "".to_owned(),
|
||||
impl Args {
|
||||
fn run(&self) -> Result<(), failure::Error> {
|
||||
match self {
|
||||
Args::Check(ref a) => cmds::check::run(a),
|
||||
Args::Config(ref a) => cmds::config::run(a),
|
||||
Args::Init(ref a) => cmds::init::run(a),
|
||||
Args::Login(ref a) => cmds::login::run(a),
|
||||
Args::Run(ref a) => cmds::run::run(a),
|
||||
Args::Sql(ref a) => cmds::sql::run(a),
|
||||
Args::Ts(ref a) => cmds::ts::run(a),
|
||||
Args::Upgrade(ref a) => cmds::upgrade::run(a),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,14 +102,7 @@ fn parse_fmt<S: AsRef<str>>(fmt: S) -> Option<mylog::Format> {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
// Parse commandline arguments.
|
||||
// (Note this differs from cmds::parse_args in that it specifies options_first.)
|
||||
let args: Args = docopt::Docopt::new(USAGE)
|
||||
.and_then(|d| d.options_first(true)
|
||||
.version(Some(version()))
|
||||
.deserialize())
|
||||
.unwrap_or_else(|e| e.exit());
|
||||
|
||||
let args = Args::from_args();
|
||||
let mut h = mylog::Builder::new()
|
||||
.set_format(::std::env::var("MOONFIRE_FORMAT")
|
||||
.ok()
|
||||
|
@ -105,7 +112,7 @@ fn main() {
|
|||
.build();
|
||||
h.clone().install().unwrap();
|
||||
|
||||
if let Err(e) = { let _a = h.r#async(); args.arg_command.unwrap().run() } {
|
||||
if let Err(e) = { let _a = h.r#async(); args.run() } {
|
||||
error!("{:?}", e);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
|
|
14
src/web.rs
14
src/web.rs
|
@ -588,10 +588,10 @@ impl ServiceInner {
|
|||
}.to_owned();
|
||||
let mut l = self.db.lock();
|
||||
let is_secure = self.is_secure(req);
|
||||
let flags = (auth::SessionFlags::HttpOnly as i32) |
|
||||
(auth::SessionFlags::SameSite as i32) |
|
||||
(auth::SessionFlags::SameSiteStrict as i32) |
|
||||
if is_secure { auth::SessionFlags::Secure as i32 } else { 0 };
|
||||
let flags = (auth::SessionFlag::HttpOnly as i32) |
|
||||
(auth::SessionFlag::SameSite as i32) |
|
||||
(auth::SessionFlag::SameSiteStrict as i32) |
|
||||
if is_secure { auth::SessionFlag::Secure as i32 } else { 0 };
|
||||
let (sid, _) = l.login_by_password(authreq, &r.username, r.password, Some(domain),
|
||||
flags)
|
||||
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
|
||||
|
@ -797,7 +797,7 @@ async fn with_json_body(mut req: Request<hyper::Body>)
|
|||
|
||||
pub struct Config<'a> {
|
||||
pub db: Arc<db::Database>,
|
||||
pub ui_dir: Option<&'a str>,
|
||||
pub ui_dir: Option<&'a std::path::Path>,
|
||||
pub trust_forward_hdrs: bool,
|
||||
pub time_zone_name: String,
|
||||
pub allow_unauthenticated_permissions: Option<db::Permissions>,
|
||||
|
@ -840,12 +840,12 @@ impl Service {
|
|||
})))
|
||||
}
|
||||
|
||||
fn fill_ui_files(dir: &str, files: &mut HashMap<String, UiFile>) {
|
||||
fn fill_ui_files(dir: &std::path::Path, files: &mut HashMap<String, UiFile>) {
|
||||
let r = match fs::read_dir(dir) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!("Unable to search --ui-dir={}; will serve no static files. Error was: {}",
|
||||
dir, e);
|
||||
dir.display(), e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue