mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-27 20:58:56 -05:00
use xsv-style subcommands like "moonfire-nvr run"
This makes it easier to understand which options are valid with each command. Additionally, there's more separation of implementations. The most obvious consequence is that "moonfire-nvr ts ..." no longer uselessly locks/opens a database.
This commit is contained in:
208
src/cmds/check.rs
Normal file
208
src/cmds/check.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
// 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/>.
|
||||
|
||||
//! Subcommand to check the database and sample file dir for errors.
|
||||
|
||||
use db;
|
||||
use error::Error;
|
||||
use recording;
|
||||
use std::fs;
|
||||
use uuid::Uuid;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Checks database integrity.
|
||||
|
||||
Usage:
|
||||
|
||||
moonfire-nvr check [options]
|
||||
moonfire-nvr check --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]
|
||||
--sample-file-dir=DIR Set the directory holding video data.
|
||||
This is typically on a hard drive.
|
||||
[default: /var/lib/moonfire-nvr/sample]
|
||||
"#;
|
||||
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_sample_file_dir: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct RecordingSummary {
|
||||
bytes: u64,
|
||||
video_samples: i32,
|
||||
video_sync_samples: i32,
|
||||
duration: i32,
|
||||
flags: i32,
|
||||
}
|
||||
|
||||
fn summarize_index(video_index: &[u8]) -> Result<RecordingSummary, Error> {
|
||||
let mut it = recording::SampleIndexIterator::new();
|
||||
let mut duration = 0;
|
||||
let mut video_samples = 0;
|
||||
let mut video_sync_samples = 0;
|
||||
let mut bytes = 0;
|
||||
while it.next(video_index)? {
|
||||
bytes += it.bytes as u64;
|
||||
duration += it.duration_90k;
|
||||
video_samples += 1;
|
||||
video_sync_samples += if it.is_key { 1 } else { 0 };
|
||||
}
|
||||
Ok(RecordingSummary{
|
||||
bytes: bytes,
|
||||
video_samples: video_samples,
|
||||
video_sync_samples: video_sync_samples,
|
||||
duration: duration,
|
||||
flags: if it.duration_90k == 0 { db::RecordingFlags::TrailingZero as i32 } else { 0 },
|
||||
})
|
||||
}
|
||||
|
||||
struct File {
|
||||
uuid: Uuid,
|
||||
len: u64,
|
||||
composite_id: Option<i64>,
|
||||
}
|
||||
|
||||
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 mut files = Vec::new();
|
||||
for e in fs::read_dir(&args.flag_sample_file_dir)? {
|
||||
let e = e?;
|
||||
let uuid = match e.file_name().to_str().and_then(|f| Uuid::parse_str(f).ok()) {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
error!("sample file directory contains file {} which isn't a uuid",
|
||||
e.file_name().to_string_lossy());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let len = e.metadata()?.len();
|
||||
files.push(File{uuid: uuid, len: len, composite_id: None});
|
||||
}
|
||||
files.sort_by(|a, b| a.uuid.cmp(&b.uuid));
|
||||
|
||||
// This statement should be a full outer join over the recording and recording_playback tables.
|
||||
// SQLite3 doesn't support that, though, so emulate it with a couple left joins and a union.
|
||||
const FIELDS: &'static str = r#"
|
||||
recording.composite_id,
|
||||
recording.flags,
|
||||
recording.sample_file_bytes,
|
||||
recording.duration_90k,
|
||||
recording.video_samples,
|
||||
recording.video_sync_samples,
|
||||
recording_playback.composite_id,
|
||||
recording_playback.sample_file_uuid,
|
||||
recording_playback.video_index
|
||||
"#;
|
||||
let mut stmt = conn.prepare(&format!(r#"
|
||||
select {}
|
||||
from recording left join recording_playback on
|
||||
(recording.composite_id = recording_playback.composite_id)
|
||||
union all
|
||||
select {}
|
||||
from recording_playback left join recording on
|
||||
(recording_playback.composite_id = recording.composite_id)
|
||||
where recording.composite_id is null
|
||||
"#, FIELDS, FIELDS))?;
|
||||
let mut rows = stmt.query(&[])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let composite_id: Option<i64> = row.get_checked(0)?;
|
||||
let playback_composite_id: Option<i64> = row.get_checked(6)?;
|
||||
let composite_id = match (composite_id, playback_composite_id) {
|
||||
(Some(id1), Some(_)) => id1,
|
||||
(Some(id1), None) => {
|
||||
error!("composite id {} has recording row but no recording_playback row", id1);
|
||||
continue;
|
||||
},
|
||||
(None, Some(id2)) => {
|
||||
error!("composite id {} has recording_playback row but no recording row", id2);
|
||||
continue;
|
||||
},
|
||||
(None, None) => {
|
||||
return Err(Error::new("outer join returned fully empty row".to_owned()));
|
||||
},
|
||||
};
|
||||
let row_summary = RecordingSummary{
|
||||
flags: row.get_checked(1)?,
|
||||
bytes: row.get_checked::<_, i64>(2)? as u64,
|
||||
duration: row.get_checked(3)?,
|
||||
video_samples: row.get_checked(4)?,
|
||||
video_sync_samples: row.get_checked(5)?,
|
||||
};
|
||||
let sample_file_uuid = Uuid::from_bytes(&row.get_checked::<_, Vec<u8>>(7)?)?;
|
||||
let video_index: Vec<u8> = row.get_checked(8)?;
|
||||
let index_summary = match summarize_index(&video_index) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("composite id {} has bad video_index: {}", composite_id, e);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
if row_summary != index_summary {
|
||||
error!("composite id {} row summary {:#?} inconsistent with index {:#?}",
|
||||
composite_id, row_summary, index_summary);
|
||||
}
|
||||
let f = match files.binary_search_by(|f| f.uuid.cmp(&sample_file_uuid)) {
|
||||
Ok(i) => &mut files[i],
|
||||
Err(_) => {
|
||||
error!("composite id {} refers to missing sample file {}",
|
||||
composite_id, sample_file_uuid);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(id) = f.composite_id {
|
||||
error!("composite id {} refers to sample file {} already used by id {}",
|
||||
composite_id, sample_file_uuid, id);
|
||||
} else {
|
||||
f.composite_id = Some(composite_id);
|
||||
}
|
||||
if row_summary.bytes != f.len {
|
||||
error!("composite id {} declares length {}, but its sample file {} has length {}",
|
||||
composite_id, row_summary.bytes, sample_file_uuid, f.len);
|
||||
}
|
||||
}
|
||||
|
||||
for f in files {
|
||||
if f.composite_id.is_none() {
|
||||
error!("sample file {} not used by any recording", f.uuid);
|
||||
}
|
||||
}
|
||||
info!("Check done.");
|
||||
Ok(())
|
||||
}
|
||||
102
src/cmds/mod.rs
Normal file
102
src/cmds/mod.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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 dir;
|
||||
use docopt;
|
||||
use error::Error;
|
||||
use libc;
|
||||
use rusqlite;
|
||||
use slog::{self, DrainExt};
|
||||
use slog_envlogger;
|
||||
use slog_stdlog;
|
||||
use slog_term;
|
||||
use std::path::Path;
|
||||
|
||||
mod check;
|
||||
mod run;
|
||||
mod ts;
|
||||
mod upgrade;
|
||||
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
pub enum Command {
|
||||
Run,
|
||||
Upgrade,
|
||||
Check,
|
||||
Ts,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
match *self {
|
||||
Command::Run => run::run(),
|
||||
Command::Upgrade => upgrade::run(),
|
||||
Command::Check => check::run(),
|
||||
Command::Ts => ts::run(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes logging.
|
||||
/// `async` should be true only for serving; otherwise logging can block useful work.
|
||||
/// Sync logging should be preferred for other modes because async apparently is never flushed
|
||||
/// before the program exits, and partial output from these tools is very confusing.
|
||||
fn install_logger(async: bool) {
|
||||
let drain = slog_term::StreamerBuilder::new();
|
||||
let drain = slog_envlogger::new(if async { drain.async() } else { drain }.full().build());
|
||||
slog_stdlog::set_logger(slog::Logger::root(drain.ignore_err(), None)).unwrap();
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let dir = dir::Fd::open(db_dir)?;
|
||||
dir.lock(if read_only { 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" }),
|
||||
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
|
||||
} |
|
||||
// rusqlite::Connection is not Sync, so there's no reason to tell SQLite3 to use the
|
||||
// serialized threading mode.
|
||||
rusqlite::SQLITE_OPEN_NO_MUTEX)?;
|
||||
Ok((dir, conn))
|
||||
}
|
||||
|
||||
fn parse_args<T>(usage: &str) -> Result<T, Error> where T: ::rustc_serialize::Decodable {
|
||||
Ok(docopt::Docopt::new(usage)
|
||||
.and_then(|d| d.decode())
|
||||
.unwrap_or_else(|e| e.exit()))
|
||||
}
|
||||
129
src/cmds/run.rs
Normal file
129
src/cmds/run.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
// 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 chan_signal;
|
||||
use clock;
|
||||
use db;
|
||||
use dir;
|
||||
use error::Error;
|
||||
use hyper::server::Server;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use stream;
|
||||
use streamer;
|
||||
use web;
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Usage: moonfire-nvr run [options]
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--sample-file-dir=DIR Set the directory holding video data.
|
||||
This is typically on a hard drive.
|
||||
[default: /var/lib/moonfire-nvr/sample]
|
||||
--http-addr=ADDR Set the bind address for the unencrypted HTTP server.
|
||||
[default: 0.0.0.0:8080]
|
||||
--read-only Forces read-only mode / disables recording.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_sample_file_dir: String,
|
||||
flag_http_addr: String,
|
||||
flag_read_only: bool,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
|
||||
// Watch for termination signals.
|
||||
// This must be started before any threads are spawned (such as the async logger thread) so
|
||||
// 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 = Arc::new(db::Database::new(conn).unwrap());
|
||||
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone()).unwrap();
|
||||
info!("Database is loaded.");
|
||||
|
||||
// Start a streamer for each camera.
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let mut streamers = Vec::new();
|
||||
let syncer = if !args.flag_read_only {
|
||||
let (syncer_channel, syncer_join) = dir::start_syncer(dir.clone()).unwrap();
|
||||
let l = db.lock();
|
||||
let cameras = l.cameras_by_id().len();
|
||||
let env = streamer::Environment{
|
||||
db: &db,
|
||||
dir: &dir,
|
||||
clocks: &clock::REAL,
|
||||
opener: &*stream::FFMPEG,
|
||||
shutdown: &shutdown,
|
||||
};
|
||||
for (i, (id, camera)) in l.cameras_by_id().iter().enumerate() {
|
||||
let rotate_offset_sec = streamer::ROTATE_INTERVAL_SEC * i as i64 / cameras as i64;
|
||||
let mut streamer = streamer::Streamer::new(&env, syncer_channel.clone(), *id, camera,
|
||||
rotate_offset_sec,
|
||||
streamer::ROTATE_INTERVAL_SEC);
|
||||
let name = format!("stream-{}", streamer.short_name());
|
||||
streamers.push(thread::Builder::new().name(name).spawn(move|| {
|
||||
streamer.run();
|
||||
}).expect("can't create thread"));
|
||||
}
|
||||
Some((syncer_channel, syncer_join))
|
||||
} else { None };
|
||||
|
||||
// Start the web interface.
|
||||
let server = Server::http(args.flag_http_addr.as_str()).unwrap();
|
||||
let h = web::Handler::new(db.clone(), dir.clone());
|
||||
let _guard = server.handle(h);
|
||||
info!("Ready to serve HTTP requests");
|
||||
|
||||
// Wait for a signal and shut down.
|
||||
chan_select! {
|
||||
signal.recv() -> signal => info!("Received signal {:?}; shutting down streamers.", signal),
|
||||
}
|
||||
shutdown.store(true, Ordering::SeqCst);
|
||||
for streamer in streamers.drain(..) {
|
||||
streamer.join().unwrap();
|
||||
}
|
||||
if let Some((syncer_channel, syncer_join)) = syncer {
|
||||
info!("Shutting down syncer.");
|
||||
drop(syncer_channel);
|
||||
syncer_join.join().unwrap();
|
||||
}
|
||||
info!("Exiting.");
|
||||
::std::process::exit(0);
|
||||
}
|
||||
52
src/cmds/ts.rs
Normal file
52
src/cmds/ts.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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 error::Error;
|
||||
use recording;
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Usage: moonfire-nvr ts <ts>...
|
||||
moonfire-nvr ts --help
|
||||
"#;
|
||||
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
struct Args {
|
||||
arg_ts: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let arg: Args = super::parse_args(&USAGE)?;
|
||||
super::install_logger(false);
|
||||
for timestamp in &arg.arg_ts {
|
||||
let t = recording::Time::parse(timestamp)?;
|
||||
println!("{} == {}", t, t.0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
128
src/cmds/upgrade/mod.rs
Normal file
128
src/cmds/upgrade/mod.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
// 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/>.
|
||||
|
||||
/// Upgrades the database schema.
|
||||
///
|
||||
/// See `guide/schema.md` for more information.
|
||||
|
||||
use db;
|
||||
use error::Error;
|
||||
use rusqlite;
|
||||
|
||||
mod v0_to_v1;
|
||||
|
||||
const USAGE: &'static str = r#"
|
||||
Upgrade to the latest database schema.
|
||||
|
||||
Usage: moonfire-nvr upgrade [options]
|
||||
|
||||
Options:
|
||||
-h, --help Show this message.
|
||||
--db-dir=DIR Set the directory holding the SQLite3 index database.
|
||||
This is typically on a flash device.
|
||||
[default: /var/lib/moonfire-nvr/db]
|
||||
--sample-file-dir=DIR Set the directory holding video data.
|
||||
This is typically on a hard drive.
|
||||
[default: /var/lib/moonfire-nvr/sample]
|
||||
--preset-journal=MODE Resets the SQLite journal_mode to the specified mode
|
||||
prior to 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 reset
|
||||
to wal after the upgrade.
|
||||
[default: delete]
|
||||
--no-vacuum Skips the normal post-upgrade vacuum operation.
|
||||
"#;
|
||||
|
||||
const UPGRADE_NOTES: &'static str =
|
||||
concat!("upgraded using moonfire-nvr ", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
const UPGRADERS: [fn(&rusqlite::Transaction) -> Result<(), Error>; 1] = [
|
||||
v0_to_v1::run,
|
||||
];
|
||||
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_sample_file_dir: String,
|
||||
flag_preset_journal: String,
|
||||
flag_no_vacuum: bool,
|
||||
}
|
||||
|
||||
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
|
||||
assert!(!requested.contains(';')); // quick check for accidental sql injection.
|
||||
let actual = conn.query_row(&format!("pragma journal_mode = {}", requested), &[],
|
||||
|row| row.get_checked::<_, String>(0))??;
|
||||
info!("...database now in journal_mode {} (requested {}).", actual, requested);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
{
|
||||
assert_eq!(UPGRADERS.len(), db::EXPECTED_VERSION as usize);
|
||||
let old_ver =
|
||||
conn.query_row("select max(id) from version", &[], |row| row.get_checked(0))??;
|
||||
if old_ver > db::EXPECTED_VERSION {
|
||||
return Err(Error::new(format!("Database is at version {}, later than expected {}",
|
||||
old_ver, db::EXPECTED_VERSION)))?;
|
||||
} else if old_ver < 0 {
|
||||
return Err(Error::new(format!("Database is at negative version {}!", old_ver)));
|
||||
}
|
||||
info!("Upgrading database from version {} to version {}...", old_ver, db::EXPECTED_VERSION);
|
||||
set_journal_mode(&conn, &args.flag_preset_journal).unwrap();
|
||||
for ver in old_ver .. db::EXPECTED_VERSION {
|
||||
info!("...from version {} to version {}", ver, ver + 1);
|
||||
let tx = conn.transaction()?;
|
||||
UPGRADERS[ver as usize](&tx)?;
|
||||
tx.execute(r#"
|
||||
insert into version (id, unix_time, notes)
|
||||
values (?, cast(strftime('%s', 'now') as int32), ?)
|
||||
"#, &[&(ver + 1), &UPGRADE_NOTES])?;
|
||||
tx.commit()?;
|
||||
}
|
||||
}
|
||||
|
||||
// WAL is the preferred journal mode for normal operation; it reduces the number of syncs
|
||||
// without compromising safety.
|
||||
set_journal_mode(&conn, "wal").unwrap();
|
||||
if !args.flag_no_vacuum {
|
||||
info!("...vacuuming database after upgrade.");
|
||||
conn.execute_batch(r#"
|
||||
pragma page_size = 16384;
|
||||
vacuum;
|
||||
"#).unwrap();
|
||||
}
|
||||
info!("...done.");
|
||||
Ok(())
|
||||
}
|
||||
245
src/cmds/upgrade/v0_to_v1.rs
Normal file
245
src/cmds/upgrade/v0_to_v1.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
// 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/>.
|
||||
|
||||
/// Upgrades a version 0 schema to a version 1 schema.
|
||||
|
||||
use db;
|
||||
use error::Error;
|
||||
use recording;
|
||||
use rusqlite;
|
||||
use std::collections::HashMap;
|
||||
use strutil;
|
||||
|
||||
pub fn run(tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||
// These create statements match the schema.sql when version 1 was the latest.
|
||||
tx.execute_batch(r#"
|
||||
alter table camera rename to old_camera;
|
||||
create table camera (
|
||||
id integer primary key,
|
||||
uuid blob unique,
|
||||
short_name text not null,
|
||||
description text,
|
||||
host text,
|
||||
username text,
|
||||
password text,
|
||||
main_rtsp_path text,
|
||||
sub_rtsp_path text,
|
||||
retain_bytes integer not null check (retain_bytes >= 0),
|
||||
next_recording_id integer not null check (next_recording_id >= 0)
|
||||
);
|
||||
alter table recording rename to old_recording;
|
||||
drop index recording_cover;
|
||||
create table recording (
|
||||
composite_id integer primary key,
|
||||
camera_id integer not null references camera (id),
|
||||
run_offset integer not null,
|
||||
flags integer not null,
|
||||
sample_file_bytes integer not null check (sample_file_bytes > 0),
|
||||
start_time_90k integer not null check (start_time_90k > 0),
|
||||
duration_90k integer not null
|
||||
check (duration_90k >= 0 and duration_90k < 5*60*90000),
|
||||
local_time_delta_90k integer not null,
|
||||
video_samples integer not null check (video_samples > 0),
|
||||
video_sync_samples integer not null check (video_samples > 0),
|
||||
video_sample_entry_id integer references video_sample_entry (id),
|
||||
check (composite_id >> 32 = camera_id)
|
||||
);
|
||||
create index recording_cover on recording (
|
||||
start_time_90k,
|
||||
duration_90k,
|
||||
video_samples,
|
||||
video_sample_entry_id,
|
||||
sample_file_bytes
|
||||
);
|
||||
create table recording_playback (
|
||||
composite_id integer primary key references recording (composite_id),
|
||||
sample_file_uuid blob not null check (length(sample_file_uuid) = 16),
|
||||
sample_file_sha1 blob not null check (length(sample_file_sha1) = 20),
|
||||
video_index blob not null check (length(video_index) > 0)
|
||||
);
|
||||
"#)?;
|
||||
let camera_state = fill_recording(tx).unwrap();
|
||||
fill_camera(tx, camera_state).unwrap();
|
||||
tx.execute_batch(r#"
|
||||
drop table old_camera;
|
||||
drop table old_recording;
|
||||
"#)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CameraState {
|
||||
/// tuple of (run_start_id, next_start_90k).
|
||||
current_run: Option<(i64, i64)>,
|
||||
|
||||
/// As in the `next_recording_id` field of the `camera` table.
|
||||
next_recording_id: i32,
|
||||
}
|
||||
|
||||
fn has_trailing_zero(video_index: &[u8]) -> Result<bool, Error> {
|
||||
let mut it = recording::SampleIndexIterator::new();
|
||||
while it.next(video_index)? {}
|
||||
Ok(it.duration_90k == 0)
|
||||
}
|
||||
|
||||
/// Fills the `recording` and `recording_playback` tables from `old_recording`, returning
|
||||
/// the `camera_state` map for use by a following call to `fill_cameras`.
|
||||
fn fill_recording(tx: &rusqlite::Transaction) -> Result<HashMap<i32, CameraState>, Error> {
|
||||
let mut select = tx.prepare(r#"
|
||||
select
|
||||
camera_id,
|
||||
sample_file_bytes,
|
||||
start_time_90k,
|
||||
duration_90k,
|
||||
local_time_delta_90k,
|
||||
video_samples,
|
||||
video_sync_samples,
|
||||
video_sample_entry_id,
|
||||
sample_file_uuid,
|
||||
sample_file_sha1,
|
||||
video_index,
|
||||
id
|
||||
from
|
||||
old_recording
|
||||
"#)?;
|
||||
let mut insert1 = tx.prepare(r#"
|
||||
insert into recording values (:composite_id, :camera_id, :run_offset, :flags,
|
||||
:sample_file_bytes, :start_time_90k, :duration_90k,
|
||||
:local_time_delta_90k, :video_samples, :video_sync_samples,
|
||||
:video_sample_entry_id)
|
||||
"#)?;
|
||||
let mut insert2 = tx.prepare(r#"
|
||||
insert into recording_playback values (:composite_id, :sample_file_uuid, :sample_file_sha1,
|
||||
:video_index)
|
||||
"#)?;
|
||||
let mut rows = select.query(&[])?;
|
||||
let mut camera_state: HashMap<i32, CameraState> = HashMap::new();
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let camera_id: i32 = row.get_checked(0)?;
|
||||
let camera_state = camera_state.entry(camera_id).or_insert_with(|| {
|
||||
CameraState{
|
||||
current_run: None,
|
||||
next_recording_id: 1,
|
||||
}
|
||||
});
|
||||
let composite_id = ((camera_id as i64) << 32) | (camera_state.next_recording_id as i64);
|
||||
camera_state.next_recording_id += 1;
|
||||
let sample_file_bytes: i32 = row.get_checked(1)?;
|
||||
let start_time_90k: i64 = row.get_checked(2)?;
|
||||
let duration_90k: i32 = row.get_checked(3)?;
|
||||
let local_time_delta_90k: i64 = row.get_checked(4)?;
|
||||
let video_samples: i32 = row.get_checked(5)?;
|
||||
let video_sync_samples: i32 = row.get_checked(6)?;
|
||||
let video_sample_entry_id: i32 = row.get_checked(7)?;
|
||||
let sample_file_uuid: Vec<u8> = row.get_checked(8)?;
|
||||
let sample_file_sha1: Vec<u8> = row.get_checked(9)?;
|
||||
let video_index: Vec<u8> = row.get_checked(10)?;
|
||||
let old_id: i32 = row.get_checked(11)?;
|
||||
let trailing_zero = has_trailing_zero(&video_index).unwrap_or_else(|e| {
|
||||
warn!("recording {}/{} (sample file {}, formerly recording {}) has corrupt \
|
||||
video_index: {}",
|
||||
camera_id, composite_id & 0xFFFF, strutil::hex(&sample_file_uuid), old_id, e);
|
||||
false
|
||||
});
|
||||
let run_id = match camera_state.current_run {
|
||||
Some((run_id, expected_start)) if expected_start == start_time_90k => run_id,
|
||||
_ => composite_id,
|
||||
};
|
||||
insert1.execute_named(&[
|
||||
(":composite_id", &composite_id),
|
||||
(":camera_id", &camera_id),
|
||||
(":run_offset", &(composite_id - run_id)),
|
||||
(":flags", &(if trailing_zero { db::RecordingFlags::TrailingZero as i32 } else { 0 })),
|
||||
(":sample_file_bytes", &sample_file_bytes),
|
||||
(":start_time_90k", &start_time_90k),
|
||||
(":duration_90k", &duration_90k),
|
||||
(":local_time_delta_90k", &local_time_delta_90k),
|
||||
(":video_samples", &video_samples),
|
||||
(":video_sync_samples", &video_sync_samples),
|
||||
(":video_sample_entry_id", &video_sample_entry_id),
|
||||
])?;
|
||||
insert2.execute_named(&[
|
||||
(":composite_id", &composite_id),
|
||||
(":sample_file_uuid", &sample_file_uuid),
|
||||
(":sample_file_sha1", &sample_file_sha1),
|
||||
(":video_index", &video_index),
|
||||
])?;
|
||||
camera_state.current_run = if trailing_zero {
|
||||
None
|
||||
} else {
|
||||
Some((run_id, start_time_90k + duration_90k as i64))
|
||||
};
|
||||
}
|
||||
Ok(camera_state)
|
||||
}
|
||||
|
||||
fn fill_camera(tx: &rusqlite::Transaction, camera_state: HashMap<i32, CameraState>)
|
||||
-> Result<(), Error> {
|
||||
let mut select = tx.prepare(r#"
|
||||
select
|
||||
id, uuid, short_name, description, host, username, password, main_rtsp_path,
|
||||
sub_rtsp_path, retain_bytes
|
||||
from
|
||||
old_camera
|
||||
"#)?;
|
||||
let mut insert = tx.prepare(r#"
|
||||
insert into camera values (:id, :uuid, :short_name, :description, :host, :username, :password,
|
||||
:main_rtsp_path, :sub_rtsp_path, :retain_bytes, :next_recording_id)
|
||||
"#)?;
|
||||
let mut rows = select.query(&[])?;
|
||||
while let Some(row) = rows.next() {
|
||||
let row = row?;
|
||||
let id: i32 = row.get_checked(0)?;
|
||||
let uuid: Vec<u8> = row.get_checked(1)?;
|
||||
let short_name: String = row.get_checked(2)?;
|
||||
let description: String = row.get_checked(3)?;
|
||||
let host: String = row.get_checked(4)?;
|
||||
let username: String = row.get_checked(5)?;
|
||||
let password: String = row.get_checked(6)?;
|
||||
let main_rtsp_path: String = row.get_checked(7)?;
|
||||
let sub_rtsp_path: String = row.get_checked(8)?;
|
||||
let retain_bytes: i64 = row.get_checked(9)?;
|
||||
insert.execute_named(&[
|
||||
(":id", &id),
|
||||
(":uuid", &uuid),
|
||||
(":short_name", &short_name),
|
||||
(":description", &description),
|
||||
(":host", &host),
|
||||
(":username", &username),
|
||||
(":password", &password),
|
||||
(":main_rtsp_path", &main_rtsp_path),
|
||||
(":sub_rtsp_path", &sub_rtsp_path),
|
||||
(":retain_bytes", &retain_bytes),
|
||||
(":next_recording_id",
|
||||
&camera_state.get(&id).map(|s| s.next_recording_id).unwrap_or(1)),
|
||||
])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user