switch from ancient clap/structopt release to bpaf

Improves #70: this reduces binary size from 12.3 MiB to 11.9 MiB (3%) on
macOS/arm64.

The user experience is almost the same. (The help output's `Usage:`
lines lack the e.g. `moonfire-nvr run` prefix of argv[0] and subcommand,
which isn't ideal, but I guess it's pretty minor in the grand scheme of
things.)
This commit is contained in:
Scott Lamb 2023-02-11 11:38:12 -08:00
parent 23c1b9404b
commit e21f795e93
No known key found for this signature in database
12 changed files with 229 additions and 217 deletions

139
server/Cargo.lock generated
View File

@ -47,15 +47,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.68" version = "1.0.68"
@ -153,6 +144,27 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bpaf"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863c0b21775e45ebf9bbb3a6d0cd9b3421c88a036e825359e3d4015561f3e23c"
dependencies = [
"bpaf_derive",
"owo-colors",
]
[[package]]
name = "bpaf_derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ed63113bfb3a9bb25dd131acdf0044c7404000ea57dd9eec1f221e3547b4748"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "1.1.0" version = "1.1.0"
@ -220,20 +232,6 @@ dependencies = [
"inout", "inout",
] ]
[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"term_size",
"textwrap",
"unicode-width",
]
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.11.1" version = "0.11.1"
@ -756,15 +754,6 @@ dependencies = [
"hashbrown 0.13.1", "hashbrown 0.13.1",
] ]
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -974,6 +963,12 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e"
[[package]]
name = "is_ci"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@ -1163,9 +1158,9 @@ version = "0.7.5"
dependencies = [ dependencies = [
"base64", "base64",
"blake3", "blake3",
"bpaf",
"byteorder", "byteorder",
"bytes", "bytes",
"clap",
"cursive", "cursive",
"failure", "failure",
"fnv", "fnv",
@ -1196,7 +1191,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"smallvec", "smallvec",
"structopt",
"sync_wrapper", "sync_wrapper",
"tempfile", "tempfile",
"time 0.1.45", "time 0.1.45",
@ -1412,6 +1406,15 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "owo-colors"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
dependencies = [
"supports-color",
]
[[package]] [[package]]
name = "password-hash" name = "password-hash"
version = "0.4.2" version = "0.4.2"
@ -1488,30 +1491,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.49" version = "1.0.49"
@ -1967,36 +1946,22 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "structopt"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
dependencies = [
"clap",
"lazy_static",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.4.1" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "supports-color"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f"
dependencies = [
"atty",
"is_ci",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.107" version = "1.0.107"
@ -2059,16 +2024,6 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"term_size",
"unicode-width",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.38" version = "1.0.38"

View File

@ -2,7 +2,7 @@
name = "moonfire-nvr" name = "moonfire-nvr"
version = "0.7.5" version = "0.7.5"
authors = ["Scott Lamb <slamb@slamb.org>"] authors = ["Scott Lamb <slamb@slamb.org>"]
edition = "2018" edition = "2021"
resolver = "2" resolver = "2"
license-file = "../LICENSE.txt" license-file = "../LICENSE.txt"
rust-version = "1.64" rust-version = "1.64"
@ -24,9 +24,9 @@ members = ["base", "db"]
base = { package = "moonfire-base", path = "base" } base = { package = "moonfire-base", path = "base" }
base64 = "0.13.0" base64 = "0.13.0"
blake3 = "1.0.0" blake3 = "1.0.0"
bpaf = { version = "0.7.8", features = ["autocomplete", "bright-color", "derive"] }
bytes = "1" bytes = "1"
byteorder = "1.0" byteorder = "1.0"
clap = { version = "2.33.3", default-features = false, features = ["color", "wrap_help"] }
cursive = "0.20.0" cursive = "0.20.0"
db = { package = "moonfire-db", path = "db" } db = { package = "moonfire-db", path = "db" }
failure = "0.1.1" failure = "0.1.1"
@ -53,7 +53,6 @@ rusqlite = "0.28.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
smallvec = { version = "1.7", features = ["union"] } smallvec = { version = "1.7", features = ["union"] }
structopt = { version = "0.3.13", default-features = false }
sync_wrapper = "0.1.0" sync_wrapper = "0.1.0"
time = "0.1" time = "0.1"
tokio = { version = "1.24", features = ["macros", "rt-multi-thread", "signal", "sync", "time"] } tokio = { version = "1.24", features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }

View File

@ -4,45 +4,39 @@
//! Subcommand to check the database and sample file dir for errors. //! Subcommand to check the database and sample file dir for errors.
use bpaf::Bpaf;
use db::check; use db::check;
use failure::Error; use failure::Error;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt;
#[derive(StructOpt)] #[derive(Bpaf, Debug)]
pub struct Args { pub struct Args {
/// Directory holding the SQLite3 index database. /// Directory holding the SQLite3 index database.
#[structopt( ///
long, /// default: `/var/lib/moonfire-nvr/db`.
default_value = "/var/lib/moonfire-nvr/db", #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))]
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf, db_dir: PathBuf,
/// Compare sample file lengths on disk to the database. /// Compares sample file lengths on disk to the database.
#[structopt(long)]
compare_lens: bool, compare_lens: bool,
/// Trash sample files without matching recording rows in the database. /// Trashes sample files without matching recording rows in the database.
/// This addresses "Missing ... row" errors. /// This addresses `Missing ... row` errors.
/// ///
/// The ids are added to the "garbage" table to indicate the files need to /// The ids are added to the `garbage` table to indicate the files need to
/// be deleted. Garbage is collected on normal startup. /// be deleted. Garbage is collected on normal startup.
#[structopt(long)]
trash_orphan_sample_files: bool, trash_orphan_sample_files: bool,
/// Delete recording rows in the database without matching sample files. /// Deletes recording rows in the database without matching sample files.
/// This addresses "Recording ... missing file" errors. ///
#[structopt(long)] /// This addresses `Recording ... missing file` errors.
delete_orphan_rows: bool, delete_orphan_rows: bool,
/// Trash recordings when their database rows appear corrupt. /// Trashes recordings when their database rows appear corrupt.
/// This addresses "bad video_index" errors. /// This addresses "bad video_index" errors.
/// ///
/// The ids are added to the "garbage" table to indicate their files need to /// The ids are added to the `garbage` table to indicate their files need to
/// be deleted. Garbage is collected on normal startup. /// be deleted. Garbage is collected on normal startup.
#[structopt(long)]
trash_corrupt_rows: bool, trash_corrupt_rows: bool,
} }

View File

@ -8,26 +8,23 @@
//! configuration will likely be almost entirely done through a web-based UI. //! configuration will likely be almost entirely done through a web-based UI.
use base::clock; use base::clock;
use bpaf::Bpaf;
use cursive::views; use cursive::views;
use cursive::Cursive; use cursive::Cursive;
use failure::Error; use failure::Error;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use structopt::StructOpt;
mod cameras; mod cameras;
mod dirs; mod dirs;
mod users; mod users;
#[derive(StructOpt)] #[derive(Bpaf, Debug)]
pub struct Args { pub struct Args {
/// Directory holding the SQLite3 index database. /// Directory holding the SQLite3 index database.
#[structopt( ///
long, /// default: `/var/lib/moonfire-nvr/db`.
default_value = "/var/lib/moonfire-nvr/db", #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))]
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf, db_dir: PathBuf,
} }

View File

@ -2,20 +2,17 @@
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
use bpaf::Bpaf;
use failure::Error; use failure::Error;
use log::info; use log::info;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt;
#[derive(StructOpt)] #[derive(Bpaf, Debug)]
pub struct Args { pub struct Args {
/// Directory holding the SQLite3 index database. /// Directory holding the SQLite3 index database.
#[structopt( ///
long, /// default: `/var/lib/moonfire-nvr/db`.
default_value = "/var/lib/moonfire-nvr/db", #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))]
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf, db_dir: PathBuf,
} }

View File

@ -5,51 +5,58 @@
//! Subcommand to login a user (without requiring a password). //! Subcommand to login a user (without requiring a password).
use base::clock::{self, Clocks}; use base::clock::{self, Clocks};
use bpaf::Bpaf;
use db::auth::SessionFlag; use db::auth::SessionFlag;
use failure::{format_err, Error}; use failure::{format_err, Error};
use std::io::Write as _; use std::io::Write as _;
use std::os::unix::fs::OpenOptionsExt as _; use std::os::unix::fs::OpenOptionsExt as _;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt; use std::str::FromStr;
#[derive(Debug, Default, StructOpt)] fn parse_perms(perms: String) -> Result<db::Permissions, protobuf::text_format::ParseError> {
protobuf::text_format::parse_from_str(&perms)
}
fn parse_flags(flags: String) -> Result<Vec<SessionFlag>, Error> {
flags.split(',').map(SessionFlag::from_str).collect()
}
#[derive(Bpaf, Debug)]
pub struct Args { pub struct Args {
/// Directory holding the SQLite3 index database. /// Directory holding the SQLite3 index database.
#[structopt( ///
long, /// default: `/var/lib/moonfire-nvr/db`.
default_value = "/var/lib/moonfire-nvr/db", #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))]
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf, db_dir: PathBuf,
/// Create a session with the given permissions. /// Creates a session with the given permissions.
/// ///
/// If unspecified, uses user's default permissions. /// If unspecified, uses user's default permissions.
#[structopt(long, value_name="perms", #[bpaf(argument::<String>("PERMS"), parse(parse_perms), optional)]
parse(try_from_str = protobuf::text_format::parse_from_str))]
permissions: Option<db::Permissions>, permissions: Option<db::Permissions>,
/// Restrict this cookie to the given domain. /// Restricts this cookie to the given domain.
#[structopt(long)] #[bpaf(argument("DOMAIN"))]
domain: Option<String>, domain: Option<String>,
/// Write the cookie to a new curl-compatible cookie-jar file. /// Writes 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. /// `--domain` must be specified. This file can be used later with curl's `--cookie` flag.
#[structopt(long, requires("domain"), value_name = "path")] #[bpaf(argument("PATH"))]
curl_cookie_jar: Option<PathBuf>, curl_cookie_jar: Option<PathBuf>,
/// Set the given db::auth::SessionFlags. /// Sets the given db::auth::SessionFlags.
#[structopt( ///
long, /// default: `http-only,secure,same-site,same-site-strict`.
default_value = "http-only,secure,same-site,same-site-strict", #[bpaf(
value_name = "flags", argument::<String>("FLAGS"),
use_delimiter = true fallback_with(|| Ok::<_, std::convert::Infallible>("http-only,secure,same-site,same-site-strict".to_owned())),
parse(parse_flags),
)] )]
session_flags: Vec<SessionFlag>, session_flags: Vec<SessionFlag>,
/// Create the session for this username. /// Create the session for this username.
#[bpaf(argument("USERNAME"))]
username: String, username: String,
} }
@ -87,7 +94,7 @@ pub fn run(args: Args) -> Result<i32, Error> {
let d = args let d = args
.domain .domain
.as_ref() .as_ref()
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?; .ok_or_else(|| format_err!("--curl-cookie-jar requires --domain"))?;
let mut f = std::fs::OpenOptions::new() let mut f = std::fs::OpenOptions::new()
.write(true) .write(true)
.create_new(true) .create_new(true)

View File

@ -27,6 +27,9 @@ pub struct ConfigFile {
pub binds: Vec<BindConfig>, pub binds: Vec<BindConfig>,
/// Directory holding the SQLite3 index database. /// Directory holding the SQLite3 index database.
///
///
/// default: `/var/lib/moonfire-nvr/db`.
#[serde(default = "default_db_dir")] #[serde(default = "default_db_dir")]
pub db_dir: PathBuf, pub db_dir: PathBuf,

View File

@ -6,6 +6,7 @@ use crate::streamer;
use crate::web; use crate::web;
use crate::web::accept::Listener; use crate::web::accept::Listener;
use base::clock; use base::clock;
use bpaf::Bpaf;
use db::{dir, writer}; use db::{dir, writer};
use failure::{bail, Error, ResultExt}; use failure::{bail, Error, ResultExt};
use fnv::FnvHashMap; use fnv::FnvHashMap;
@ -18,23 +19,24 @@ use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use structopt::StructOpt;
use tokio::signal::unix::{signal, SignalKind}; use tokio::signal::unix::{signal, SignalKind};
use self::config::ConfigFile; use self::config::ConfigFile;
mod config; mod config;
#[derive(StructOpt)] #[derive(Bpaf, Debug)]
pub struct Args { pub struct Args {
#[structopt(short, long, default_value = "/etc/moonfire-nvr.toml")] /// Path to configuration file.
///
/// default: `/etc/moonfire-nvr.toml`. See `ref/config.md` for config file documentation.
#[bpaf(short, long, argument("PATH"), fallback_with(|| Ok::<_, Error>("/etc/moonfire-nvr.toml".into())))]
config: PathBuf, config: PathBuf,
/// Open the database in read-only mode and disables recording. /// Opens the database in read-only mode and disables recording.
/// ///
/// Note this is incompatible with session authentication; consider adding /// Note this is incompatible with session authentication; consider adding
/// a bind with `allow_unauthenticated_permissions` to your config. /// a bind with `allowUnauthenticatedPermissions` to your config.
#[structopt(long)]
read_only: bool, read_only: bool,
} }

View File

@ -5,35 +5,31 @@
//! Subcommand to run a SQLite shell. //! Subcommand to run a SQLite shell.
use super::OpenMode; use super::OpenMode;
use bpaf::Bpaf;
use failure::Error; use failure::Error;
use std::ffi::OsString; use std::ffi::OsString;
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use structopt::StructOpt;
#[derive(StructOpt)] #[derive(Bpaf, Debug, PartialEq, Eq)]
pub struct Args { pub struct Args {
/// Directory holding the SQLite3 index database. /// Directory holding the SQLite3 index database.
#[structopt( ///
long, /// default: `/var/lib/moonfire-nvr/db`.
default_value = "/var/lib/moonfire-nvr/db", #[bpaf(fallback_with(crate::default_db_dir))]
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf, db_dir: PathBuf,
/// Opens the database in read-only mode and locks it only for shared access. /// 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". /// This can be run simultaneously with `moonfire-nvr run --read-only`.
#[structopt(long)]
read_only: bool, read_only: bool,
/// Arguments to pass to sqlite3. /// Arguments to pass to sqlite3.
/// ///
/// Use the -- separator to pass sqlite3 options, as in /// Use the `--` separator to pass sqlite3 options, as in
/// "moonfire-nvr sql -- -line 'select username from user'". /// `moonfire-nvr sql -- -line 'select username from user'`.
#[structopt(parse(from_os_str))] #[bpaf(positional)]
arg: Vec<OsString>, arg: Vec<OsString>,
} }
@ -58,3 +54,32 @@ pub fn run(args: Args) -> Result<i32, Error> {
.exec() .exec()
.into()) .into())
} }
#[cfg(test)]
mod tests {
use super::*;
use bpaf::Parser;
#[test]
fn parse_args() {
let args = args()
.to_options()
.run_inner(bpaf::Args::from(&[
"--db-dir",
"/foo/bar",
"--",
"-line",
"select username from user",
]))
.unwrap();
assert_eq!(
args,
Args {
db_dir: "/foo/bar".into(),
read_only: false, // default
arg: vec!["-line".into(), "select username from user".into()],
}
);
}
}

View File

@ -2,18 +2,18 @@
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
use bpaf::Bpaf;
use failure::Error; use failure::Error;
use structopt::StructOpt;
#[derive(StructOpt)] #[derive(Bpaf, Debug)]
pub struct Args { pub struct Args {
/// Timestamp(s) to translate. /// Timestamp(s) to translate.
/// ///
/// May be either an integer or an RFC-3339-like string: /// May be either an integer or an RFC-3339-like string:
/// `YYYY-mm-dd[THH:MM[:SS[:FFFFF]]][{Z,{+,-,}HH:MM}]`. /// `YYYY-mm-dd[THH:MM[:SS[:FFFFF]]][{Z,{+,-,}HH:MM}]`.
/// ///
/// Eg: `142913484000000`, `2020-04-26`, `2020-04-26T12:00:00:00000-07:00`. /// E.g.: `142913484000000`, `2020-04-26`, `2020-04-26T12:00:00:00000-07:00`.
#[structopt(required = true)] #[bpaf(positional("TS"), some("must specify at least one timestamp"))]
timestamps: Vec<String>, timestamps: Vec<String>,
} }

View File

@ -2,41 +2,36 @@
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
use bpaf::Bpaf;
/// Upgrades the database schema. /// Upgrades the database schema.
/// ///
/// See `guide/schema.md` for more information. /// See `guide/schema.md` for more information.
use failure::Error; use failure::Error;
use structopt::StructOpt;
#[derive(StructOpt)] #[derive(Bpaf, Debug)]
pub struct Args { pub struct Args {
#[structopt( /// Directory holding the SQLite3 index database.
long, ///
help = "Directory holding the SQLite3 index database.", ///
default_value = "/var/lib/moonfire-nvr/db", /// default: `/var/lib/moonfire-nvr/db`.
parse(from_os_str) #[bpaf(argument("PATH"), fallback_with(crate::default_db_dir))]
)]
db_dir: std::path::PathBuf, db_dir: std::path::PathBuf,
#[structopt( /// When upgrading from schema version 1 to 2, the sample file directory.
help = "When upgrading from schema version 1 to 2, the sample file directory.", #[bpaf(argument("PATH"))]
long,
parse(from_os_str)
)]
sample_file_dir: Option<std::path::PathBuf>, sample_file_dir: Option<std::path::PathBuf>,
#[structopt( /// Resets the SQLite journal_mode to the specified mode prior to
help = "Resets the SQLite journal_mode to the specified mode prior to \ /// the upgrade.
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 \ /// default: `delete` (recommended). `off` is very dangerous but may be
reset to wal after the upgrade.", /// desirable in some circumstances. See `guide/schema.md` for more
long, /// information. The journal mode will be reset to `wal` after the upgrade.
default_value = "delete" #[bpaf(argument("MODE"), fallback_with(|| Ok::<_, std::convert::Infallible>("delete".into())))]
)]
preset_journal: String, preset_journal: String,
#[structopt(help = "Skips the normal post-upgrade vacuum operation.", long)] /// Skips the normal post-upgrade vacuum operation.
no_vacuum: bool, no_vacuum: bool,
} }

View File

@ -4,10 +4,10 @@
#![cfg_attr(all(feature = "nightly", test), feature(test))] #![cfg_attr(all(feature = "nightly", test), feature(test))]
use bpaf::Bpaf;
use log::{debug, error}; use log::{debug, error};
use std::fmt::Write; use std::fmt::Write;
use std::str::FromStr; use std::str::FromStr;
use structopt::StructOpt;
mod body; mod body;
mod cmds; mod cmds;
@ -19,43 +19,50 @@ mod stream;
mod streamer; mod streamer;
mod web; mod web;
#[derive(StructOpt)] /// Moonfire NVR: security camera network video recorder.
#[structopt( #[derive(Bpaf, Debug)]
name = "moonfire-nvr", #[bpaf(options, version)]
about = "security camera network video recorder",
global_settings(&[clap::AppSettings::ColoredHelp])
)]
enum Args { enum Args {
/// Checks database integrity (like fsck). /// Checks database integrity (like fsck).
Check(cmds::check::Args), #[bpaf(command)]
Check(#[bpaf(external(cmds::check::args))] cmds::check::Args),
/// Interactively edits configuration. /// Interactively edits configuration.
Config(cmds::config::Args), #[bpaf(command)]
Config(#[bpaf(external(cmds::config::args))] cmds::config::Args),
/// Initializes a database. /// Initializes a database.
Init(cmds::init::Args), #[bpaf(command)]
Init(#[bpaf(external(cmds::init::args))] cmds::init::Args),
/// Logs in a user, returning the session cookie. /// Logs in a user, returning the session cookie.
/// ///
///
/// This is a privileged command that directly accesses the database. It doesn't check the /// 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 /// user's password and even can be used to create sessions with permissions the user doesn't
/// have. /// have.
Login(cmds::login::Args), #[bpaf(command)]
Login(#[bpaf(external(cmds::login::args))] cmds::login::Args),
/// Runs the server, saving recordings and allowing web access. /// Runs the server, saving recordings and allowing web access.
Run(cmds::run::Args), #[bpaf(command)]
Run(#[bpaf(external(cmds::run::args))] cmds::run::Args),
/// Runs a SQLite3 shell on Moonfire NVR's index database. /// Runs a SQLite3 shell on Moonfire NVR's index database.
/// ///
///
/// Note this locks the database to prevent simultaneous access with a running server. The /// Note this locks the database to prevent simultaneous access with a running server. The
/// server maintains cached state which could be invalidated otherwise. /// server maintains cached state which could be invalidated otherwise.
Sql(cmds::sql::Args), #[bpaf(command)]
Sql(#[bpaf(external(cmds::sql::args))] cmds::sql::Args),
/// Translates between integer and human-readable timestamps. /// Translates between integer and human-readable timestamps.
Ts(cmds::ts::Args), #[bpaf(command)]
Ts(#[bpaf(external(cmds::ts::args))] cmds::ts::Args),
/// Upgrades to the latest database schema. /// Upgrades to the latest database schema.
Upgrade(cmds::upgrade::Args), #[bpaf(command)]
Upgrade(#[bpaf(external(cmds::upgrade::args))] cmds::upgrade::Args),
} }
impl Args { impl Args {
@ -73,6 +80,11 @@ impl Args {
} }
} }
/// Returns the default database dir, for use in argument parsing with `bpaf(fallback_with(...))`.
fn default_db_dir() -> Result<std::path::PathBuf, std::convert::Infallible> {
Ok("/var/lib/moonfire-nvr/db".into())
}
/// Custom panic hook that logs instead of directly writing to stderr. /// Custom panic hook that logs instead of directly writing to stderr.
/// ///
/// This means it includes a timestamp and is more recognizable as a serious /// This means it includes a timestamp and is more recognizable as a serious
@ -112,7 +124,6 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
let args = Args::from_args();
let mut h = mylog::Builder::new() let mut h = mylog::Builder::new()
.set_format( .set_format(
::std::env::var("MOONFIRE_FORMAT") ::std::env::var("MOONFIRE_FORMAT")
@ -130,6 +141,22 @@ fn main() {
.build(); .build();
h.clone().install().unwrap(); h.clone().install().unwrap();
// TODO: remove this when bpaf adds more direct support for defaulting to `--help`.
// See discussion: <https://github.com/pacak/bpaf/discussions/165>.
if std::env::args_os().len() < 2 {
match args().run_inner(bpaf::Args::from(&["--help"])) {
Ok(a) => panic!("bpaf --help should not return Ok: {a:#?}"),
Err(bpaf::ParseFailure::Stdout(msg)) => {
print!("{msg}");
std::process::exit(0);
}
Err(bpaf::ParseFailure::Stderr(msg)) => {
eprint!("{msg}");
std::process::exit(1);
}
}
}
let use_panic_hook = ::std::env::var("MOONFIRE_PANIC_HOOK") let use_panic_hook = ::std::env::var("MOONFIRE_PANIC_HOOK")
.map(|s| s != "false" && s != "0") .map(|s| s != "false" && s != "0")
.unwrap_or(true); .unwrap_or(true);
@ -137,6 +164,9 @@ fn main() {
std::panic::set_hook(Box::new(&panic_hook)); std::panic::set_hook(Box::new(&panic_hook));
} }
let args = args().run();
log::trace!("Parsed command-line arguments: {args:#?}");
let r = { let r = {
let _a = h.async_scope(); let _a = h.async_scope();
args.run() args.run()
@ -152,3 +182,11 @@ fn main() {
} }
} }
} }
#[cfg(test)]
mod tests {
#[test]
fn bpaf_invariants() {
super::args().check_invariants(false);
}
}