moonfire-nvr/server/db/json.rs

314 lines
12 KiB
Rust
Raw Normal View History

// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
//! JSON types for use in the database schema. See references from `schema.sql`.
//!
//! In general, every table in the database with reasonably low expected row
//! count should have a JSON config column. This allows the schema to be
//! modified without a major migration.
//!
//! JSON should be avoided for very high-row-count tables (eg `reocrding`) for
//! storage efficiency, in favor of separate columns or a binary type.
//! (Currently protobuf is used within `user_session`. A future schema version
//! might switch to a more JSON-like binary format to minimize impedance
//! mismatch.)
//!
//! JSON types should be designed for extensibility with forward and backward
//! compatibility:
//!
//! * Every struct has a flattened `unknown` so that if an unknown attribute is
//! written with a newer version of the binary, then the config is saved
//! (read and re-written) with an older version, the value will be
//! preserved.
//! * If a field is only for use by the UI and there's no need for the server
//! to constrain it, leave it in `unknown`.
//! * Fields shouldn't use closed enumerations or other restrictive types,
//! so that parsing the config with a non-understood value will not fail. If
//! the behavior of unknown values is not obvious, it should be clarified
//! via a comment.
//! * Fields should generally parse without values, via `#[serde(default)]`,
//! so that they can be removed in a future version if they no longer make
//! sense. It also makes sense to avoid serializing them when empty.
2021-10-26 14:47:13 -04:00
use std::{collections::BTreeMap, path::PathBuf};
use rusqlite::types::{FromSqlError, ValueRef};
use serde::{Deserialize, Serialize};
2021-10-26 16:08:45 -04:00
use serde_json::Value;
use url::Url;
use uuid::Uuid;
/// Serializes and deserializes JSON as a SQLite3 `text` column, compatible with the
/// [JSON1 extension](https://www.sqlite.org/json1.html).
macro_rules! sql {
($l:ident) => {
impl rusqlite::types::FromSql for $l {
fn column_result(value: ValueRef) -> Result<Self, FromSqlError> {
match value {
ValueRef::Text(t) => {
Ok(serde_json::from_slice(t)
.map_err(|e| FromSqlError::Other(Box::new(e)))?)
}
ValueRef::Null => Ok($l::default()),
_ => Err(FromSqlError::InvalidType),
}
}
}
impl rusqlite::types::ToSql for $l {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(serde_json::to_string(&self)
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?
.into())
}
}
};
}
/// Global configuration, used in the `config` column of the `meta` table.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GlobalConfig {
/// The maximum number of entries in the `signal_state` table (or `None` for unlimited).
///
/// If an update causes this to be exceeded, older times will be garbage
/// collected to stay within the limit.
2021-10-26 16:08:45 -04:00
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_signal_changes: Option<u32>,
/// Information about signal types.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub signal_types: BTreeMap<Uuid, SignalTypeConfig>,
/// Information about signals.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub signals: BTreeMap<u32, SignalConfig>,
#[serde(flatten)]
2021-10-26 16:08:45 -04:00
pub unknown: BTreeMap<String, Value>,
}
sql!(GlobalConfig);
2021-10-26 14:47:13 -04:00
/// Sample file directory configuration, used in the `config` column of the `sample_file_dir` table.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SampleFileDirConfig {
pub path: PathBuf,
#[serde(flatten)]
2021-10-26 16:08:45 -04:00
pub unknown: BTreeMap<String, Value>,
2021-10-26 14:47:13 -04:00
}
sql!(SampleFileDirConfig);
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalTypeConfig {
/// Information about possible enumeration values of this signal type.
///
/// 0 always means `unknown`. Other values may be specified here to set
/// their configuration. It's more efficient in terms of encoded length
/// and RAM at runtime for common values (eg, `still`, `normal`, or
/// `disarmed`) to be numerically lower than rarer values (eg `motion`,
/// `violated`, or `armed`) and for the value space to be dense (eg, to use
/// values 1, 2, 3 rather than 1, 10, 20).
///
/// Currently values must be in the range `[0, 16)`.
///
/// Nothing enforces that only values specified here may be set for a signal
/// of this type.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub values: BTreeMap<u8, SignalTypeValueConfig>,
#[serde(flatten)]
2021-10-26 16:08:45 -04:00
pub unknown: BTreeMap<String, Value>,
}
sql!(SignalTypeConfig);
/// Information about a signal type value; used in `SignalTypeConfig::values`.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalTypeValueConfig {
pub name: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub motion: bool,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub color: String,
#[serde(flatten)]
2021-10-26 16:08:45 -04:00
pub unknown: BTreeMap<String, Value>,
}
impl SignalTypeValueConfig {
pub fn is_empty(&self) -> bool {
self.unknown.is_empty()
}
}
/// Camera configuration, used in the `config` column of the `camera` table.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CameraConfig {
/// A short description of the camera.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
/// The base URL for accessing ONVIF; `device_service` will be joined on
/// automatically to form the device management service URL.
/// Eg with `onvif_base=http://192.168.1.110:85`, the full
/// URL of the device management service will be
/// `http://192.168.1.110:85/device_service`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub onvif_base_url: Option<Url>,
/// The username to use when accessing the camera.
/// If empty, no username or password will be supplied.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub username: String,
/// The password to use when accessing the camera.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub password: String,
#[serde(flatten)]
2021-10-26 16:08:45 -04:00
pub unknown: BTreeMap<String, Value>,
}
sql!(CameraConfig);
impl CameraConfig {
pub fn is_empty(&self) -> bool {
self.description.is_empty()
&& self.onvif_base_url.is_none()
&& self.username.is_empty()
&& self.password.is_empty()
&& self.unknown.is_empty()
}
}
/// Stream configuration, used in the `config` column of the `stream` table.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StreamConfig {
/// The mode of operation for this camera on startup.
///
/// Null means entirely disabled. At present, so does any value other than
/// `record`.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub mode: String,
/// The `rtsp://` URL to use for this stream, excluding username and
/// password.
///
/// In the future, this might support additional protocols such as `rtmp://`
/// or even a private use URI scheme for the [Baichuan
/// protocol](https://github.com/thirtythreeforty/neolink).
///
/// (Credentials are taken from [`CameraConfig`]'s respective fields.)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
/// The RTSP transport (`tcp` or `udp`) to use.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub rtsp_transport: String,
/// The number of bytes of video to retain, excluding the
/// currently-recording file.
///
/// Older files will be deleted as necessary to stay within this limit.
#[serde(default)]
pub retain_bytes: i64,
/// Flush the database when the first instant of completed recording is this
/// many seconds old. A value of 0 means that every completed recording will
/// cause an immediate flush. Higher values may allow flushes to be combined,
/// reducing SSD write cycles. For example, if all streams have a
/// `flush_if_sec` >= *x* sec, there will be:
///
/// * at most one flush per *x* sec in total
/// * at most *x* sec of completed but unflushed recordings per stream.
/// * at most *x* completed but unflushed recordings per stream, in the
/// worst case where a recording instantly fails, waits the 1-second retry
/// delay, then fails again, forever.
#[serde(default)]
pub flush_if_sec: u32,
#[serde(flatten)]
2021-10-26 16:08:45 -04:00
pub unknown: BTreeMap<String, Value>,
}
sql!(StreamConfig);
pub const STREAM_MODE_RECORD: &'static str = "record";
impl StreamConfig {
pub fn is_empty(&self) -> bool {
self.mode.is_empty()
&& self.url.is_none()
&& self.retain_bytes == 0
&& self.flush_if_sec == 0
&& self.unknown.is_empty()
}
}
/// Signal configuration, used in the `config` column of the `signal` table.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalConfig {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub short_name: String,
/// Map of associated cameras to the type of association.
///
/// `direct` is as if the event source is the camera's own motion detection.
/// Here are a couple ways this could be used:
///
/// * when viewing the camera, hotkeys to go to the start of the next or
/// previous event should respect this event.
/// * a list of events might include the recordings associated with the
/// camera in the same timespan.
///
/// `indirect` might mean a screen associated with the camera should given
/// some indication of this event, but there should be no assumption that
/// the camera will have a direct view of the event. For example, all
/// cameras might be indirectly associated with a doorknob press. Cameras
/// at the back of the house shouldn't be expected to have a direct view of
/// this event, but motion events shortly afterward might warrant extra
/// scrutiny.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub camera_associations: BTreeMap<i32, String>,
#[serde(flatten)]
2021-10-26 16:08:45 -04:00
pub unknown: BTreeMap<String, Value>,
}
sql!(SignalConfig);
2021-10-26 16:08:45 -04:00
/// User configuration, used in the `config` column of the `user` table.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserConfig {
/// If true, no method of authentication will succeed for this user.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub disabled: bool,
/// If set, a Unix UID that is accepted for authentication when using HTTP over
/// a Unix domain socket.
///
/// (Additionally, the UID running Moonfire NVR can authenticate as anyone;
/// there's no point in trying to do otherwise.) This might be an easy
/// bootstrap method once configuration happens through a web UI rather than
/// text UI.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unix_uid: Option<u64>,
/// Preferences controlled by the user.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub preferences: UserPreferences,
#[serde(flatten)]
pub unknown: BTreeMap<String, Value>,
}
sql!(UserConfig);
pub type UserPreferences = BTreeMap<String, Value>;