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