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.
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
## <a name="cameras"></a>Camera configuration and hard drive mounting

View File

@ -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}"

View File

@ -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?;

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;
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.

View File

@ -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.");

View File

@ -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);

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
/// (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 {}); \

View File

@ -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.