mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-12-04 06:35:58 -05:00
update "moonfire-nvr check" for new schema
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2018 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
|
||||
@@ -30,10 +30,8 @@
|
||||
|
||||
//! Subcommand to check the database and sample file dir for errors.
|
||||
|
||||
use db::{self, recording};
|
||||
use db::check;
|
||||
use failure::Error;
|
||||
use std::fs;
|
||||
use uuid::Uuid;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Checks database integrity.
|
||||
@@ -48,157 +46,21 @@ 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]
|
||||
--compare-lens Compare sample file lengths on disk to the database.
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
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 += it.is_key() as i32;
|
||||
}
|
||||
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>,
|
||||
flag_compare_lens: bool,
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let args: Args = super::parse_args(USAGE)?;
|
||||
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?;
|
||||
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) => bail!("outer join returned fully empty row"),
|
||||
};
|
||||
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(())
|
||||
// TODO: ReadOnly should be sufficient but seems to fail.
|
||||
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
|
||||
check::run(&conn, &check::Options {
|
||||
compare_lens: args.flag_compare_lens,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user