mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-03-31 01:33:42 -04:00
Use fixed-size directory meta files
Add a new schema version 5; now 4 means the directory meta may or may not be upgraded. Fixes #65: now it's possible to open the directory even if it lies on a completely full disk.
This commit is contained in:
parent
13b192949d
commit
d61b5e1bdd
@ -183,7 +183,7 @@ fn read_dir(path: &str, opts: &Options) -> Result<Dir, Error> {
|
|||||||
let f = e.file_name();
|
let f = e.file_name();
|
||||||
let f = f.as_bytes();
|
let f = f.as_bytes();
|
||||||
match f {
|
match f {
|
||||||
b"meta" | b"meta-tmp" => continue,
|
b"meta" => continue,
|
||||||
_ => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
let id = match dir::parse_id(f) {
|
let id = match dir::parse_id(f) {
|
||||||
|
10
db/db.rs
10
db/db.rs
@ -83,7 +83,7 @@ use time;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Expected schema version. See `guide/schema.md` for more information.
|
/// Expected schema version. See `guide/schema.md` for more information.
|
||||||
pub const EXPECTED_VERSION: i32 = 4;
|
pub const EXPECTED_VERSION: i32 = 5;
|
||||||
|
|
||||||
const GET_RECORDING_PLAYBACK_SQL: &'static str = r#"
|
const GET_RECORDING_PLAYBACK_SQL: &'static str = r#"
|
||||||
select
|
select
|
||||||
@ -2189,20 +2189,20 @@ mod tests {
|
|||||||
fn test_version_too_old() {
|
fn test_version_too_old() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let c = setup_conn();
|
let c = setup_conn();
|
||||||
c.execute_batch("delete from version; insert into version values (3, 0, '');").unwrap();
|
c.execute_batch("delete from version; insert into version values (4, 0, '');").unwrap();
|
||||||
let e = Database::new(clock::RealClocks {}, c, false).err().unwrap();
|
let e = Database::new(clock::RealClocks {}, c, false).err().unwrap();
|
||||||
assert!(e.to_string().starts_with(
|
assert!(e.to_string().starts_with(
|
||||||
"Database schema version 3 is too old (expected 4)"), "got: {:?}", e);
|
"Database schema version 4 is too old (expected 5)"), "got: {:?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_version_too_new() {
|
fn test_version_too_new() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let c = setup_conn();
|
let c = setup_conn();
|
||||||
c.execute_batch("delete from version; insert into version values (5, 0, '');").unwrap();
|
c.execute_batch("delete from version; insert into version values (6, 0, '');").unwrap();
|
||||||
let e = Database::new(clock::RealClocks {}, c, false).err().unwrap();
|
let e = Database::new(clock::RealClocks {}, c, false).err().unwrap();
|
||||||
assert!(e.to_string().starts_with(
|
assert!(e.to_string().starts_with(
|
||||||
"Database schema version 5 is too new (expected 4)"), "got: {:?}", e);
|
"Database schema version 6 is too new (expected 5)"), "got: {:?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Basic test of running some queries on a fresh database.
|
/// Basic test of running some queries on a fresh database.
|
||||||
|
76
db/dir.rs
76
db/dir.rs
@ -32,6 +32,7 @@
|
|||||||
//!
|
//!
|
||||||
//! This includes opening files for serving, rotating away old files, and saving new files.
|
//! This includes opening files for serving, rotating away old files, and saving new files.
|
||||||
|
|
||||||
|
use crate::coding;
|
||||||
use crate::db::CompositeId;
|
use crate::db::CompositeId;
|
||||||
use cstr::*;
|
use cstr::*;
|
||||||
use failure::{Error, Fail, bail, format_err};
|
use failure::{Error, Fail, bail, format_err};
|
||||||
@ -47,6 +48,11 @@ use std::os::unix::ffi::OsStrExt;
|
|||||||
use std::os::unix::io::FromRawFd;
|
use std::os::unix::io::FromRawFd;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// The fixed length of a directory's `meta` file.
|
||||||
|
///
|
||||||
|
/// See DirMeta comments within proto/schema.proto for more explanation.
|
||||||
|
const FIXED_DIR_META_LEN: usize = 512;
|
||||||
|
|
||||||
/// A sample file directory. Typically one per physical disk drive.
|
/// A sample file directory. Typically one per physical disk drive.
|
||||||
///
|
///
|
||||||
/// If the directory is used for writing, the `start_syncer` function should be called to start
|
/// If the directory is used for writing, the `start_syncer` function should be called to start
|
||||||
@ -100,8 +106,8 @@ impl Fd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a sample file within this directory with the given flags and (if creating) mode.
|
/// Opens a sample file within this directory with the given flags and (if creating) mode.
|
||||||
unsafe fn openat(&self, p: *const c_char, flags: libc::c_int, mode: libc::c_int)
|
pub(crate) unsafe fn openat(&self, p: *const c_char, flags: libc::c_int, mode: libc::c_int)
|
||||||
-> Result<fs::File, io::Error> {
|
-> Result<fs::File, io::Error> {
|
||||||
let fd = libc::openat(self.0, p, flags, mode);
|
let fd = libc::openat(self.0, p, flags, mode);
|
||||||
if fd < 0 {
|
if fd < 0 {
|
||||||
return Err(io::Error::last_os_error())
|
return Err(io::Error::last_os_error())
|
||||||
@ -153,6 +159,13 @@ pub(crate) fn read_meta(dir: &Fd) -> Result<schema::DirMeta, Error> {
|
|||||||
};
|
};
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
f.read_to_end(&mut data)?;
|
f.read_to_end(&mut data)?;
|
||||||
|
let (len, pos) = coding::decode_varint32(&data, 0)
|
||||||
|
.map_err(|_| format_err!("Unable to decode varint length in meta file"))?;
|
||||||
|
if data.len() != FIXED_DIR_META_LEN || len as usize + pos > FIXED_DIR_META_LEN {
|
||||||
|
bail!("Expected a {}-byte file with a varint length of a DirMeta message; got \
|
||||||
|
a {}-byte file with length {}", FIXED_DIR_META_LEN, data.len(), len);
|
||||||
|
}
|
||||||
|
let data = &data[pos..pos+len as usize];
|
||||||
let mut s = protobuf::CodedInputStream::from_bytes(&data);
|
let mut s = protobuf::CodedInputStream::from_bytes(&data);
|
||||||
meta.merge_from(&mut s).map_err(|e| e.context("Unable to parse metadata proto: {}"))?;
|
meta.merge_from(&mut s).map_err(|e| e.context("Unable to parse metadata proto: {}"))?;
|
||||||
Ok(meta)
|
Ok(meta)
|
||||||
@ -160,14 +173,28 @@ pub(crate) fn read_meta(dir: &Fd) -> Result<schema::DirMeta, Error> {
|
|||||||
|
|
||||||
/// Write `dir`'s metadata, clobbering existing data.
|
/// Write `dir`'s metadata, clobbering existing data.
|
||||||
pub(crate) fn write_meta(dir: &Fd, meta: &schema::DirMeta) -> Result<(), Error> {
|
pub(crate) fn write_meta(dir: &Fd, meta: &schema::DirMeta) -> Result<(), Error> {
|
||||||
let tmp_path = cstr!("meta.tmp");
|
let mut data = meta.write_length_delimited_to_bytes().expect("proto3->vec is infallible");
|
||||||
let final_path = cstr!("meta");
|
if data.len() > FIXED_DIR_META_LEN {
|
||||||
let mut f = unsafe { dir.openat(tmp_path.as_ptr(),
|
bail!("Length-delimited DirMeta message requires {} bytes, over limit of {}",
|
||||||
libc::O_CREAT | libc::O_TRUNC | libc::O_WRONLY, 0o600)? };
|
data.len(), FIXED_DIR_META_LEN);
|
||||||
meta.write_to_writer(&mut f)?;
|
}
|
||||||
f.sync_all()?;
|
data.resize(FIXED_DIR_META_LEN, 0); // pad to required length.
|
||||||
unsafe { renameat(&dir, tmp_path.as_ptr(), &dir, final_path.as_ptr())? };
|
let path = cstr!("meta");
|
||||||
dir.sync()?;
|
let mut f = unsafe { dir.openat(path.as_ptr(),
|
||||||
|
libc::O_CREAT | libc::O_WRONLY, 0o600)? };
|
||||||
|
let stat = f.metadata()?;
|
||||||
|
if stat.len() == 0 {
|
||||||
|
// Need to sync not only the data but also the file metadata and dirent.
|
||||||
|
f.write_all(&data)?;
|
||||||
|
f.sync_all()?;
|
||||||
|
dir.sync()?;
|
||||||
|
} else if stat.len() == FIXED_DIR_META_LEN as u64 {
|
||||||
|
// Just syncing the data will suffice; existing metadata and dirent are fine.
|
||||||
|
f.write_all(&data)?;
|
||||||
|
f.sync_data()?;
|
||||||
|
} else {
|
||||||
|
bail!("Existing meta file is {}-byte; expected {}", stat.len(), FIXED_DIR_META_LEN);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +210,10 @@ impl SampleFileDir {
|
|||||||
s.fd.lock(if read_write { libc::LOCK_EX } else { libc::LOCK_SH } | libc::LOCK_NB)?;
|
s.fd.lock(if read_write { libc::LOCK_EX } else { libc::LOCK_SH } | libc::LOCK_NB)?;
|
||||||
let dir_meta = read_meta(&s.fd)?;
|
let dir_meta = read_meta(&s.fd)?;
|
||||||
if !SampleFileDir::consistent(db_meta, &dir_meta) {
|
if !SampleFileDir::consistent(db_meta, &dir_meta) {
|
||||||
bail!("metadata mismatch.\ndb: {:#?}\ndir: {:#?}", db_meta, &dir_meta);
|
let serialized =
|
||||||
|
db_meta.write_length_delimited_to_bytes().expect("proto3->vec is infallible");
|
||||||
|
bail!("metadata mismatch.\ndb: {:#?}\ndir: {:#?}\nserialized db: {:#?}",
|
||||||
|
db_meta, &dir_meta, &serialized);
|
||||||
}
|
}
|
||||||
if db_meta.in_progress_open.is_some() {
|
if db_meta.in_progress_open.is_some() {
|
||||||
s.write_meta(db_meta)?;
|
s.write_meta(db_meta)?;
|
||||||
@ -193,7 +223,7 @@ impl SampleFileDir {
|
|||||||
|
|
||||||
/// Returns true if the existing directory and database metadata are consistent; the directory
|
/// Returns true if the existing directory and database metadata are consistent; the directory
|
||||||
/// is then openable.
|
/// is then openable.
|
||||||
fn consistent(db_meta: &schema::DirMeta, dir_meta: &schema::DirMeta) -> bool {
|
pub(crate) fn consistent(db_meta: &schema::DirMeta, dir_meta: &schema::DirMeta) -> bool {
|
||||||
if dir_meta.db_uuid != db_meta.db_uuid { return false; }
|
if dir_meta.db_uuid != db_meta.db_uuid { return false; }
|
||||||
if dir_meta.dir_uuid != db_meta.dir_uuid { return false; }
|
if dir_meta.dir_uuid != db_meta.dir_uuid { return false; }
|
||||||
|
|
||||||
@ -234,7 +264,7 @@ impl SampleFileDir {
|
|||||||
let e = e?;
|
let e = e?;
|
||||||
match e.file_name().as_bytes() {
|
match e.file_name().as_bytes() {
|
||||||
b"." | b".." => continue,
|
b"." | b".." => continue,
|
||||||
b"meta" | b"meta-tmp" => continue, // existing metadata is fine.
|
b"meta" => continue, // existing metadata is fine.
|
||||||
_ => return Ok(false),
|
_ => return Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -291,7 +321,7 @@ impl SampleFileDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a composite id filename.
|
/// Parses a composite id filename.
|
||||||
///
|
///
|
||||||
/// These are exactly 16 bytes, lowercase hex.
|
/// These are exactly 16 bytes, lowercase hex.
|
||||||
pub(crate) fn parse_id(id: &[u8]) -> Result<CompositeId, ()> {
|
pub(crate) fn parse_id(id: &[u8]) -> Result<CompositeId, ()> {
|
||||||
@ -311,6 +341,9 @@ pub(crate) fn parse_id(id: &[u8]) -> Result<CompositeId, ()> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use protobuf::prelude::MessageField;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_id() {
|
fn parse_id() {
|
||||||
use super::parse_id;
|
use super::parse_id;
|
||||||
@ -321,4 +354,19 @@ mod tests {
|
|||||||
parse_id(b"0").unwrap_err();
|
parse_id(b"0").unwrap_err();
|
||||||
parse_id(b"000000010000000x").unwrap_err();
|
parse_id(b"000000010000000x").unwrap_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures that a DirMeta with all fields filled fits within the maximum size.
|
||||||
|
#[test]
|
||||||
|
fn max_len_meta() {
|
||||||
|
let mut meta = schema::DirMeta::new();
|
||||||
|
let fake_uuid = &[0u8; 16][..];
|
||||||
|
meta.db_uuid.extend_from_slice(fake_uuid);
|
||||||
|
meta.dir_uuid.extend_from_slice(fake_uuid);
|
||||||
|
meta.last_complete_open.mut_message().id = u32::max_value();
|
||||||
|
meta.last_complete_open.mut_message().id = u32::max_value();
|
||||||
|
meta.in_progress_open.mut_message().uuid.extend_from_slice(fake_uuid);
|
||||||
|
meta.in_progress_open.mut_message().uuid.extend_from_slice(fake_uuid);
|
||||||
|
let data = meta.write_length_delimited_to_bytes().expect("proto3->vec is infallible");
|
||||||
|
assert!(data.len() <= FIXED_DIR_META_LEN, "{} vs {}", data.len(), FIXED_DIR_META_LEN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,26 @@ syntax = "proto3";
|
|||||||
// against the metadata stored within the database to detect inconsistencies
|
// against the metadata stored within the database to detect inconsistencies
|
||||||
// between the directory and database, such as those described in
|
// between the directory and database, such as those described in
|
||||||
// design/schema.md.
|
// design/schema.md.
|
||||||
|
//
|
||||||
|
// As of schema version 4, the overall file format is as follows: a
|
||||||
|
// varint-encoded length, followed by a serialized DirMeta message, followed
|
||||||
|
// by NUL bytes padding to a total length of 512 bytes. This message never
|
||||||
|
// exceeds that length.
|
||||||
|
//
|
||||||
|
// The goal of this format is to allow atomically rewriting a meta file
|
||||||
|
// in-place. I hope that on modern OSs and hardware, a single-sector
|
||||||
|
// rewrite is atomic, though POSIX frustratingly doesn't seem to guarantee
|
||||||
|
// this. There's some discussion of that here:
|
||||||
|
// <https://stackoverflow.com/a/2068608/23584>. At worst, there's a short
|
||||||
|
// window during which the meta file can be corrupted. As the file's purpose
|
||||||
|
// is to check for inconsistencies, it can be reconstructed if you assume no
|
||||||
|
// inconsistency exists.
|
||||||
|
//
|
||||||
|
// Schema version 3 wrote a serialized DirMeta message with no length or
|
||||||
|
// padding, and renamed new meta files over the top of old. This scheme
|
||||||
|
// requires extra space while opening the directory. If the filesystem is
|
||||||
|
// completely full, it requires freeing space manually, an undocumented and
|
||||||
|
// error-prone administrator procedure.
|
||||||
message DirMeta {
|
message DirMeta {
|
||||||
// A uuid associated with the database, in binary form. dir_uuid is strictly
|
// A uuid associated with the database, in binary form. dir_uuid is strictly
|
||||||
// more powerful, but it improves diagnostics to know if the directory
|
// more powerful, but it improves diagnostics to know if the directory
|
||||||
|
@ -489,4 +489,4 @@ create table signal_change (
|
|||||||
);
|
);
|
||||||
|
|
||||||
insert into version (id, unix_time, notes)
|
insert into version (id, unix_time, notes)
|
||||||
values (4, cast(strftime('%s', 'now') as int), 'db creation');
|
values (5, cast(strftime('%s', 'now') as int), 'db creation');
|
||||||
|
@ -41,6 +41,7 @@ mod v0_to_v1;
|
|||||||
mod v1_to_v2;
|
mod v1_to_v2;
|
||||||
mod v2_to_v3;
|
mod v2_to_v3;
|
||||||
mod v3_to_v4;
|
mod v3_to_v4;
|
||||||
|
mod v4_to_v5;
|
||||||
|
|
||||||
const UPGRADE_NOTES: &'static str =
|
const UPGRADE_NOTES: &'static str =
|
||||||
concat!("upgraded using moonfire-db ", env!("CARGO_PKG_VERSION"));
|
concat!("upgraded using moonfire-db ", env!("CARGO_PKG_VERSION"));
|
||||||
@ -66,6 +67,7 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> {
|
|||||||
v1_to_v2::run,
|
v1_to_v2::run,
|
||||||
v2_to_v3::run,
|
v2_to_v3::run,
|
||||||
v3_to_v4::run,
|
v3_to_v4::run,
|
||||||
|
v4_to_v5::run,
|
||||||
];
|
];
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||||
// Copyright (C) 2018 Scott Lamb <slamb@slamb.org>
|
// Copyright (C) 2019 Scott Lamb <slamb@slamb.org>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
110
db/upgrade/v4_to_v5.rs
Normal file
110
db/upgrade/v4_to_v5.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||||
|
// Copyright (C) 2019 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 4 schema to a version 5 schema.
|
||||||
|
///
|
||||||
|
/// This just handles the directory meta files. If they're already in the new format, great.
|
||||||
|
/// Otherwise, verify they are consistent with the database then upgrade them.
|
||||||
|
|
||||||
|
use crate::db::FromSqlUuid;
|
||||||
|
use crate::{dir, schema};
|
||||||
|
use cstr::*;
|
||||||
|
use failure::{Error, Fail, bail};
|
||||||
|
use protobuf::{Message, prelude::MessageField};
|
||||||
|
use rusqlite::params;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
const FIXED_DIR_META_LEN: usize = 512;
|
||||||
|
|
||||||
|
pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||||
|
let db_uuid: FromSqlUuid =
|
||||||
|
tx.query_row_and_then(r"select uuid from meta", params![], |row| row.get(0))?;
|
||||||
|
let mut stmt = tx.prepare(r#"
|
||||||
|
select
|
||||||
|
d.path,
|
||||||
|
d.uuid,
|
||||||
|
d.last_complete_open_id,
|
||||||
|
o.uuid
|
||||||
|
from
|
||||||
|
sample_file_dir d
|
||||||
|
left join open o on (d.last_complete_open_id = o.id);
|
||||||
|
"#)?;
|
||||||
|
let mut rows = stmt.query(params![])?;
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let path = row.get_raw_checked(0)?.as_str()?;
|
||||||
|
let dir_uuid: FromSqlUuid = row.get(1)?;
|
||||||
|
let open_id: Option<u32> = row.get(2)?;
|
||||||
|
let open_uuid: Option<FromSqlUuid> = row.get(3)?;
|
||||||
|
let mut db_meta = schema::DirMeta::new();
|
||||||
|
db_meta.db_uuid.extend_from_slice(&db_uuid.0.as_bytes()[..]);
|
||||||
|
db_meta.dir_uuid.extend_from_slice(&dir_uuid.0.as_bytes()[..]);
|
||||||
|
match (open_id, open_uuid) {
|
||||||
|
(Some(id), Some(uuid)) => {
|
||||||
|
let mut o = db_meta.last_complete_open.mut_message();
|
||||||
|
o.id = id;
|
||||||
|
o.uuid.extend_from_slice(&uuid.0.as_bytes()[..]);
|
||||||
|
},
|
||||||
|
(None, None) => {},
|
||||||
|
_ => bail!("open table missing id"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir = dir::Fd::open(path, false)?;
|
||||||
|
dir.lock(libc::LOCK_EX)?;
|
||||||
|
let tmp_path = cstr!("meta.tmp");
|
||||||
|
let path = cstr!("meta");
|
||||||
|
let mut f = unsafe { dir.openat(path.as_ptr(), libc::O_RDONLY, 0) }?;
|
||||||
|
let mut data = Vec::new();
|
||||||
|
f.read_to_end(&mut data)?;
|
||||||
|
if data.len() == FIXED_DIR_META_LEN {
|
||||||
|
continue; // already upgraded.
|
||||||
|
}
|
||||||
|
let mut s = protobuf::CodedInputStream::from_bytes(&data);
|
||||||
|
let mut dir_meta = schema::DirMeta::new();
|
||||||
|
dir_meta.merge_from(&mut s)
|
||||||
|
.map_err(|e| e.context("Unable to parse metadata proto: {}"))?;
|
||||||
|
if !dir::SampleFileDir::consistent(&db_meta, &dir_meta) {
|
||||||
|
bail!("Inconsistent db_meta={:?} dir_meta={:?}", &db_meta, &dir_meta);
|
||||||
|
}
|
||||||
|
let mut f = unsafe { dir.openat(tmp_path.as_ptr(),
|
||||||
|
libc::O_CREAT | libc::O_TRUNC | libc::O_WRONLY, 0o600)? };
|
||||||
|
let mut data =
|
||||||
|
dir_meta.write_length_delimited_to_bytes().expect("proto3->vec is infallible");
|
||||||
|
if data.len() > FIXED_DIR_META_LEN {
|
||||||
|
bail!("Length-delimited DirMeta message requires {} bytes, over limit of {}",
|
||||||
|
data.len(), FIXED_DIR_META_LEN);
|
||||||
|
}
|
||||||
|
data.resize(FIXED_DIR_META_LEN, 0); // pad to required length.
|
||||||
|
f.write_all(&data)?;
|
||||||
|
f.sync_all()?;
|
||||||
|
unsafe { dir::renameat(&dir, tmp_path.as_ptr(), &dir, path.as_ptr())? };
|
||||||
|
dir.sync()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -268,6 +268,24 @@ create table sample_file_dir (
|
|||||||
```
|
```
|
||||||
|
|
||||||
```proto
|
```proto
|
||||||
|
// Metadata stored in sample file dirs as "<dir>/meta". This is checked
|
||||||
|
// against the metadata stored within the database to detect inconsistencies
|
||||||
|
// between the directory and database, such as those described in
|
||||||
|
// design/schema.md.
|
||||||
|
//
|
||||||
|
// As of schema version 4, the overall file format is as follows: a
|
||||||
|
// varint-encoded length, followed by a serialized DirMeta message, followed
|
||||||
|
// by NUL bytes padding to a total length of 512 bytes. This message never
|
||||||
|
// exceeds that length.
|
||||||
|
//
|
||||||
|
// The goal of this format is to allow atomically rewriting a meta file
|
||||||
|
// in-place. I hope that on modern OSs and hardware, a single-sector
|
||||||
|
// rewrite is atomic, though POSIX frustratingly doesn't seem to guarantee
|
||||||
|
// this. There's some discussion of that here:
|
||||||
|
// <https://stackoverflow.com/a/2068608/23584>. At worst, there's a short
|
||||||
|
// window during which the meta file can be corrupted. As the file's purpose
|
||||||
|
// is to check for inconsistencies, it can be reconstructed if you assume no
|
||||||
|
// inconsistency exists.
|
||||||
message DirMeta {
|
message DirMeta {
|
||||||
// A uuid associated with the database, in binary form. dir_uuid is strictly
|
// A uuid associated with the database, in binary form. dir_uuid is strictly
|
||||||
// more powerful, but it improves diagnostics to know if the directory
|
// more powerful, but it improves diagnostics to know if the directory
|
||||||
@ -302,13 +320,12 @@ These are updated through procedures below:
|
|||||||
|
|
||||||
This is a sub-procedure used in several places below.
|
This is a sub-procedure used in several places below.
|
||||||
|
|
||||||
Precondition: the directory's lock is held with `LOCK_EX` (exclusive).
|
Precondition: the directory's lock is held with `LOCK_EX` (exclusive) and
|
||||||
|
there is an existing metadata file.
|
||||||
|
|
||||||
1. Write a new `meta.tmp` (opened with `O_CREAT|O_TRUNC` to discard an
|
1. Open the metadata file.
|
||||||
existing temporary file if any).
|
2. Rewrite the fixed-length data atomically.
|
||||||
2. `fsync` the `meta.tmp` file descriptor.
|
3. `fdatasync` the file.
|
||||||
3. `rename` `meta.tmp` to `meta`.
|
|
||||||
4. `fsync` the directory.
|
|
||||||
|
|
||||||
*Open the database as read-only*
|
*Open the database as read-only*
|
||||||
|
|
||||||
|
@ -223,11 +223,18 @@ Version 3 adds over version 1:
|
|||||||
* additional timestamp fields which may be useful in diagnosing/correcting
|
* additional timestamp fields which may be useful in diagnosing/correcting
|
||||||
time jumps/inconsistencies.
|
time jumps/inconsistencies.
|
||||||
|
|
||||||
### Version 3 to version 4
|
### Version 3 to version 4 to version 5
|
||||||
|
|
||||||
This upgrade affects only the SQLite database. Version 4 adds over version 3:
|
This upgrade affects only the SQLite database.
|
||||||
|
|
||||||
|
Version 4 represents a half-finished upgrade from version 3 to version 5; it
|
||||||
|
is never used.
|
||||||
|
|
||||||
|
Version 5 adds over version 3:
|
||||||
|
|
||||||
* permissions for users and sessions. Existing users will have only the
|
* permissions for users and sessions. Existing users will have only the
|
||||||
`view_video` permission, matching their previous behavior.
|
`view_video` permission, matching their previous behavior.
|
||||||
* the `signals` schema, used to store status of signals such as camera
|
* the `signals` schema, used to store status of signals such as camera
|
||||||
motion detection, security system zones, etc.
|
motion detection, security system zones, etc.
|
||||||
|
* the ability to recover from a completely full sample file directory (#65)
|
||||||
|
without manual intervention.
|
||||||
|
@ -34,8 +34,7 @@ use failure::{Error, bail};
|
|||||||
use ffmpeg;
|
use ffmpeg;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use std::os::raw::c_char;
|
use std::ffi::CString;
|
||||||
use std::ffi::{CStr, CString};
|
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
use std::sync;
|
use std::sync;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user