mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-13 16:03:22 -05:00
version the sqlite3 database schema
See guide/schema.md for instructions on upgrading past this commit.
This commit is contained in:
parent
eb4221851e
commit
86dd36d7a5
19
README.md
19
README.md
@ -148,15 +148,18 @@ Once prerequisites are installed, Moonfire NVR can be built as follows:
|
|||||||
Moonfire NVR should be run under a dedicated user. It keeps two kinds of
|
Moonfire NVR should be run under a dedicated user. It keeps two kinds of
|
||||||
state:
|
state:
|
||||||
|
|
||||||
* a SQLite database, typically <1 GiB. It should be stored on flash if
|
* a SQLite database, typically <1 GiB. It should be stored on flash if
|
||||||
available.
|
available.
|
||||||
* the "sample file directory", which holds the actual samples/frames of H.264
|
* the "sample file directory", which holds the actual samples/frames of
|
||||||
video. This should be quite large and typically is stored on a hard drive.
|
H.264 video. This should be quite large and typically is stored on a hard
|
||||||
|
drive.
|
||||||
|
|
||||||
Both are intended to be accessed only by Moonfire NVR itself. However, the
|
(See [guide/schema.md](guide/schema.md) for more information.)
|
||||||
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
|
Both kinds of state are intended to be accessed only by Moonfire NVR itself.
|
||||||
tool prior to starting Moonfire NVR.
|
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.
|
||||||
|
|
||||||
Manual commands would look something like this:
|
Manual commands would look something like this:
|
||||||
|
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
Status: **current**. This is largely implemented; there is optimization and
|
Status: **current**. This is largely implemented; there is optimization and
|
||||||
testing work left to do.
|
testing work left to do.
|
||||||
|
|
||||||
|
This is the initial design for the most fundamental parts of the Moonfire NVR
|
||||||
|
storage schema. See also [guide/schema.md](../guide/schema.md) for more
|
||||||
|
administrator-focused documentation.
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
|
|
||||||
Goals:
|
Goals:
|
||||||
@ -40,8 +44,6 @@ Possible future goals:
|
|||||||
* record audio and/or other types of timestamped samples (such as
|
* record audio and/or other types of timestamped samples (such as
|
||||||
[Xandem][xandem] tomography data).
|
[Xandem][xandem] tomography data).
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
### Cameras
|
### Cameras
|
||||||
|
|
||||||
Inexpensive modern ONVIF/PSIA IP security cameras, such as the $100
|
Inexpensive modern ONVIF/PSIA IP security cameras, such as the $100
|
||||||
|
79
guide/schema.md
Normal file
79
guide/schema.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# Moonfire NVR Schema Guide
|
||||||
|
|
||||||
|
This document has notes about the Moonfire NVR storage schema. As described in
|
||||||
|
[README.md](../README.md), this consists of two kinds of state:
|
||||||
|
|
||||||
|
* a SQLite database, typically <1 GiB. It should be stored on flash if
|
||||||
|
available.
|
||||||
|
* the "sample file directory", which holds the actual samples/frames of
|
||||||
|
H.264 video. This should be quite large and typically is stored on a hard
|
||||||
|
drive.
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
The database schema includes a version number to quickly identify if a
|
||||||
|
the database is compatible with a particular version of the software. Some
|
||||||
|
software upgrades will require you to upgrade the database.
|
||||||
|
|
||||||
|
### Unversioned to version 0
|
||||||
|
|
||||||
|
Early versions of Moonfire NVR did not include the version information in the
|
||||||
|
schema. You can manually add this information to your schema using the
|
||||||
|
`sqlite3` commandline. This process is backward compatible, meaning that
|
||||||
|
software versions that accept an unversioned database will also accept a
|
||||||
|
version 0 database.
|
||||||
|
|
||||||
|
Version 0 makes two changes:
|
||||||
|
|
||||||
|
* schema versioning, as described above.
|
||||||
|
* adding a column (`video_sync_samples`) to a database index to speed up
|
||||||
|
certain operations.
|
||||||
|
|
||||||
|
First ensure Moonfire NVR is not running; if you are using systemd with the
|
||||||
|
service name `moonfire-nvr`, you can do this as follows:
|
||||||
|
|
||||||
|
$ sudo systemctl stop moonfire-nvr
|
||||||
|
|
||||||
|
The service takes a moment to shut down; wait until the following command
|
||||||
|
reports that it is not running:
|
||||||
|
|
||||||
|
$ sudo systemctl status moonfire-nvr
|
||||||
|
|
||||||
|
Then use `sqlite3` to manually edit the database. The default path is
|
||||||
|
`/var/lib/moonfire-nvr/db/db`; if you've specified a different `--db_dir`,
|
||||||
|
use that directory with a suffix of `/db`.
|
||||||
|
|
||||||
|
$ sudo -u moonfire-nvr sqlite3 /var/lib/moonfire-nvr/db/db
|
||||||
|
sqlite3>
|
||||||
|
|
||||||
|
At the prompt, run the following commands:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
begin transaction;
|
||||||
|
|
||||||
|
create table version (
|
||||||
|
id integer primary key,
|
||||||
|
unix_time integer not null,
|
||||||
|
notes text
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into version values (0, cast(strftime('%s', 'now') as int),
|
||||||
|
'manual upgrade to version 0');
|
||||||
|
|
||||||
|
drop index recording_cover;
|
||||||
|
|
||||||
|
create index recording_cover on recording (
|
||||||
|
camera_id,
|
||||||
|
start_time_90k,
|
||||||
|
duration_90k,
|
||||||
|
video_samples,
|
||||||
|
video_sample_entry_id,
|
||||||
|
sample_file_bytes
|
||||||
|
);
|
||||||
|
|
||||||
|
commit transaction;
|
||||||
|
```
|
||||||
|
|
||||||
|
When you are done, you can restart the service:
|
||||||
|
|
||||||
|
$ sudo systemctl start moonfire-nvr
|
69
src/db.rs
69
src/db.rs
@ -69,6 +69,9 @@ use std::vec::Vec;
|
|||||||
use time;
|
use time;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Expected schema version. See `guide/schema.md` for more information.
|
||||||
|
pub const EXPECTED_VERSION: i32 = 0;
|
||||||
|
|
||||||
const GET_RECORDING_SQL: &'static str =
|
const GET_RECORDING_SQL: &'static str =
|
||||||
"select sample_file_uuid, video_index from recording where id = :id";
|
"select sample_file_uuid, video_index from recording where id = :id";
|
||||||
|
|
||||||
@ -414,6 +417,7 @@ fn init_recordings(conn: &mut rusqlite::Connection, camera_id: i32, camera: &mut
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct LockedDatabase {
|
pub struct LockedDatabase {
|
||||||
conn: rusqlite::Connection,
|
conn: rusqlite::Connection,
|
||||||
state: State,
|
state: State,
|
||||||
@ -422,6 +426,7 @@ pub struct LockedDatabase {
|
|||||||
/// In-memory state from the database.
|
/// In-memory state from the database.
|
||||||
/// This is separated out of `LockedDatabase` so that `Transaction` can mutably borrow `state`
|
/// This is separated out of `LockedDatabase` so that `Transaction` can mutably borrow `state`
|
||||||
/// while its underlying `rusqlite::Transaction` is borrowing `conn`.
|
/// while its underlying `rusqlite::Transaction` is borrowing `conn`.
|
||||||
|
#[derive(Debug)]
|
||||||
struct State {
|
struct State {
|
||||||
cameras_by_id: BTreeMap<i32, Camera>,
|
cameras_by_id: BTreeMap<i32, Camera>,
|
||||||
cameras_by_uuid: BTreeMap<Uuid, i32>,
|
cameras_by_uuid: BTreeMap<Uuid, i32>,
|
||||||
@ -959,6 +964,7 @@ impl LockedDatabase {
|
|||||||
/// 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.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Database(Mutex<LockedDatabase>);
|
pub struct Database(Mutex<LockedDatabase>);
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
@ -983,6 +989,36 @@ impl Database {
|
|||||||
order by
|
order by
|
||||||
recording.start_time_90k
|
recording.start_time_90k
|
||||||
"#, recording::MAX_RECORDING_DURATION);
|
"#, 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
|
||||||
|
complete the schema. If you are starting from a database \
|
||||||
|
that predates schema versioning, see guide/schema.md."
|
||||||
|
.to_owned()));
|
||||||
|
},
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
if ver < EXPECTED_VERSION {
|
||||||
|
return Err(Error::new(format!(
|
||||||
|
"Database schema version {} is too old (expected {}); \
|
||||||
|
see upgrade instructions in guide/upgrade.md.",
|
||||||
|
ver, EXPECTED_VERSION)));
|
||||||
|
} else if ver > EXPECTED_VERSION {
|
||||||
|
return Err(Error::new(format!(
|
||||||
|
"Database schema version {} is too new (expected {}); \
|
||||||
|
must use a newer binary to match.", ver,
|
||||||
|
EXPECTED_VERSION)));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
let db = Database(Mutex::new(LockedDatabase{
|
let db = Database(Mutex::new(LockedDatabase{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
state: State{
|
state: State{
|
||||||
@ -1024,6 +1060,7 @@ mod tests {
|
|||||||
use recording::{self, TIME_UNITS_PER_SEC};
|
use recording::{self, TIME_UNITS_PER_SEC};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::error::Error as E;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use testutil;
|
use testutil;
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -1203,9 +1240,37 @@ mod tests {
|
|||||||
assert_eq!(0, m.len());
|
assert_eq!(0, m.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Basic test of running some queries on an empty database.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_db() {
|
fn test_no_version() {
|
||||||
|
testutil::init();
|
||||||
|
let e = Database::new(Connection::open_in_memory().unwrap()).unwrap_err();
|
||||||
|
assert!(e.description().starts_with("no such table: version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version_too_old() {
|
||||||
|
testutil::init();
|
||||||
|
let c = setup_conn();
|
||||||
|
c.execute_batch("delete from version; insert into version values (-1, 0, '');").unwrap();
|
||||||
|
let e = Database::new(c).unwrap_err();
|
||||||
|
assert!(e.description().starts_with(
|
||||||
|
"Database schema version -1 is too old (expected 0)"), "got: {:?}",
|
||||||
|
e.description());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version_too_new() {
|
||||||
|
testutil::init();
|
||||||
|
let c = setup_conn();
|
||||||
|
c.execute_batch("delete from version; insert into version values (1, 0, '');").unwrap();
|
||||||
|
let e = Database::new(c).unwrap_err();
|
||||||
|
assert!(e.description().starts_with(
|
||||||
|
"Database schema version 1 is too new (expected 0)"), "got: {:?}", e.description());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Basic test of running some queries on a fresh database.
|
||||||
|
#[test]
|
||||||
|
fn test_fresh_db() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let conn = setup_conn();
|
let conn = setup_conn();
|
||||||
let db = Database::new(conn).unwrap();
|
let db = Database::new(conn).unwrap();
|
||||||
|
@ -33,6 +33,20 @@
|
|||||||
|
|
||||||
--pragma journal_mode = wal;
|
--pragma journal_mode = wal;
|
||||||
|
|
||||||
|
-- This table tracks the schema version.
|
||||||
|
-- There is one row for the initial database creation (inserted below, after the
|
||||||
|
-- create statements) and one for each upgrade procedure (if any).
|
||||||
|
create table version (
|
||||||
|
id integer primary key,
|
||||||
|
|
||||||
|
-- The unix time as of the creation/upgrade, as determined by
|
||||||
|
-- cast(strftime('%s', 'now') as int).
|
||||||
|
unix_time integer not null,
|
||||||
|
|
||||||
|
-- Optional notes on the creation/upgrade; could include the binary version.
|
||||||
|
notes text
|
||||||
|
);
|
||||||
|
|
||||||
create table camera (
|
create table camera (
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
uuid blob unique,-- not null check (length(uuid) = 16),
|
uuid blob unique,-- not null check (length(uuid) = 16),
|
||||||
@ -140,3 +154,6 @@ create table video_sample_entry (
|
|||||||
-- the case of H.264).
|
-- the case of H.264).
|
||||||
data blob not null check (length(data) > 86)
|
data blob not null check (length(data) > 86)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
insert into version (id, unix_time, notes)
|
||||||
|
values (0, cast(strftime('%s', 'now') as int), 'db creation');
|
||||||
|
Loading…
Reference in New Issue
Block a user