new command to initialize a database

This commit is contained in:
Scott Lamb 2017-01-16 14:21:08 -08:00
parent 3af9aeee96
commit 168cd743f4
9 changed files with 131 additions and 35 deletions

View File

@ -161,8 +161,8 @@ state:
Both kinds of state are intended to be accessed only by Moonfire NVR itself. 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 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` have to manually insert cameras with the `sqlite3` command line tool prior to
command line tool prior to starting Moonfire NVR. starting Moonfire NVR.
Manual commands would look something like this: 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 adduser --system moonfire-nvr --home /var/lib/moonfire-nvr
$ sudo mkdir /var/lib/moonfire-nvr $ sudo mkdir /var/lib/moonfire-nvr
$ sudo -u moonfire-nvr -H mkdir db sample $ 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
## <a name="cameras"></a>Camera configuration and hard drive mounting ## <a name="cameras"></a>Camera configuration and hard drive mounting

View File

@ -216,7 +216,7 @@ if [ ! -d "${DB_DIR}" ]; then
fi fi
DB_PATH="${DB_DIR}/${DB_NAME}" DB_PATH="${DB_DIR}/${DB_NAME}"
CAMERAS_PATH="${SRC_DIR}/../cameras.sql" 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 if [ -r "${CAMERAS_PATH}" ]; then
echo 'Add cameras...'; echo echo 'Add cameras...'; echo
sudo -u ${NVR_USER} -H sqlite3 "${DB_PATH}" < "${CAMERAS_PATH}" sudo -u ${NVR_USER} -H sqlite3 "${DB_PATH}" < "${CAMERAS_PATH}"

View File

@ -99,7 +99,7 @@ struct File {
pub fn run() -> Result<(), Error> { pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let args: Args = super::parse_args(USAGE)?;
super::install_logger(false); 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(); let mut files = Vec::new();
for e in fs::read_dir(&args.flag_sample_file_dir)? { for e in fs::read_dir(&args.flag_sample_file_dir)? {
let e = e?; let e = e?;

75
src/cmds/init.rs Normal file
View File

@ -0,0 +1,75 @@
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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(())
}

View File

@ -40,25 +40,28 @@ use slog_term;
use std::path::Path; use std::path::Path;
mod check; mod check;
mod init;
mod run; mod run;
mod ts; mod ts;
mod upgrade; mod upgrade;
#[derive(Debug, RustcDecodable)] #[derive(Debug, RustcDecodable)]
pub enum Command { pub enum Command {
Run,
Upgrade,
Check, Check,
Init,
Run,
Ts, Ts,
Upgrade,
} }
impl Command { impl Command {
pub fn run(&self) -> Result<(), Error> { pub fn run(&self) -> Result<(), Error> {
match *self { match *self {
Command::Run => run::run(),
Command::Upgrade => upgrade::run(),
Command::Check => check::run(), Command::Check => check::run(),
Command::Init => init::run(),
Command::Run => run::run(),
Command::Ts => ts::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(); 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. /// Locks and opens the database.
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is. /// 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)?; 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", .map_err(|e| Error{description: format!("db dir {:?} already in use; can't get {} lock",
db_dir, db_dir,
if read_only { "shared" } else { "exclusive" }), if ro { "shared" } else { "exclusive" }),
cause: Some(Box::new(e))})?; cause: Some(Box::new(e))})?;
let conn = rusqlite::Connection::open_with_flags( let conn = rusqlite::Connection::open_with_flags(
Path::new(&db_dir).join("db"), Path::new(&db_dir).join("db"),
if read_only { match mode {
rusqlite::SQLITE_OPEN_READ_ONLY OpenMode::ReadOnly => rusqlite::SQLITE_OPEN_READ_ONLY,
} else { OpenMode::ReadWrite => rusqlite::SQLITE_OPEN_READ_WRITE,
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 // rusqlite::Connection is not Sync, so there's no reason to tell SQLite3 to use the
// serialized threading mode. // serialized threading mode.

View File

@ -73,7 +73,9 @@ pub fn run() -> Result<(), Error> {
// that signals will be blocked in all threads. // that signals will be blocked in all threads.
let signal = chan_signal::notify(&[chan_signal::Signal::INT, chan_signal::Signal::TERM]); let signal = chan_signal::notify(&[chan_signal::Signal::INT, chan_signal::Signal::TERM]);
super::install_logger(true); 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 db = Arc::new(db::Database::new(conn).unwrap());
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone()).unwrap(); let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone()).unwrap();
info!("Database is loaded."); info!("Database is loaded.");

View File

@ -87,7 +87,7 @@ fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(),
pub fn run() -> Result<(), Error> { pub fn run() -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let args: Args = super::parse_args(USAGE)?;
super::install_logger(false); 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); assert_eq!(UPGRADERS.len(), db::EXPECTED_VERSION as usize);

View File

@ -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<Option<i32>, 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 /// 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 /// (loaded on startup, and updated on successful commit) to avoid expensive scans over the
/// recording table on common queries. /// recording table on common queries.
@ -1136,21 +1150,14 @@ impl Database {
recording.start_time_90k recording.start_time_90k
"#, recording::MAX_RECORDING_DURATION); "#, recording::MAX_RECORDING_DURATION);
{ {
use std::error::Error as E; let ver = get_schema_version(&conn)?.ok_or_else(|| Error::new(
let ver: i32 = match conn.query_row("select max(id) from version", &[], "no such table: 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 \ If you are starting from an \
empty database, see README.md to complete the \ empty database, see README.md to complete the \
installation. If you are starting from a database \ installation. If you are starting from a database \
that predates schema versioning, see guide/schema.md." that predates schema versioning, see guide/schema.md."
.to_owned())); .to_owned()))?;
},
Err(e) => return Err(e.into()),
};
if ver < EXPECTED_VERSION { if ver < EXPECTED_VERSION {
return Err(Error::new(format!( return Err(Error::new(format!(
"Database schema version {} is too old (expected {}); \ "Database schema version {} is too old (expected {}); \

View File

@ -90,10 +90,11 @@ Options:
--version Show the version of moonfire-nvr. --version Show the version of moonfire-nvr.
Commands: Commands:
run Run the daemon: record from cameras and handle HTTP requests
upgrade Upgrade the database to the latest schema
check Check database integrity 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 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. /// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate.