mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-25 21:53:16 -05:00
new "moonfire-nvr config" subcommand
This is a ncurses-based user interface for configuration. This fills a major usability gap: the system can be configured without manual SQL commands.
This commit is contained in:
parent
b3a7795407
commit
c82f038bef
112
Cargo.lock
generated
112
Cargo.lock
generated
@ -5,6 +5,7 @@ dependencies = [
|
||||
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chan 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chan-signal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cursive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"docopt 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ffmpeg 0.2.0-alpha.2 (git+https://github.com/scottlamb/rust-ffmpeg?branch=2.x)",
|
||||
"ffmpeg-sys 2.8.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -51,6 +52,14 @@ dependencies = [
|
||||
"memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.4.0"
|
||||
@ -87,6 +96,17 @@ dependencies = [
|
||||
"rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chan-signal"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chan 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chan-signal"
|
||||
version = "0.2.0"
|
||||
@ -112,6 +132,21 @@ name = "crossbeam"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cursive"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"chan 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chan-signal 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ncurses 5.85.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"odds 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-segmentation 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docopt"
|
||||
version = "0.7.0"
|
||||
@ -254,6 +289,11 @@ name = "language-tags"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "0.2.2"
|
||||
@ -342,16 +382,48 @@ dependencies = [
|
||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ncurses"
|
||||
version = "5.85.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"gcc 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-bigint 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-complex 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-rational 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.32"
|
||||
@ -369,6 +441,17 @@ dependencies = [
|
||||
"num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-bigint 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.36"
|
||||
@ -390,6 +473,11 @@ dependencies = [
|
||||
"libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "odds"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.9.6"
|
||||
@ -726,6 +814,9 @@ dependencies = [
|
||||
name = "toml"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "traitobject"
|
||||
@ -758,6 +849,16 @@ name = "unicode-normalization"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.0.4"
|
||||
@ -826,15 +927,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
[metadata]
|
||||
"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
|
||||
"checksum aho-corasick 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4f660b942762979b56c9f07b4b36bb559776fbad102f05d6771e1b629e8fd5bf"
|
||||
"checksum bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e6e1e6fb1c9e3d6fcdec57216a74eaa03e41f52a22f13a16438251d8e88b89da"
|
||||
"checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c"
|
||||
"checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d"
|
||||
"checksum bitflags 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32866f4d103c4e438b1db1158aa1b1a80ee078e5d77a59a2f906fd62a577389c"
|
||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||
"checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8"
|
||||
"checksum chan 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "82b22acfef7960fd8f829bc50749273be637cbd76b9d4cc20497666cc3a33329"
|
||||
"checksum chan-signal 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "365122ab60a9dc6240b48e39d011b4389c3853093d98bf586edd2b79bfb4fbfa"
|
||||
"checksum chan-signal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0f3bb6c3bc387004ad914f0c5b7f33ace8bf7604bbec35f228b1a017f52cd3a0"
|
||||
"checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00"
|
||||
"checksum crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0c5ea215664ca264da8a9d9c3be80d2eaf30923c259d03e870388eb927508f97"
|
||||
"checksum cursive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8d226ba768f44025aeec3485ce3ca9c7e93c3f2a7e02e8f00d5b4c70c490011b"
|
||||
"checksum docopt 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ab32ea6e284d87987066f21a9e809a73c14720571ef34516f0890b3d355ccfd8"
|
||||
"checksum dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0dd841b58510c9618291ffa448da2e4e0f699d984d436122372f446dae62263d"
|
||||
"checksum error-chain 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "318cb3c71ee4cdea69fdc9e15c173b245ed6063e1709029e8fd32525a881120f"
|
||||
@ -852,6 +956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1"
|
||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
|
||||
"checksum lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417"
|
||||
"checksum lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6abe0ee2e758cd6bc8a2cd56726359007748fbf4128da998b65d0b70f881e19b"
|
||||
"checksum libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)" = "684f330624d8c3784fb9558ca46c4ce488073a8d22450415c5eb4f4cfb0d11b5"
|
||||
"checksum libsqlite3-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b6de3eea39ba6ed0cddf04e1c7a78486e3f750441e0a0b15b6ea39d0dd8e1b8c"
|
||||
@ -864,12 +969,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum memmap 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "065ce59af31c18ea2c419100bda6247dd4ec3099423202b12f0bd32e529fabd2"
|
||||
"checksum metadeps 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829fffe7ea1d747e23f64be972991bc516b2f1ac2ae4a3b33d8bea150c410151"
|
||||
"checksum mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5c93a4bd787ddc6e7833c519b73a50883deb5863d76d9b71eb8216fb7f94e66"
|
||||
"checksum ncurses 5.85.0 (registry+https://github.com/rust-lang/crates.io-index)" = "21f71f0e1ae96558612b1e9d188ec4f23149a11ee4fb4b96e130523bea52d605"
|
||||
"checksum num 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "bde7c03b09e7c6a301ee81f6ddf66d7a28ec305699e3d3b056d2fc56470e3120"
|
||||
"checksum num-bigint 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "88b14378471f7c2adc5262f05b4701ef53e8da376453a8d8fee48e51db745e49"
|
||||
"checksum num-complex 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c78e054dd19c3fd03419ade63fa661e9c49bb890ce3beb4eee5b7baf93f92f"
|
||||
"checksum num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "fb24d9bfb3f222010df27995441ded1e954f8f69cd35021f6bef02ca9552fb92"
|
||||
"checksum num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "287a1c9969a847055e1122ec0ea7a5c5d6f72aad97934e131c83d5c08ab4e45c"
|
||||
"checksum num-rational 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "54ff603b8334a72fbb27fe66948aac0abaaa40231b3cecd189e76162f6f38aaf"
|
||||
"checksum num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "a16a42856a256b39c6d3484f097f6713e14feacd9bfb02290917904fae46c81c"
|
||||
"checksum num_cpus 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "cee7e88156f3f9e19bdd598f8d6c9db7bf4078f99f8381f43a55b09648d1a6e3"
|
||||
"checksum num_cpus 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a225d1e2717567599c24f88e49f00856c6e825a12125181ee42c4257e3688d39"
|
||||
"checksum odds 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "c3df9b730298cea3a1c3faa90b7e2f9df3a9c400d0936d6015e6165734eefcba"
|
||||
"checksum openssl 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0c00da69323449142e00a5410f0e022b39e8bbb7dc569cee8fc6af279279483c"
|
||||
"checksum openssl-sys 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b1482f9a06f56c906007e17ea14d73d102210b5d27bc948bf5e175f493f3f7c3"
|
||||
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
|
||||
@ -914,6 +1024,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764"
|
||||
"checksum unicode-bidi 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b61814f3e7fd0e0f15370f767c7c943e08bc2e3214233ae8f88522b334ceb778"
|
||||
"checksum unicode-normalization 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5e94e9f6961090fcc75180629c4ef33e5310d6ed2c0dd173f4ca63c9043b669e"
|
||||
"checksum unicode-segmentation 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7baebdc1df1363fa66161fca2fe047e4f4209011cc7e045948298996afdf85df"
|
||||
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
|
||||
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
|
||||
"checksum unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2ae5ddb18e1c92664717616dd9549dde73f539f01bd7b77c2edb2446bdff91"
|
||||
"checksum url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f5ba8a749fb4479b043733416c244fa9d1d3af3d7c23804944651c8a448cb87e"
|
||||
|
@ -42,6 +42,11 @@ serde_codegen = "0.8"
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3"
|
||||
|
||||
[dependencies.cursive]
|
||||
version = "0.4"
|
||||
#default-features = false
|
||||
#features = ["termion"]
|
||||
|
||||
[dependencies.ffmpeg]
|
||||
git = "https://github.com/scottlamb/rust-ffmpeg"
|
||||
branch = "2.x"
|
||||
|
110
README.md
110
README.md
@ -89,6 +89,9 @@ You will need the following C libraries installed:
|
||||
|
||||
* [SQLite3](https://www.sqlite.org/).
|
||||
|
||||
* [`ncursesw`](https://www.gnu.org/software/ncurses/), the UTF-8 version of
|
||||
the `ncurses` library.
|
||||
|
||||
On Ubuntu 16.04.1 LTS or Raspbian Jessie, the following command will install
|
||||
all non-Rust dependencies:
|
||||
|
||||
@ -97,13 +100,8 @@ all non-Rust dependencies:
|
||||
libavcodec-dev \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
uuid-runtime
|
||||
|
||||
uuid-runtime is only necessary if you wish to use the uuid command to generate
|
||||
uuids for your cameras (see below). If you obtain them elsewhere, you can skip this
|
||||
package.
|
||||
libncursesw-dev \
|
||||
libsqlite3-dev
|
||||
|
||||
Next, you need Rust and Cargo. The easiest way to install them is by following
|
||||
the instructions at [rustup.rs](https://www.rustup.rs/). Note that Rust 1.13
|
||||
@ -119,7 +117,6 @@ build and install, or alternatively you can run the prep script called `prep.sh`
|
||||
|
||||
The script will take the following command line options, should you need them:
|
||||
|
||||
* `-D`: Skip database initialization.
|
||||
* `-S`: Skip updating and installing dependencies through apt-get. This too can be
|
||||
useful on repeated builds.
|
||||
|
||||
@ -133,7 +130,7 @@ store the video samples inside a directory named `samples` there, you would set:
|
||||
SAMPLES_DIR=/media/nvr/samples
|
||||
|
||||
The script will perform all necessary steps to leave you with a fully built,
|
||||
installed moonfire-nvr binary and (running) system service. The only thing
|
||||
installed moonfire-nvr binary. The only thing
|
||||
you'll have to do manually is add your camera configuration(s) to the database.
|
||||
Alternatively, before running the script, you can create a file named `cameras.sql`
|
||||
in the same directory as the `prep.sh` script and it will be automatically
|
||||
@ -180,82 +177,35 @@ If a dedicated hard drive is available, set up the mount point:
|
||||
$ sudo mount /var/lib/moonfire-nvr/sample
|
||||
|
||||
Once setup is complete, it is time to add camera configurations to the
|
||||
database. However, the interface for adding new cameras is not yet written,
|
||||
so you will have to manually insert cameras configurations with the `sqlite3`
|
||||
command line tool prior to starting Moonfire NVR.
|
||||
database. If the daemon is running, you will need to stop it temporarily:
|
||||
|
||||
Before setting up a camera, it may be helpful to test settings with the
|
||||
`ffmpeg` command line tool:
|
||||
$ sudo systemctl stop moonfire-nvr
|
||||
|
||||
$ ffmpeg \
|
||||
-i "rtsp://admin:12345@192.168.1.101:554/Streaming/Channels/1" \
|
||||
-c copy \
|
||||
-map 0:0 \
|
||||
-rtsp_transport tcp \
|
||||
-flags:v +global_header \
|
||||
test.mp4
|
||||
You can configure the system through a text-based user interface:
|
||||
|
||||
Once you have a working `ffmpeg` command line, insert the camera config as
|
||||
follows. See the schema SQL file's comments for more information.
|
||||
Note that the sum of `retain_bytes` for all cameras combined should be
|
||||
somewhat less than the available bytes on the sample file directory's
|
||||
filesystem, as the currently-writing sample files are not included in
|
||||
this sum. Be sure also to subtract out the filesystem's reserve for root
|
||||
(typically 5%).
|
||||
$ sudo -u moonfire-nvr moonfire-nvr config
|
||||
|
||||
In the following example, we generate a uuid which is then later used
|
||||
to uniquely identify this camera. Thus, you will generate a new one for
|
||||
each camera you insert using this method.
|
||||
In the user interface, add your cameras under the "Edit cameras" dialog.
|
||||
There's a "Test" button to verify your settings directly from the dialog.
|
||||
|
||||
$ uuidgen | sed -e 's/-//g'
|
||||
b47f48706d91414591cd6c931bf836b4
|
||||
$ sudo -u moonfire-nvr sqlite3 ~moonfire-nvr/db/db
|
||||
sqlite3> insert into camera (
|
||||
...> uuid, short_name, description, host, username, password,
|
||||
...> main_rtsp_path, sub_rtsp_path, retain_bytes,
|
||||
...> next_recording_id) values (
|
||||
...> X'b47f48706d91414591cd6c931bf836b4', 'driveway',
|
||||
...> 'Longer description of this camera', '192.168.1.101',
|
||||
...> 'admin', '12345', '/Streaming/Channels/1',
|
||||
...> '/Streaming/Channels/2', 104857600, 0);
|
||||
sqlite3> ^D
|
||||
After the cameras look correct, go to "Edit retention" to assign disk space to
|
||||
each camera. Leave a little slack (at least 100 MB per camera) between the total
|
||||
limit and the filesystem capacity, even if you store nothing else on the disk.
|
||||
There are several reasons this is needed:
|
||||
|
||||
### Using automatic camera configuration inclusion with `prep.sh`
|
||||
* The limit currently controls fully-written files only. There will be up
|
||||
to two minutes of video per camera of additional video.
|
||||
* The rotation happens after the limit is exceeded, not proactively.
|
||||
* Moonfire NVR currently doesn't account for the unused space in the final
|
||||
filesystem block at the end of each file.
|
||||
* Moonfire NVR doesn't account for the space used for directory listings.
|
||||
* If a file is open when it is deleted (such as if a HTTP client is
|
||||
downloading it), it stays around until the file is closed. Moonfire NVR
|
||||
currently doesn't account for this.
|
||||
|
||||
Not withstanding the instructions above, you can also prepare a file named
|
||||
`cameras.sql` before you run the `prep.sh` script. The format of this file
|
||||
should be something like in the example below for two cameras (SQL gives you
|
||||
lots of freedom in the use of blank space and newlines, so this is formatted
|
||||
for easy reading, and editing, and does not have to be altered in formatting,
|
||||
but can if you wish and know what you are doing):
|
||||
When finished, start the daemon:
|
||||
|
||||
insert into camera (
|
||||
uuid,
|
||||
short_name, description,
|
||||
host, username, password,
|
||||
main_rtsp_path, sub_rtsp_path,
|
||||
retain_bytes, next_recording_id
|
||||
)
|
||||
values
|
||||
(
|
||||
X'1c944181b8074b8083eb579c8e194451',
|
||||
'Front Left', 'Front Left Driveway',
|
||||
'192.168.1.41',
|
||||
'admin', 'secret',
|
||||
'/Streaming/Channels/1', '/Streaming/Channels/2',
|
||||
346870912000, 0
|
||||
),
|
||||
(
|
||||
X'da5921f493ac4279aafe68e69e174026',
|
||||
'Front Right', 'Front Right Driveway',
|
||||
'192.168.1.42',
|
||||
'admin', 'secret',
|
||||
'/Streaming/Channels/1', '/Streaming/Channels/2',
|
||||
346870912000, 0
|
||||
);
|
||||
|
||||
You'll still have to find the correct rtsp paths, usernames and passwords, and
|
||||
set retained byte counts, as explained above.
|
||||
$ sudo systemctl start moonfire-nvr
|
||||
|
||||
## System Service
|
||||
|
||||
@ -290,9 +240,9 @@ directly exposed to the Internet.
|
||||
Complete the installation through `systemctl` commands:
|
||||
|
||||
$ sudo systemctl daemon-reload
|
||||
$ sudo systemctl start moonfire-nvr.service
|
||||
$ sudo systemctl status moonfire-nvr.service
|
||||
$ sudo systemctl enable moonfire-nvr.service
|
||||
$ sudo systemctl start moonfire-nvr
|
||||
$ sudo systemctl status moonfire-nvr
|
||||
$ sudo systemctl enable moonfire-nvr
|
||||
|
||||
See the [systemd](http://www.freedesktop.org/wiki/Software/systemd/)
|
||||
documentation for more information. The [manual
|
||||
|
10
prep.sh
10
prep.sh
@ -34,7 +34,6 @@
|
||||
# Script to prepare for moonfire-nvr operations
|
||||
#
|
||||
# Command line options:
|
||||
# -D: Skip database initialization
|
||||
# -S: Skip apt-get update and install
|
||||
#
|
||||
|
||||
@ -106,7 +105,6 @@ NVR_GROUP="${NVR_GROUP:-$NVR_USER}"
|
||||
NVR_PORT="${NVR_PORT:-8080}"
|
||||
NVR_HOME_BASE="${NVR_HOME_BASE:-/var/lib}"
|
||||
NVR_HOME="${NVR_HOME_BASE}/${NVR_USER}"
|
||||
DB_NAME="${DB_NAME:-db}"
|
||||
DB_DIR="${DB_DIR:-$NVR_HOME/db}"
|
||||
SAMPLES_DIR_NAME="${SAMPLES_DIR_NAME:-samples}"
|
||||
SAMPLES_DIR="${SAMPLES_DIR:-$NVR_HOME/$SAMPLES_DIR_NAME}"
|
||||
@ -214,13 +212,7 @@ echo 'Create database...'; echo
|
||||
if [ ! -d "${DB_DIR}" ]; then
|
||||
sudo -u ${NVR_USER} -H mkdir "${DB_DIR}"
|
||||
fi
|
||||
DB_PATH="${DB_DIR}/${DB_NAME}"
|
||||
CAMERAS_PATH="${SRC_DIR}/../cameras.sql"
|
||||
[ "${SKIP_DB:-0}" == 0 ] && sudo -u ${NVR_USER} -H ${SERVICE_BIN} init --db-dir="${DB_PATH}"
|
||||
if [ -r "${CAMERAS_PATH}" ]; then
|
||||
echo 'Add cameras...'; echo
|
||||
sudo -u ${NVR_USER} -H sqlite3 "${DB_PATH}" < "${CAMERAS_PATH}"
|
||||
fi
|
||||
sudo -u ${NVR_USER} -H ${SERVICE_BIN} init --db-dir="${DB_DIR}"
|
||||
|
||||
# Prepare service files
|
||||
#
|
||||
|
270
src/cmds/config/cameras.rs
Normal file
270
src/cmds/config/cameras.rs
Normal file
@ -0,0 +1,270 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2017 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/>.
|
||||
|
||||
extern crate cursive;
|
||||
|
||||
use self::cursive::Cursive;
|
||||
use self::cursive::traits::{Boxable, Identifiable, Finder};
|
||||
use self::cursive::views;
|
||||
use db;
|
||||
use dir;
|
||||
use error::Error;
|
||||
use std::sync::Arc;
|
||||
use stream::{self, Opener, Stream};
|
||||
use super::{decode_size, encode_size};
|
||||
|
||||
/// Builds a `CameraChange` from an active `edit_camera_dialog`.
|
||||
fn get_change(siv: &mut Cursive) -> db::CameraChange {
|
||||
db::CameraChange{
|
||||
short_name: siv.find_id::<views::EditView>("short_name")
|
||||
.unwrap().get_content().as_str().into(),
|
||||
description: siv.find_id::<views::TextArea>("description").unwrap().get_content().into(),
|
||||
host: siv.find_id::<views::EditView>("host").unwrap().get_content().as_str().into(),
|
||||
username: siv.find_id::<views::EditView>("username").unwrap().get_content().as_str().into(),
|
||||
password: siv.find_id::<views::EditView>("password").unwrap().get_content().as_str().into(),
|
||||
main_rtsp_path: siv.find_id::<views::EditView>("main_rtsp_path")
|
||||
.unwrap().get_content().as_str().into(),
|
||||
sub_rtsp_path: siv.find_id::<views::EditView>("sub_rtsp_path")
|
||||
.unwrap().get_content().as_str().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn press_edit(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
|
||||
id: Option<i32>) {
|
||||
let change = get_change(siv);
|
||||
siv.pop_layer(); // get rid of the add/edit camera dialog.
|
||||
|
||||
let result = {
|
||||
let mut l = db.lock();
|
||||
if let Some(id) = id {
|
||||
l.update_camera(id, change)
|
||||
} else {
|
||||
l.add_camera(change).map(|_| ())
|
||||
}
|
||||
};
|
||||
if let Err(e) = result {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to add camera: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
} else {
|
||||
// Recreate the "Edit cameras" dialog from scratch; it's easier than adding the new entry.
|
||||
siv.pop_layer();
|
||||
add_dialog(db, dir, siv);
|
||||
}
|
||||
}
|
||||
|
||||
fn press_test_inner(url: &str) -> Result<String, Error> {
|
||||
let stream = stream::FFMPEG.open(stream::Source::Rtsp(url))?;
|
||||
let extra_data = stream.get_extra_data()?;
|
||||
Ok(format!("{}x{} video stream", extra_data.width, extra_data.height))
|
||||
}
|
||||
|
||||
fn press_test(siv: &mut Cursive, c: &db::CameraChange, stream: &str, path: &str) {
|
||||
let url = format!("rtsp://{}:{}@{}{}", c.username, c.password, c.host, path);
|
||||
let description = match press_test_inner(&url) {
|
||||
Err(e) => {
|
||||
siv.add_layer(
|
||||
views::Dialog::text(format!("{} stream at {}:\n\n{}", stream, url, e))
|
||||
.title("Stream test failed")
|
||||
.dismiss_button("Back"));
|
||||
return;
|
||||
},
|
||||
Ok(d) => d,
|
||||
};
|
||||
siv.add_layer(views::Dialog::text(format!("{} stream at {}:\n\n{}", stream, url, description))
|
||||
.title("Stream test succeeded")
|
||||
.dismiss_button("Back"));
|
||||
}
|
||||
|
||||
fn press_delete(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, id: i32,
|
||||
name: String, to_delete: i64) {
|
||||
let dialog = if to_delete > 0 {
|
||||
let prompt = format!("Camera {} has recorded video. Please confirm the amount \
|
||||
of data to delete by typing it back:\n\n{}", name,
|
||||
encode_size(to_delete));
|
||||
views::Dialog::around(
|
||||
views::LinearLayout::vertical()
|
||||
.child(views::TextView::new(prompt))
|
||||
.child(views::DummyView)
|
||||
.child(views::EditView::new().on_submit({
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |siv, _| confirm_deletion(siv, &db, &dir, id, to_delete)
|
||||
}).with_id("confirm")))
|
||||
.button("Delete", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |siv| confirm_deletion(siv, &db, &dir, id, to_delete)
|
||||
})
|
||||
} else {
|
||||
views::Dialog::text(format!("Delete camera {}? This camera has no recorded video.", name))
|
||||
.button("Delete", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| actually_delete(s, &db, &dir, id)
|
||||
})
|
||||
}.title("Delete camera").dismiss_button("Cancel");
|
||||
siv.add_layer(dialog);
|
||||
}
|
||||
|
||||
fn confirm_deletion(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
|
||||
id: i32, to_delete: i64) {
|
||||
let typed = siv.find_id::<views::EditView>("confirm").unwrap().get_content();
|
||||
if decode_size(typed.as_str()).ok() == Some(to_delete) {
|
||||
siv.pop_layer(); // deletion confirmation dialog
|
||||
if let Err(e) = dir::lower_retention(dir.clone(),
|
||||
&[dir::NewLimit{camera_id: id, limit: 0}]) {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to delete recordings: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
return;
|
||||
}
|
||||
actually_delete(siv, db, dir, id);
|
||||
} else {
|
||||
siv.add_layer(views::Dialog::text("Please confirm amount.")
|
||||
.title("Try again")
|
||||
.dismiss_button("Back"));
|
||||
}
|
||||
}
|
||||
|
||||
fn actually_delete(siv: &mut Cursive, db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>,
|
||||
id: i32) {
|
||||
info!("actually_delete call");
|
||||
siv.pop_layer(); // get rid of the add/edit camera dialog.
|
||||
let result = {
|
||||
let mut l = db.lock();
|
||||
l.delete_camera(id)
|
||||
};
|
||||
if let Err(e) = result {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to delete camera: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
} else {
|
||||
// Recreate the "Edit cameras" dialog from scratch; it's easier than adding the new entry.
|
||||
siv.pop_layer();
|
||||
add_dialog(db, dir, siv);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds or updates a camera.
|
||||
/// (The former if `item` is None; the latter otherwise.)
|
||||
fn edit_camera_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive,
|
||||
item: &Option<i32>) {
|
||||
let list = views::ListView::new()
|
||||
.child("id", views::TextView::new(match *item {
|
||||
None => "<new>".to_string(),
|
||||
Some(id) => id.to_string(),
|
||||
}))
|
||||
.child("uuid", views::TextView::new("<new>").with_id("uuid"))
|
||||
.child("short name", views::EditView::new().with_id("short_name"))
|
||||
.child("host", views::EditView::new().with_id("host"))
|
||||
.child("username", views::EditView::new().with_id("username"))
|
||||
.child("password", views::EditView::new().with_id("password"))
|
||||
.child("main_rtsp_path", views::LinearLayout::horizontal()
|
||||
.child(views::EditView::new().with_id("main_rtsp_path").full_width())
|
||||
.child(views::DummyView)
|
||||
.child(views::Button::new("Test", |siv| {
|
||||
let c = get_change(siv);
|
||||
press_test(siv, &c, "main", &c.main_rtsp_path)
|
||||
})))
|
||||
.child("sub_rtsp_path", views::LinearLayout::horizontal()
|
||||
.child(views::EditView::new().with_id("sub_rtsp_path").full_width())
|
||||
.child(views::DummyView)
|
||||
.child(views::Button::new("Test", |siv| {
|
||||
let c = get_change(siv);
|
||||
press_test(siv, &c, "sub", &c.sub_rtsp_path)
|
||||
})))
|
||||
.min_height(8);
|
||||
let layout = views::LinearLayout::vertical()
|
||||
.child(list)
|
||||
.child(views::TextView::new("description"))
|
||||
.child(views::TextArea::new().with_id("description").min_height(3))
|
||||
.full_width();
|
||||
let mut dialog = views::Dialog::around(layout);
|
||||
let dialog = if let Some(id) = *item {
|
||||
let l = db.lock();
|
||||
let camera = l.cameras_by_id().get(&id).expect("missing camera");
|
||||
dialog.find_id::<views::TextView>("uuid")
|
||||
.expect("missing TextView")
|
||||
.set_content(camera.uuid.to_string());
|
||||
let bytes = camera.sample_file_bytes;
|
||||
let name = camera.short_name.clone();
|
||||
for &(view_id, content) in &[("short_name", &camera.short_name),
|
||||
("host", &camera.host),
|
||||
("username", &camera.username),
|
||||
("password", &camera.password),
|
||||
("main_rtsp_path", &camera.main_rtsp_path),
|
||||
("sub_rtsp_path", &camera.sub_rtsp_path)] {
|
||||
dialog.find_id::<views::EditView>(view_id)
|
||||
.expect("missing EditView")
|
||||
.set_content(content.to_string());
|
||||
}
|
||||
dialog.find_id::<views::TextArea>("description")
|
||||
.expect("missing TextArea")
|
||||
.set_content(camera.description.to_string());
|
||||
dialog.title("Edit camera")
|
||||
.button("Edit", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| press_edit(s, &db, &dir, Some(id))
|
||||
})
|
||||
.button("Delete", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| press_delete(s, &db, &dir, id, name.clone(), bytes)
|
||||
})
|
||||
} else {
|
||||
dialog.title("Add camera")
|
||||
.button("Add", {
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |s| press_edit(s, &db, &dir, None)
|
||||
})
|
||||
};
|
||||
siv.add_layer(dialog.dismiss_button("Cancel"));
|
||||
}
|
||||
|
||||
pub fn add_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive) {
|
||||
siv.add_layer(views::Dialog::around(
|
||||
views::SelectView::new()
|
||||
.on_submit({
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |siv, item| edit_camera_dialog(&db, &dir, siv, item)
|
||||
})
|
||||
.item("<new camera>".to_string(), None)
|
||||
.with_all(db.lock()
|
||||
.cameras_by_id()
|
||||
.iter()
|
||||
.map(|(&id, camera)| (format!("{}: {}", id, camera.short_name), Some(id))))
|
||||
.full_width())
|
||||
.dismiss_button("Done")
|
||||
.title("Edit cameras"));
|
||||
}
|
163
src/cmds/config/mod.rs
Normal file
163
src/cmds/config/mod.rs
Normal file
@ -0,0 +1,163 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2017 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/>.
|
||||
|
||||
//! Text-based configuration interface.
|
||||
//!
|
||||
//! This code is a bit messy, but it's essentially a prototype. Eventually Moonfire NVR's
|
||||
//! configuration will likely be almost entirely done through a web-based UI.
|
||||
|
||||
extern crate cursive;
|
||||
|
||||
use self::cursive::Cursive;
|
||||
use self::cursive::views;
|
||||
use db;
|
||||
use dir;
|
||||
use error::Error;
|
||||
use regex::Regex;
|
||||
use std::sync::Arc;
|
||||
use std::fmt::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod cameras;
|
||||
mod retention;
|
||||
|
||||
static USAGE: &'static str = r#"
|
||||
Interactive configuration editor.
|
||||
|
||||
Usage:
|
||||
|
||||
moonfire-nvr config [options]
|
||||
moonfire-nvr config --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]
|
||||
"#;
|
||||
|
||||
static MULTIPLIERS: [(char, u64); 4] = [
|
||||
// (suffix character, power of 2)
|
||||
('T', 40),
|
||||
('G', 30),
|
||||
('M', 20),
|
||||
('K', 10),
|
||||
];
|
||||
|
||||
fn encode_size(mut raw: i64) -> String {
|
||||
let mut encoded = String::new();
|
||||
for &(c, n) in &MULTIPLIERS {
|
||||
if raw >= 1i64<<n {
|
||||
write!(&mut encoded, "{}{} ", raw >> n, c).unwrap();
|
||||
raw &= (1i64 << n) - 1;
|
||||
}
|
||||
}
|
||||
if raw > 0 || encoded.len() == 0 {
|
||||
write!(&mut encoded, "{}", raw).unwrap();
|
||||
} else {
|
||||
encoded.pop(); // remove trailing space.
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
fn decode_size(encoded: &str) -> Result<i64, ()> {
|
||||
let mut decoded = 0i64;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"\s*([0-9]+)([TGMK])?,?\s*").unwrap();
|
||||
}
|
||||
let mut last_pos = 0;
|
||||
for cap in RE.captures_iter(encoded) {
|
||||
let whole_cap = cap.get(0).unwrap();
|
||||
if whole_cap.start() > last_pos {
|
||||
return Err(());
|
||||
}
|
||||
last_pos = whole_cap.end();
|
||||
let mut piece = i64::from_str(&cap[1]).map_err(|_| ())?;
|
||||
if let Some(m) = cap.get(2) {
|
||||
let m = m.as_str().as_bytes()[0] as char;
|
||||
for &(some_m, n) in &MULTIPLIERS {
|
||||
if some_m == m {
|
||||
piece *= 1i64<<n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
decoded += piece;
|
||||
}
|
||||
if last_pos < encoded.len() {
|
||||
return Err(());
|
||||
}
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
struct Args {
|
||||
flag_db_dir: String,
|
||||
flag_sample_file_dir: String,
|
||||
}
|
||||
|
||||
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, super::OpenMode::ReadWrite)?;
|
||||
let db = Arc::new(db::Database::new(conn)?);
|
||||
//let dir = Arc::new(dir::Fd::open(&args.flag_sample_file_dir)?);
|
||||
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone())?;
|
||||
|
||||
let mut siv = Cursive::new();
|
||||
//siv.add_global_callback('q', |s| s.quit());
|
||||
|
||||
siv.add_layer(views::Dialog::around(
|
||||
views::SelectView::<fn(&Arc<db::Database>, &Arc<dir::SampleFileDir>, &mut Cursive)>::new()
|
||||
.on_submit({
|
||||
let db = db.clone();
|
||||
let dir = dir.clone();
|
||||
move |siv, item| item(&db, &dir, siv)
|
||||
})
|
||||
.item("Edit cameras".to_string(), cameras::add_dialog)
|
||||
.item("Edit retention".to_string(), retention::add_dialog))
|
||||
.button("Quit", |siv| siv.quit())
|
||||
.title("Main menu"));
|
||||
|
||||
siv.run();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20);
|
||||
}
|
||||
}
|
265
src/cmds/config/retention.rs
Normal file
265
src/cmds/config/retention.rs
Normal file
@ -0,0 +1,265 @@
|
||||
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
||||
// Copyright (C) 2017 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/>.
|
||||
|
||||
extern crate cursive;
|
||||
|
||||
use self::cursive::Cursive;
|
||||
use self::cursive::traits::{Boxable, Identifiable};
|
||||
use self::cursive::views;
|
||||
use db;
|
||||
use dir;
|
||||
use error::Error;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use super::{decode_size, encode_size};
|
||||
|
||||
struct Camera {
|
||||
label: String,
|
||||
used: i64,
|
||||
retain: Option<i64>, // None if unparseable
|
||||
}
|
||||
|
||||
struct Model {
|
||||
db: Arc<db::Database>,
|
||||
dir: Arc<dir::SampleFileDir>,
|
||||
fs_capacity: i64,
|
||||
total_used: i64,
|
||||
total_retain: i64,
|
||||
errors: isize,
|
||||
cameras: BTreeMap<i32, Camera>,
|
||||
}
|
||||
|
||||
/// Updates the limits in the database. Doesn't delete excess data (if any).
|
||||
fn update_limits_inner(model: &Model) -> Result<(), Error> {
|
||||
let mut db = model.db.lock();
|
||||
let mut tx = db.tx()?;
|
||||
for (&id, camera) in &model.cameras {
|
||||
tx.update_retention(id, camera.retain.unwrap())?;
|
||||
}
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
fn update_limits(model: &Model, siv: &mut Cursive) {
|
||||
if let Err(e) = update_limits_inner(model) {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to update limits: {}", e))
|
||||
.dismiss_button("Back")
|
||||
.title("Error"));
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_limit(model: &RefCell<Model>, siv: &mut Cursive, id: i32, content: &str) {
|
||||
info!("on_edit called for id {}", id);
|
||||
let mut model = model.borrow_mut();
|
||||
let model: &mut Model = &mut *model;
|
||||
let camera = model.cameras.get_mut(&id).unwrap();
|
||||
let new_value = decode_size(content).ok();
|
||||
let delta = new_value.unwrap_or(0) - camera.retain.unwrap_or(0);
|
||||
let old_errors = model.errors;
|
||||
if delta != 0 {
|
||||
let prev_over = model.total_retain > model.fs_capacity;
|
||||
model.total_retain += delta;
|
||||
siv.find_id::<views::TextView>("total_retain")
|
||||
.unwrap()
|
||||
.set_content(encode_size(model.total_retain));
|
||||
let now_over = model.total_retain > model.fs_capacity;
|
||||
info!("now_over: {}", now_over);
|
||||
if now_over != prev_over {
|
||||
model.errors += if now_over { 1 } else { -1 };
|
||||
siv.find_id::<views::TextView>("total_ok")
|
||||
.unwrap()
|
||||
.set_content(if now_over { "*" } else { " " });
|
||||
}
|
||||
}
|
||||
if new_value.is_none() != camera.retain.is_none() {
|
||||
model.errors += if new_value.is_none() { 1 } else { -1 };
|
||||
siv.find_id::<views::TextView>(&format!("{}_ok", id))
|
||||
.unwrap()
|
||||
.set_content(if new_value.is_none() { "*" } else { " " });
|
||||
}
|
||||
camera.retain = new_value;
|
||||
info!("model.errors = {}", model.errors);
|
||||
if (model.errors == 0) != (old_errors == 0) {
|
||||
info!("toggling change state: errors={}", model.errors);
|
||||
siv.find_id::<views::Button>("change")
|
||||
.unwrap()
|
||||
.set_enabled(model.errors == 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_deletion(model: &RefCell<Model>, siv: &mut Cursive, to_delete: i64) {
|
||||
let typed = siv.find_id::<views::EditView>("confirm")
|
||||
.unwrap()
|
||||
.get_content();
|
||||
info!("confirm, typed: {} vs expected: {}", typed.as_str(), to_delete);
|
||||
if decode_size(typed.as_str()).ok() == Some(to_delete) {
|
||||
actually_delete(model, siv);
|
||||
} else {
|
||||
siv.add_layer(views::Dialog::text("Please confirm amount.")
|
||||
.title("Try again")
|
||||
.dismiss_button("Back"));
|
||||
}
|
||||
}
|
||||
|
||||
fn actually_delete(model: &RefCell<Model>, siv: &mut Cursive) {
|
||||
let model = &*model.borrow();
|
||||
let new_limits: Vec<_> =
|
||||
model.cameras.iter()
|
||||
.map(|(&id, c)| dir::NewLimit{camera_id: id, limit: c.retain.unwrap()})
|
||||
.collect();
|
||||
siv.pop_layer(); // deletion confirmation
|
||||
siv.pop_layer(); // retention dialog
|
||||
if let Err(e) = dir::lower_retention(model.dir.clone(), &new_limits[..]) {
|
||||
siv.add_layer(views::Dialog::text(format!("Unable to delete excess video: {}", e))
|
||||
.title("Error")
|
||||
.dismiss_button("Abort"));
|
||||
} else {
|
||||
update_limits(model, siv);
|
||||
}
|
||||
}
|
||||
|
||||
fn press_change(model: &Rc<RefCell<Model>>, siv: &mut Cursive) {
|
||||
if model.borrow().errors > 0 {
|
||||
return;
|
||||
}
|
||||
let to_delete = model.borrow().cameras.values().map(
|
||||
|c| ::std::cmp::max(c.used - c.retain.unwrap(), 0)).sum();
|
||||
info!("change press, to_delete={}", to_delete);
|
||||
if to_delete > 0 {
|
||||
let prompt = format!("Some cameras' usage exceeds new limit. Please confirm the amount \
|
||||
of data to delete by typing it back:\n\n{}", encode_size(to_delete));
|
||||
let dialog = views::Dialog::around(
|
||||
views::LinearLayout::vertical()
|
||||
.child(views::TextView::new(prompt))
|
||||
.child(views::DummyView)
|
||||
.child(views::EditView::new().on_submit({
|
||||
let model = model.clone();
|
||||
move |siv, _| confirm_deletion(&model, siv, to_delete)
|
||||
}).with_id("confirm")))
|
||||
.button("Confirm", {
|
||||
let model = model.clone();
|
||||
move |siv| confirm_deletion(&model, siv, to_delete)
|
||||
})
|
||||
.dismiss_button("Cancel")
|
||||
.title("Confirm deletion");
|
||||
siv.add_layer(dialog);
|
||||
} else {
|
||||
siv.screen_mut().pop_layer();
|
||||
update_limits(&model.borrow(), siv);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_dialog(db: &Arc<db::Database>, dir: &Arc<dir::SampleFileDir>, siv: &mut Cursive) {
|
||||
let model = {
|
||||
let mut cameras = BTreeMap::new();
|
||||
let mut total_used = 0;
|
||||
let mut total_retain = 0;
|
||||
{
|
||||
let db = db.lock();
|
||||
for (&id, camera) in db.cameras_by_id() {
|
||||
cameras.insert(id, Camera{
|
||||
label: format!("{}: {}", id, camera.short_name),
|
||||
used: camera.sample_file_bytes,
|
||||
retain: Some(camera.retain_bytes),
|
||||
});
|
||||
total_used += camera.sample_file_bytes;
|
||||
total_retain += camera.retain_bytes;
|
||||
}
|
||||
}
|
||||
let stat = dir.statfs().unwrap();
|
||||
let fs_capacity = (stat.f_bsize * (stat.f_blocks - stat.f_bfree + stat.f_bavail)) as i64 -
|
||||
total_used;
|
||||
Rc::new(RefCell::new(Model{
|
||||
dir: dir.clone(),
|
||||
db: db.clone(),
|
||||
fs_capacity: fs_capacity,
|
||||
total_used: total_used,
|
||||
total_retain: total_retain,
|
||||
errors: (total_retain > fs_capacity) as isize,
|
||||
cameras: cameras,
|
||||
}))
|
||||
};
|
||||
|
||||
let mut list = views::ListView::new();
|
||||
list.add_child(
|
||||
"camera",
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new("usage").fixed_width(25))
|
||||
.child(views::TextView::new("limit").fixed_width(25)));
|
||||
for (&id, camera) in &model.borrow().cameras {
|
||||
list.add_child(
|
||||
&camera.label,
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new(encode_size(camera.used)).fixed_width(25))
|
||||
.child(views::EditView::new()
|
||||
.content(encode_size(camera.retain.unwrap()))
|
||||
.on_edit({
|
||||
let model = model.clone();
|
||||
move |siv, content, _pos| edit_limit(&model, siv, id, content)
|
||||
})
|
||||
.on_submit({
|
||||
let model = model.clone();
|
||||
move |siv, _| press_change(&model, siv)
|
||||
})
|
||||
.fixed_width(25))
|
||||
.child(views::TextView::new("").with_id(&format!("{}_ok", id)).fixed_width(1)));
|
||||
}
|
||||
let over = model.borrow().total_retain > model.borrow().fs_capacity;
|
||||
list.add_child(
|
||||
"total",
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new(encode_size(model.borrow().total_used)).fixed_width(25))
|
||||
.child(views::TextView::new(encode_size(model.borrow().total_retain))
|
||||
.with_id("total_retain").fixed_width(25))
|
||||
.child(views::TextView::new(if over { "*" } else { " " }).with_id("total_ok")));
|
||||
list.add_child(
|
||||
"filesystem",
|
||||
views::LinearLayout::horizontal()
|
||||
.child(views::TextView::new("").fixed_width(25))
|
||||
.child(views::TextView::new(encode_size(model.borrow().fs_capacity)).fixed_width(25)));
|
||||
let mut change_button = views::Button::new("Change", {
|
||||
let model = model.clone();
|
||||
move |siv| press_change(&model, siv)
|
||||
});
|
||||
change_button.set_enabled(!over);
|
||||
let mut buttons = views::LinearLayout::horizontal()
|
||||
.child(views::DummyView.full_width());
|
||||
buttons.add_child(change_button.with_id("change"));
|
||||
buttons.add_child(views::DummyView);
|
||||
buttons.add_child(views::Button::new("Cancel", |siv| siv.screen_mut().pop_layer()));
|
||||
siv.add_layer(
|
||||
views::Dialog::around(
|
||||
views::LinearLayout::vertical()
|
||||
.child(list)
|
||||
.child(views::DummyView)
|
||||
.child(buttons))
|
||||
.title("Edit retention"));
|
||||
}
|
@ -40,6 +40,7 @@ use slog_term;
|
||||
use std::path::Path;
|
||||
|
||||
mod check;
|
||||
mod config;
|
||||
mod init;
|
||||
mod run;
|
||||
mod ts;
|
||||
@ -48,6 +49,7 @@ mod upgrade;
|
||||
#[derive(Debug, RustcDecodable)]
|
||||
pub enum Command {
|
||||
Check,
|
||||
Config,
|
||||
Init,
|
||||
Run,
|
||||
Ts,
|
||||
@ -58,6 +60,7 @@ impl Command {
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
match *self {
|
||||
Command::Check => check::run(),
|
||||
Command::Config => config::run(),
|
||||
Command::Init => init::run(),
|
||||
Command::Run => run::run(),
|
||||
Command::Ts => ts::run(),
|
||||
@ -71,7 +74,7 @@ impl Command {
|
||||
/// 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_term::StreamerBuilder::new().stderr();
|
||||
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();
|
||||
}
|
||||
|
135
src/db.rs
135
src/db.rs
@ -342,6 +342,18 @@ pub struct Camera {
|
||||
next_recording_id: i32,
|
||||
}
|
||||
|
||||
/// Information about a camera, used by `add_camera` and `update_camera`.
|
||||
#[derive(Debug)]
|
||||
pub struct CameraChange {
|
||||
pub short_name: String,
|
||||
pub description: String,
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub main_rtsp_path: String,
|
||||
pub sub_rtsp_path: String,
|
||||
}
|
||||
|
||||
/// Adds `delta` to the day represented by `day` in the map `m`.
|
||||
/// Inserts a map entry if absent; removes the entry if it has 0 entries on exit.
|
||||
fn adjust_day(day: CameraDayKey, delta: CameraDayValue,
|
||||
@ -527,6 +539,9 @@ struct CameraModification {
|
||||
|
||||
/// Reset the next_recording_id to the specified value.
|
||||
new_next_recording_id: Option<i32>,
|
||||
|
||||
/// Reset the retain_bytes to the specified value.
|
||||
new_retain_bytes: Option<i64>,
|
||||
}
|
||||
|
||||
fn composite_id(camera_id: i32, recording_id: i32) -> i64 {
|
||||
@ -667,6 +682,26 @@ impl<'a> Transaction<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the `retain_bytes` for the given camera to the specified limit.
|
||||
/// Note this just resets the limit in the database; it's the caller's responsibility to ensure
|
||||
/// current usage is under the new limit if desired.
|
||||
pub fn update_retention(&mut self, camera_id: i32, new_limit: i64) -> Result<(), Error> {
|
||||
if new_limit < 0 {
|
||||
return Err(Error::new(format!("can't set limit for camera {} to {}; must be >= 0",
|
||||
camera_id, new_limit)));
|
||||
}
|
||||
self.check_must_rollback()?;
|
||||
let mut stmt =
|
||||
self.tx.prepare_cached("update camera set retain_bytes = :retain where id = :id")?;
|
||||
let changes = stmt.execute_named(&[(":retain", &new_limit), (":id", &camera_id)])?;
|
||||
if changes != 1 {
|
||||
return Err(Error::new(format!("no such camera {}", camera_id)));
|
||||
}
|
||||
let mut m = Transaction::get_mods_by_camera(&mut self.mods_by_camera, camera_id);
|
||||
m.new_retain_bytes = Some(new_limit);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commits these changes, consuming the Transaction.
|
||||
pub fn commit(mut self) -> Result<(), Error> {
|
||||
self.check_must_rollback()?;
|
||||
@ -684,6 +719,9 @@ impl<'a> Transaction<'a> {
|
||||
if let Some(id) = m.new_next_recording_id {
|
||||
camera.next_recording_id = id;
|
||||
}
|
||||
if let Some(b) = m.new_retain_bytes {
|
||||
camera.retain_bytes = b;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -706,6 +744,7 @@ impl<'a> Transaction<'a> {
|
||||
range: None,
|
||||
days: BTreeMap::new(),
|
||||
new_next_recording_id: None,
|
||||
new_retain_bytes: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1103,6 +1142,102 @@ impl LockedDatabase {
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Adds a camera.
|
||||
pub fn add_camera(&mut self, camera: CameraChange) -> Result<i32, Error> {
|
||||
let uuid = Uuid::new_v4();
|
||||
let uuid_bytes = &uuid.as_bytes()[..];
|
||||
let mut stmt = self.conn.prepare_cached(r#"
|
||||
insert into camera (uuid, short_name, description, host, username, password,
|
||||
main_rtsp_path, sub_rtsp_path, retain_bytes, next_recording_id)
|
||||
values (:uuid, :short_name, :description, :host, :username, :password,
|
||||
:main_rtsp_path, :sub_rtsp_path, 0, 1)
|
||||
"#)?;
|
||||
stmt.execute_named(&[
|
||||
(":uuid", &uuid_bytes),
|
||||
(":short_name", &camera.short_name),
|
||||
(":description", &camera.description),
|
||||
(":host", &camera.host),
|
||||
(":username", &camera.username),
|
||||
(":password", &camera.password),
|
||||
(":main_rtsp_path", &camera.main_rtsp_path),
|
||||
(":sub_rtsp_path", &camera.sub_rtsp_path),
|
||||
])?;
|
||||
let id = self.conn.last_insert_rowid() as i32;
|
||||
self.state.cameras_by_id.insert(id, Camera{
|
||||
id: id,
|
||||
uuid: uuid,
|
||||
short_name: camera.short_name,
|
||||
description: camera.description,
|
||||
host: camera.host,
|
||||
username: camera.username,
|
||||
password: camera.password,
|
||||
main_rtsp_path: camera.main_rtsp_path,
|
||||
sub_rtsp_path: camera.sub_rtsp_path,
|
||||
retain_bytes: 0,
|
||||
range: None,
|
||||
sample_file_bytes: 0,
|
||||
duration: recording::Duration(0),
|
||||
days: BTreeMap::new(),
|
||||
next_recording_id: 1,
|
||||
});
|
||||
self.state.cameras_by_uuid.insert(uuid, id);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Updates a camera.
|
||||
pub fn update_camera(&mut self, id: i32, camera: CameraChange) -> Result<(), Error> {
|
||||
let mut stmt = self.conn.prepare_cached(r#"
|
||||
update camera set
|
||||
short_name = :short_name,
|
||||
description = :description,
|
||||
host = :host,
|
||||
username = :username,
|
||||
password = :password,
|
||||
main_rtsp_path = :main_rtsp_path,
|
||||
sub_rtsp_path = :sub_rtsp_path
|
||||
where
|
||||
id = :id
|
||||
"#)?;
|
||||
stmt.execute_named(&[
|
||||
(":id", &id),
|
||||
(":short_name", &camera.short_name),
|
||||
(":description", &camera.description),
|
||||
(":host", &camera.host),
|
||||
(":username", &camera.username),
|
||||
(":password", &camera.password),
|
||||
(":main_rtsp_path", &camera.main_rtsp_path),
|
||||
(":sub_rtsp_path", &camera.sub_rtsp_path),
|
||||
])?;
|
||||
let c = self.state.cameras_by_id.get_mut(&id).unwrap();
|
||||
c.short_name = camera.short_name;
|
||||
c.description = camera.description;
|
||||
c.host = camera.host;
|
||||
c.username = camera.username;
|
||||
c.password = camera.password;
|
||||
c.main_rtsp_path = camera.main_rtsp_path;
|
||||
c.sub_rtsp_path = camera.sub_rtsp_path;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a camera. The camera must have no recordings.
|
||||
pub fn delete_camera(&mut self, id: i32) -> Result<(), Error> {
|
||||
let (has_recordings, uuid) =
|
||||
self.state.cameras_by_id.get(&id)
|
||||
.map(|c| (c.range.is_some(), c.uuid))
|
||||
.ok_or_else(|| Error::new(format!("No such camera {} to remove", id)))?;
|
||||
if has_recordings {
|
||||
return Err(Error::new(format!("Can't remove camera {}; has recordings.", id)));
|
||||
};
|
||||
let mut stmt = self.conn.prepare_cached(r"delete from camera where id = :id")?;
|
||||
let rows = stmt.execute_named(&[(":id", &id)])?;
|
||||
if rows != 1 {
|
||||
return Err(Error::new(format!("Camera {} missing from database", id)));
|
||||
}
|
||||
self.state.cameras_by_id.remove(&id);
|
||||
self.state.cameras_by_uuid.remove(&uuid);
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the schema version from the given database connection.
|
||||
|
136
src/dir.rs
136
src/dir.rs
@ -43,7 +43,7 @@ use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::mem;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use uuid::Uuid;
|
||||
@ -98,6 +98,16 @@ impl Fd {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn statfs(&self) -> Result<libc::statvfs, io::Error> {
|
||||
unsafe {
|
||||
let mut stat: libc::statvfs = mem::zeroed();
|
||||
if libc::fstatvfs(self.0, &mut stat) < 0 {
|
||||
return Err(io::Error::last_os_error())
|
||||
}
|
||||
Ok(stat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SampleFileDir {
|
||||
@ -153,6 +163,8 @@ impl SampleFileDir {
|
||||
Writer::open(f, uuid, prev, camera_id, video_sample_entry_id, channel)
|
||||
}
|
||||
|
||||
pub fn statfs(&self) -> Result<libc::statvfs, io::Error> { self.fd.statfs() }
|
||||
|
||||
/// Opens a sample file within this directory with the given flags and (if creating) mode.
|
||||
fn open_int(&self, uuid: Uuid, flags: libc::c_int, mode: libc::c_int)
|
||||
-> Result<fs::File, io::Error> {
|
||||
@ -215,11 +227,10 @@ enum SyncerCommand {
|
||||
pub struct SyncerChannel(mpsc::Sender<SyncerCommand>);
|
||||
|
||||
/// State of the worker thread.
|
||||
struct SyncerState {
|
||||
struct Syncer {
|
||||
dir: Arc<SampleFileDir>,
|
||||
to_unlink: Vec<Uuid>,
|
||||
to_mark_deleted: Vec<Uuid>,
|
||||
cmds: mpsc::Receiver<SyncerCommand>,
|
||||
}
|
||||
|
||||
/// Starts a syncer for the given sample file directory.
|
||||
@ -234,15 +245,71 @@ pub fn start_syncer(dir: Arc<SampleFileDir>)
|
||||
-> Result<(SyncerChannel, thread::JoinHandle<()>), Error> {
|
||||
let to_unlink = dir.db.lock().list_reserved_sample_files()?;
|
||||
let (snd, rcv) = mpsc::channel();
|
||||
let mut state = SyncerState {
|
||||
let mut syncer = Syncer {
|
||||
dir: dir,
|
||||
to_unlink: to_unlink,
|
||||
to_mark_deleted: Vec::new(),
|
||||
cmds: rcv,
|
||||
};
|
||||
state.initial_rotation()?;
|
||||
syncer.initial_rotation()?;
|
||||
Ok((SyncerChannel(snd),
|
||||
thread::Builder::new().name("syncer".into()).spawn(move || state.run()).unwrap()))
|
||||
thread::Builder::new().name("syncer".into()).spawn(move || syncer.run(rcv)).unwrap()))
|
||||
}
|
||||
|
||||
pub struct NewLimit {
|
||||
pub camera_id: i32,
|
||||
pub limit: i64,
|
||||
}
|
||||
|
||||
/// Deletes recordings if necessary to fit within the given new `retain_bytes` limit.
|
||||
/// Note this doesn't change the limit in the database; it only deletes files.
|
||||
/// Pass a limit of 0 to delete all recordings associated with a camera.
|
||||
pub fn lower_retention(dir: Arc<SampleFileDir>, limits: &[NewLimit]) -> Result<(), Error> {
|
||||
let to_unlink = dir.db.lock().list_reserved_sample_files()?;
|
||||
let mut syncer = Syncer {
|
||||
dir: dir,
|
||||
to_unlink: to_unlink,
|
||||
to_mark_deleted: Vec::new(),
|
||||
};
|
||||
syncer.do_rotation(|db| {
|
||||
let mut to_delete = Vec::new();
|
||||
for l in limits {
|
||||
let before = to_delete.len();
|
||||
let camera = db.cameras_by_id().get(&l.camera_id)
|
||||
.ok_or_else(|| Error::new(format!("no such camera {}", l.camera_id)))?;
|
||||
if l.limit >= camera.sample_file_bytes { continue }
|
||||
get_rows_to_delete(db, l.camera_id, camera, camera.retain_bytes - l.limit,
|
||||
&mut to_delete)?;
|
||||
info!("camera {}, {}->{}, deleting {} rows", camera.short_name,
|
||||
camera.sample_file_bytes, l.limit, to_delete.len() - before);
|
||||
}
|
||||
Ok(to_delete)
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets rows to delete to bring a camera's disk usage within bounds.
|
||||
fn get_rows_to_delete(db: &db::LockedDatabase, camera_id: i32,
|
||||
camera: &db::Camera, extra_bytes_needed: i64,
|
||||
to_delete: &mut Vec<db::ListOldestSampleFilesRow>) -> Result<(), Error> {
|
||||
let bytes_needed = camera.sample_file_bytes + extra_bytes_needed - camera.retain_bytes;
|
||||
let mut bytes_to_delete = 0;
|
||||
if bytes_needed <= 0 {
|
||||
debug!("{}: have remaining quota of {}", camera.short_name, -bytes_needed);
|
||||
return Ok(());
|
||||
}
|
||||
let mut n = 0;
|
||||
db.list_oldest_sample_files(camera_id, |row| {
|
||||
bytes_to_delete += row.sample_file_bytes as i64;
|
||||
to_delete.push(row);
|
||||
n += 1;
|
||||
bytes_needed > bytes_to_delete // continue as long as more deletions are needed.
|
||||
})?;
|
||||
if bytes_needed > bytes_to_delete {
|
||||
return Err(Error::new(format!("{}: couldn't find enough files to delete: {} left.",
|
||||
camera.short_name, bytes_needed)));
|
||||
}
|
||||
info!("{}: deleting {} bytes in {} recordings ({} bytes needed)",
|
||||
camera.short_name, bytes_to_delete, n, bytes_needed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl SyncerChannel {
|
||||
@ -265,10 +332,10 @@ impl SyncerChannel {
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncerState {
|
||||
fn run(&mut self) {
|
||||
impl Syncer {
|
||||
fn run(&mut self, cmds: mpsc::Receiver<SyncerCommand>) {
|
||||
loop {
|
||||
match self.cmds.recv() {
|
||||
match cmds.recv() {
|
||||
Err(_) => return, // all senders have closed the channel; shutdown
|
||||
Ok(SyncerCommand::AsyncSaveRecording(recording, f)) => self.save(recording, f),
|
||||
Ok(SyncerCommand::AsyncAbandonRecording(uuid)) => self.abandon(uuid),
|
||||
@ -280,16 +347,25 @@ impl SyncerState {
|
||||
|
||||
/// Rotates files for all cameras and deletes stale reserved uuids from previous runs.
|
||||
fn initial_rotation(&mut self) -> Result<(), Error> {
|
||||
let mut to_delete = Vec::new();
|
||||
{
|
||||
let mut db = self.dir.db.lock();
|
||||
self.do_rotation(|db| {
|
||||
let mut to_delete = Vec::new();
|
||||
for (camera_id, camera) in db.cameras_by_id() {
|
||||
self.get_rows_to_delete(&db, *camera_id, camera, 0, &mut to_delete)?;
|
||||
get_rows_to_delete(&db, *camera_id, camera, 0, &mut to_delete)?;
|
||||
}
|
||||
Ok(to_delete)
|
||||
})
|
||||
}
|
||||
|
||||
fn do_rotation<F>(&mut self, get_rows_to_delete: F) -> Result<(), Error>
|
||||
where F: FnOnce(&db::LockedDatabase) -> Result<Vec<db::ListOldestSampleFilesRow>, Error> {
|
||||
let to_delete = {
|
||||
let mut db = self.dir.db.lock();
|
||||
let to_delete = get_rows_to_delete(&*db)?;
|
||||
let mut tx = db.tx()?;
|
||||
tx.delete_recordings(&to_delete)?;
|
||||
tx.commit()?;
|
||||
}
|
||||
to_delete
|
||||
};
|
||||
for row in to_delete {
|
||||
self.to_unlink.push(row.uuid);
|
||||
}
|
||||
@ -345,8 +421,8 @@ impl SyncerState {
|
||||
let camera =
|
||||
db.cameras_by_id().get(&recording.camera_id)
|
||||
.ok_or_else(|| Error::new(format!("no such camera {}", recording.camera_id)))?;
|
||||
self.get_rows_to_delete(&db, recording.camera_id, camera,
|
||||
recording.sample_file_bytes as i64, &mut to_delete)?;
|
||||
get_rows_to_delete(&db, recording.camera_id, camera,
|
||||
recording.sample_file_bytes as i64, &mut to_delete)?;
|
||||
}
|
||||
let mut tx = db.tx()?;
|
||||
tx.mark_sample_files_deleted(&self.to_mark_deleted)?;
|
||||
@ -363,32 +439,6 @@ impl SyncerState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets rows to delete to bring a camera's disk usage within bounds.
|
||||
fn get_rows_to_delete(&self, db: &MutexGuard<db::LockedDatabase>, camera_id: i32,
|
||||
camera: &db::Camera, extra_bytes_needed: i64,
|
||||
to_delete: &mut Vec<db::ListOldestSampleFilesRow>) -> Result<(), Error> {
|
||||
let bytes_needed = camera.sample_file_bytes + extra_bytes_needed - camera.retain_bytes;
|
||||
let mut bytes_to_delete = 0;
|
||||
if bytes_needed <= 0 {
|
||||
debug!("{}: have remaining quota of {}", camera.short_name, -bytes_needed);
|
||||
return Ok(());
|
||||
}
|
||||
let mut n = 0;
|
||||
db.list_oldest_sample_files(camera_id, |row| {
|
||||
bytes_to_delete += row.sample_file_bytes as i64;
|
||||
to_delete.push(row);
|
||||
n += 1;
|
||||
bytes_needed > bytes_to_delete // continue as long as more deletions are needed.
|
||||
})?;
|
||||
if bytes_needed > bytes_to_delete {
|
||||
return Err(Error::new(format!("{}: couldn't find enough files to delete: {} left.",
|
||||
camera.short_name, bytes_needed)));
|
||||
}
|
||||
info!("{}: deleting {} bytes in {} recordings ({} bytes needed)",
|
||||
camera.short_name, bytes_to_delete, n, bytes_needed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to unlink all the uuids in `self.to_unlink`. Any which can't be unlinked will
|
||||
/// be retained in the vec.
|
||||
fn try_unlink(&mut self) {
|
||||
|
@ -93,6 +93,7 @@ Commands:
|
||||
check Check database integrity
|
||||
init Initialize a database
|
||||
run Run the daemon: record from cameras and handle HTTP requests
|
||||
shell Start an interactive shell to modify the database
|
||||
ts Translate between human-readable and numeric timestamps
|
||||
upgrade Upgrade the database to the latest schema
|
||||
";
|
||||
|
Loading…
x
Reference in New Issue
Block a user