diff --git a/README.md b/README.md index af6411b..3468b63 100644 --- a/README.md +++ b/README.md @@ -161,8 +161,8 @@ state: Both kinds of state are intended to be accessed only by Moonfire NVR itself. However, the interface for adding new cameras is not yet written, so you will -have to manually create the database and insert cameras with the `sqlite3` -command line tool prior to starting Moonfire NVR. +have to manually insert cameras with the `sqlite3` command line tool prior to +starting Moonfire NVR. Manual commands would look something like this: @@ -170,7 +170,7 @@ Manual commands would look something like this: $ sudo adduser --system moonfire-nvr --home /var/lib/moonfire-nvr $ sudo mkdir /var/lib/moonfire-nvr $ sudo -u moonfire-nvr -H mkdir db sample - $ sudo -u moonfire-nvr sqlite3 ~moonfire-nvr/db/db < path/to/schema.sql + $ sudo -u moonfire-nvr moonfire-nvr init ## Camera configuration and hard drive mounting diff --git a/prep.sh b/prep.sh index 18603d8..52e4a5f 100755 --- a/prep.sh +++ b/prep.sh @@ -216,7 +216,7 @@ if [ ! -d "${DB_DIR}" ]; then fi DB_PATH="${DB_DIR}/${DB_NAME}" CAMERAS_PATH="${SRC_DIR}/../cameras.sql" -[ "${SKIP_DB:-0}" == 0 ] && sudo -u ${NVR_USER} -H sqlite3 "${DB_PATH}" < "${SRC_DIR}/schema.sql" +[ "${SKIP_DB:-0}" == 0 ] && sudo -u ${NVR_USER} -H ${SERVICE_BIN} init --db-dir="${DB_PATH}" if [ -r "${CAMERAS_PATH}" ]; then echo 'Add cameras...'; echo sudo -u ${NVR_USER} -H sqlite3 "${DB_PATH}" < "${CAMERAS_PATH}" diff --git a/src/cmds/check.rs b/src/cmds/check.rs index 47f3e55..2e40992 100644 --- a/src/cmds/check.rs +++ b/src/cmds/check.rs @@ -99,7 +99,7 @@ struct File { 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, true)?; + let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadOnly)?; let mut files = Vec::new(); for e in fs::read_dir(&args.flag_sample_file_dir)? { let e = e?; diff --git a/src/cmds/init.rs b/src/cmds/init.rs new file mode 100644 index 0000000..16b7b26 --- /dev/null +++ b/src/cmds/init.rs @@ -0,0 +1,75 @@ +// This file is part of Moonfire NVR, a security camera digital video recorder. +// Copyright (C) 2016 Scott Lamb +// +// 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 . + +use db; +use error::Error; + +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, RustcDecodable)] +struct Args { + flag_db_dir: String, +} + +pub fn run() -> Result<(), Error> { + let args: Args = super::parse_args(USAGE)?; + super::install_logger(false); + let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::Create)?; + + // Check if the database has already been initialized. + let cur_ver = db::get_schema_version(&conn)?; + if let Some(v) = cur_ver { + info!("Database is already initialized with schema version {}.", v); + return Ok(()); + } + + conn.execute_batch(r#" + pragma journal_mode = wal; + pragma page_size = 16384; + "#)?; + let tx = conn.transaction()?; + tx.execute_batch(include_str!("../schema.sql"))?; + tx.commit()?; + info!("Database initialized."); + Ok(()) +} diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 73ece47..de6667e 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -40,25 +40,28 @@ use slog_term; use std::path::Path; mod check; +mod init; mod run; mod ts; mod upgrade; #[derive(Debug, RustcDecodable)] pub enum Command { - Run, - Upgrade, Check, + Init, + Run, Ts, + Upgrade, } impl Command { pub fn run(&self) -> Result<(), Error> { match *self { - Command::Run => run::run(), - Command::Upgrade => upgrade::run(), Command::Check => check::run(), + Command::Init => init::run(), + Command::Run => run::run(), Command::Ts => ts::run(), + Command::Upgrade => upgrade::run(), } } } @@ -73,21 +76,29 @@ fn install_logger(async: bool) { slog_stdlog::set_logger(slog::Logger::root(drain.ignore_err(), None)).unwrap(); } +#[derive(PartialEq, Eq)] +enum OpenMode { + ReadOnly, + ReadWrite, + Create +} + /// 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, read_only: bool) -> Result<(dir::Fd, rusqlite::Connection), Error> { +fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> { let dir = dir::Fd::open(db_dir)?; - dir.lock(if read_only { libc::LOCK_SH } else { libc::LOCK_EX } | libc::LOCK_NB) + let ro = mode == OpenMode::ReadOnly; + dir.lock(if ro { libc::LOCK_SH } else { libc::LOCK_EX } | libc::LOCK_NB) .map_err(|e| Error{description: format!("db dir {:?} already in use; can't get {} lock", db_dir, - if read_only { "shared" } else { "exclusive" }), + if ro { "shared" } else { "exclusive" }), cause: Some(Box::new(e))})?; let conn = rusqlite::Connection::open_with_flags( Path::new(&db_dir).join("db"), - if read_only { - rusqlite::SQLITE_OPEN_READ_ONLY - } else { - rusqlite::SQLITE_OPEN_READ_WRITE + match mode { + OpenMode::ReadOnly => rusqlite::SQLITE_OPEN_READ_ONLY, + OpenMode::ReadWrite => rusqlite::SQLITE_OPEN_READ_WRITE, + OpenMode::Create => rusqlite::SQLITE_OPEN_READ_WRITE | rusqlite::SQLITE_OPEN_CREATE, } | // rusqlite::Connection is not Sync, so there's no reason to tell SQLite3 to use the // serialized threading mode. diff --git a/src/cmds/run.rs b/src/cmds/run.rs index 0a8598f..1a94cb4 100644 --- a/src/cmds/run.rs +++ b/src/cmds/run.rs @@ -73,7 +73,9 @@ pub fn run() -> Result<(), Error> { // that signals will be blocked in all threads. let signal = chan_signal::notify(&[chan_signal::Signal::INT, chan_signal::Signal::TERM]); super::install_logger(true); - let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, args.flag_read_only)?; + 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(conn).unwrap()); let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone()).unwrap(); info!("Database is loaded."); diff --git a/src/cmds/upgrade/mod.rs b/src/cmds/upgrade/mod.rs index 501172a..0b751a4 100644 --- a/src/cmds/upgrade/mod.rs +++ b/src/cmds/upgrade/mod.rs @@ -87,7 +87,7 @@ fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), pub fn run() -> Result<(), Error> { let args: Args = super::parse_args(USAGE)?; super::install_logger(false); - let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, false)?; + let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?; { assert_eq!(UPGRADERS.len(), db::EXPECTED_VERSION as usize); diff --git a/src/db.rs b/src/db.rs index fddb3e2..e9609c4 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1105,6 +1105,20 @@ impl LockedDatabase { } } +/// Gets the schema version from the given database connection. +/// A fully initialized database will return `Ok(Some(version))` where `version` is an integer that +/// can be compared to `EXPECTED_VERSION`. An empty database will return `Ok(None)`. A partially +/// initialized database (in particular, one without a version row) will return some error. +pub fn get_schema_version(conn: &rusqlite::Connection) -> Result, Error> { + let ver_tables: i32 = conn.query_row_and_then( + "select count(*) from sqlite_master where name = 'version'", + &[], |row| row.get_checked(0))?; + if ver_tables == 0 { + return Ok(None); + } + Ok(Some(conn.query_row_and_then("select max(id) from version", &[], |row| row.get_checked(0))?)) +} + /// The recording database. Abstracts away SQLite queries. Also maintains in-memory state /// (loaded on startup, and updated on successful commit) to avoid expensive scans over the /// recording table on common queries. @@ -1136,21 +1150,14 @@ impl Database { recording.start_time_90k "#, recording::MAX_RECORDING_DURATION); { - use std::error::Error as E; - let ver: i32 = match conn.query_row("select max(id) from version", &[], - |row| row.get_checked::<_, i32>(0)) { - Ok(r) => r?, - Err(ref e) if e.description().starts_with("no such table: version") => { - return Err(Error::new("no such table: version. \ - \ - If you are starting from an \ - empty database, see README.md to complete the \ - installation. If you are starting from a database \ - that predates schema versioning, see guide/schema.md." - .to_owned())); - }, - Err(e) => return Err(e.into()), - }; + let ver = get_schema_version(&conn)?.ok_or_else(|| Error::new( + "no such table: version. \ + \ + If you are starting from an \ + empty database, see README.md to complete the \ + installation. If you are starting from a database \ + that predates schema versioning, see guide/schema.md." + .to_owned()))?; if ver < EXPECTED_VERSION { return Err(Error::new(format!( "Database schema version {} is too old (expected {}); \ diff --git a/src/main.rs b/src/main.rs index e417d8a..3ffeffa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,10 +90,11 @@ Options: --version Show the version of moonfire-nvr. Commands: - run Run the daemon: record from cameras and handle HTTP requests - upgrade Upgrade the database to the latest schema check Check database integrity + init Initialize a database + run Run the daemon: record from cameras and handle HTTP requests ts Translate between human-readable and numeric timestamps + upgrade Upgrade the database to the latest schema "; /// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate.