mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-04-23 03:55:40 -04: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)",
|
"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 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)",
|
"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)",
|
"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 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)",
|
"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)",
|
"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]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -87,6 +96,17 @@ dependencies = [
|
|||||||
"rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "chan-signal"
|
name = "chan-signal"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -112,6 +132,21 @@ name = "crossbeam"
|
|||||||
version = "0.2.10"
|
version = "0.2.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "docopt"
|
name = "docopt"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -254,6 +289,11 @@ name = "language-tags"
|
|||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@ -342,16 +382,48 @@ dependencies = [
|
|||||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
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-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-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)",
|
"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]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.32"
|
version = "0.1.32"
|
||||||
@ -369,6 +441,17 @@ dependencies = [
|
|||||||
"num-traits 0.1.36 (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-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]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
@ -390,6 +473,11 @@ dependencies = [
|
|||||||
"libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
@ -726,6 +814,9 @@ dependencies = [
|
|||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "traitobject"
|
name = "traitobject"
|
||||||
@ -758,6 +849,16 @@ name = "unicode-normalization"
|
|||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
@ -826,15 +927,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
[metadata]
|
[metadata]
|
||||||
"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
|
"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 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-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 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.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 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 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 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 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 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 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 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 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"
|
"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 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 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 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 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 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"
|
"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 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 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 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 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-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-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-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 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 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 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 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"
|
"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 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-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-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 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 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"
|
"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]
|
[dev-dependencies]
|
||||||
tempdir = "0.3"
|
tempdir = "0.3"
|
||||||
|
|
||||||
|
[dependencies.cursive]
|
||||||
|
version = "0.4"
|
||||||
|
#default-features = false
|
||||||
|
#features = ["termion"]
|
||||||
|
|
||||||
[dependencies.ffmpeg]
|
[dependencies.ffmpeg]
|
||||||
git = "https://github.com/scottlamb/rust-ffmpeg"
|
git = "https://github.com/scottlamb/rust-ffmpeg"
|
||||||
branch = "2.x"
|
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/).
|
* [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
|
On Ubuntu 16.04.1 LTS or Raspbian Jessie, the following command will install
|
||||||
all non-Rust dependencies:
|
all non-Rust dependencies:
|
||||||
|
|
||||||
@ -97,13 +100,8 @@ all non-Rust dependencies:
|
|||||||
libavcodec-dev \
|
libavcodec-dev \
|
||||||
libavformat-dev \
|
libavformat-dev \
|
||||||
libavutil-dev \
|
libavutil-dev \
|
||||||
sqlite3 \
|
libncursesw-dev \
|
||||||
libsqlite3-dev \
|
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.
|
|
||||||
|
|
||||||
Next, you need Rust and Cargo. The easiest way to install them is by following
|
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
|
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:
|
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
|
* `-S`: Skip updating and installing dependencies through apt-get. This too can be
|
||||||
useful on repeated builds.
|
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
|
SAMPLES_DIR=/media/nvr/samples
|
||||||
|
|
||||||
The script will perform all necessary steps to leave you with a fully built,
|
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.
|
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`
|
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
|
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
|
$ sudo mount /var/lib/moonfire-nvr/sample
|
||||||
|
|
||||||
Once setup is complete, it is time to add camera configurations to the
|
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,
|
database. If the daemon is running, you will need to stop it temporarily:
|
||||||
so you will have to manually insert cameras configurations with the `sqlite3`
|
|
||||||
command line tool prior to starting Moonfire NVR.
|
|
||||||
|
|
||||||
Before setting up a camera, it may be helpful to test settings with the
|
$ sudo systemctl stop moonfire-nvr
|
||||||
`ffmpeg` command line tool:
|
|
||||||
|
|
||||||
$ ffmpeg \
|
You can configure the system through a text-based user interface:
|
||||||
-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
|
|
||||||
|
|
||||||
Once you have a working `ffmpeg` command line, insert the camera config as
|
$ sudo -u moonfire-nvr moonfire-nvr config
|
||||||
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%).
|
|
||||||
|
|
||||||
In the following example, we generate a uuid which is then later used
|
In the user interface, add your cameras under the "Edit cameras" dialog.
|
||||||
to uniquely identify this camera. Thus, you will generate a new one for
|
There's a "Test" button to verify your settings directly from the dialog.
|
||||||
each camera you insert using this method.
|
|
||||||
|
|
||||||
$ uuidgen | sed -e 's/-//g'
|
After the cameras look correct, go to "Edit retention" to assign disk space to
|
||||||
b47f48706d91414591cd6c931bf836b4
|
each camera. Leave a little slack (at least 100 MB per camera) between the total
|
||||||
$ sudo -u moonfire-nvr sqlite3 ~moonfire-nvr/db/db
|
limit and the filesystem capacity, even if you store nothing else on the disk.
|
||||||
sqlite3> insert into camera (
|
There are several reasons this is needed:
|
||||||
...> 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
|
|
||||||
|
|
||||||
### 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
|
When finished, start the daemon:
|
||||||
`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):
|
|
||||||
|
|
||||||
insert into camera (
|
$ sudo systemctl start moonfire-nvr
|
||||||
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.
|
|
||||||
|
|
||||||
## System Service
|
## System Service
|
||||||
|
|
||||||
@ -290,9 +240,9 @@ directly exposed to the Internet.
|
|||||||
Complete the installation through `systemctl` commands:
|
Complete the installation through `systemctl` commands:
|
||||||
|
|
||||||
$ sudo systemctl daemon-reload
|
$ sudo systemctl daemon-reload
|
||||||
$ sudo systemctl start moonfire-nvr.service
|
$ sudo systemctl start moonfire-nvr
|
||||||
$ sudo systemctl status moonfire-nvr.service
|
$ sudo systemctl status moonfire-nvr
|
||||||
$ sudo systemctl enable moonfire-nvr.service
|
$ sudo systemctl enable moonfire-nvr
|
||||||
|
|
||||||
See the [systemd](http://www.freedesktop.org/wiki/Software/systemd/)
|
See the [systemd](http://www.freedesktop.org/wiki/Software/systemd/)
|
||||||
documentation for more information. The [manual
|
documentation for more information. The [manual
|
||||||
|
10
prep.sh
10
prep.sh
@ -34,7 +34,6 @@
|
|||||||
# Script to prepare for moonfire-nvr operations
|
# Script to prepare for moonfire-nvr operations
|
||||||
#
|
#
|
||||||
# Command line options:
|
# Command line options:
|
||||||
# -D: Skip database initialization
|
|
||||||
# -S: Skip apt-get update and install
|
# -S: Skip apt-get update and install
|
||||||
#
|
#
|
||||||
|
|
||||||
@ -106,7 +105,6 @@ NVR_GROUP="${NVR_GROUP:-$NVR_USER}"
|
|||||||
NVR_PORT="${NVR_PORT:-8080}"
|
NVR_PORT="${NVR_PORT:-8080}"
|
||||||
NVR_HOME_BASE="${NVR_HOME_BASE:-/var/lib}"
|
NVR_HOME_BASE="${NVR_HOME_BASE:-/var/lib}"
|
||||||
NVR_HOME="${NVR_HOME_BASE}/${NVR_USER}"
|
NVR_HOME="${NVR_HOME_BASE}/${NVR_USER}"
|
||||||
DB_NAME="${DB_NAME:-db}"
|
|
||||||
DB_DIR="${DB_DIR:-$NVR_HOME/db}"
|
DB_DIR="${DB_DIR:-$NVR_HOME/db}"
|
||||||
SAMPLES_DIR_NAME="${SAMPLES_DIR_NAME:-samples}"
|
SAMPLES_DIR_NAME="${SAMPLES_DIR_NAME:-samples}"
|
||||||
SAMPLES_DIR="${SAMPLES_DIR:-$NVR_HOME/$SAMPLES_DIR_NAME}"
|
SAMPLES_DIR="${SAMPLES_DIR:-$NVR_HOME/$SAMPLES_DIR_NAME}"
|
||||||
@ -214,13 +212,7 @@ echo 'Create database...'; echo
|
|||||||
if [ ! -d "${DB_DIR}" ]; then
|
if [ ! -d "${DB_DIR}" ]; then
|
||||||
sudo -u ${NVR_USER} -H mkdir "${DB_DIR}"
|
sudo -u ${NVR_USER} -H mkdir "${DB_DIR}"
|
||||||
fi
|
fi
|
||||||
DB_PATH="${DB_DIR}/${DB_NAME}"
|
sudo -u ${NVR_USER} -H ${SERVICE_BIN} init --db-dir="${DB_DIR}"
|
||||||
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
|
|
||||||
|
|
||||||
# Prepare service files
|
# 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;
|
use std::path::Path;
|
||||||
|
|
||||||
mod check;
|
mod check;
|
||||||
|
mod config;
|
||||||
mod init;
|
mod init;
|
||||||
mod run;
|
mod run;
|
||||||
mod ts;
|
mod ts;
|
||||||
@ -48,6 +49,7 @@ mod upgrade;
|
|||||||
#[derive(Debug, RustcDecodable)]
|
#[derive(Debug, RustcDecodable)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Check,
|
Check,
|
||||||
|
Config,
|
||||||
Init,
|
Init,
|
||||||
Run,
|
Run,
|
||||||
Ts,
|
Ts,
|
||||||
@ -58,6 +60,7 @@ impl Command {
|
|||||||
pub fn run(&self) -> Result<(), Error> {
|
pub fn run(&self) -> Result<(), Error> {
|
||||||
match *self {
|
match *self {
|
||||||
Command::Check => check::run(),
|
Command::Check => check::run(),
|
||||||
|
Command::Config => config::run(),
|
||||||
Command::Init => init::run(),
|
Command::Init => init::run(),
|
||||||
Command::Run => run::run(),
|
Command::Run => run::run(),
|
||||||
Command::Ts => ts::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
|
/// 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.
|
/// before the program exits, and partial output from these tools is very confusing.
|
||||||
fn install_logger(async: bool) {
|
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());
|
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();
|
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,
|
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`.
|
/// 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.
|
/// Inserts a map entry if absent; removes the entry if it has 0 entries on exit.
|
||||||
fn adjust_day(day: CameraDayKey, delta: CameraDayValue,
|
fn adjust_day(day: CameraDayKey, delta: CameraDayValue,
|
||||||
@ -527,6 +539,9 @@ struct CameraModification {
|
|||||||
|
|
||||||
/// Reset the next_recording_id to the specified value.
|
/// Reset the next_recording_id to the specified value.
|
||||||
new_next_recording_id: Option<i32>,
|
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 {
|
fn composite_id(camera_id: i32, recording_id: i32) -> i64 {
|
||||||
@ -667,6 +682,26 @@ impl<'a> Transaction<'a> {
|
|||||||
Ok(())
|
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.
|
/// Commits these changes, consuming the Transaction.
|
||||||
pub fn commit(mut self) -> Result<(), Error> {
|
pub fn commit(mut self) -> Result<(), Error> {
|
||||||
self.check_must_rollback()?;
|
self.check_must_rollback()?;
|
||||||
@ -684,6 +719,9 @@ impl<'a> Transaction<'a> {
|
|||||||
if let Some(id) = m.new_next_recording_id {
|
if let Some(id) = m.new_next_recording_id {
|
||||||
camera.next_recording_id = id;
|
camera.next_recording_id = id;
|
||||||
}
|
}
|
||||||
|
if let Some(b) = m.new_retain_bytes {
|
||||||
|
camera.retain_bytes = b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -706,6 +744,7 @@ impl<'a> Transaction<'a> {
|
|||||||
range: None,
|
range: None,
|
||||||
days: BTreeMap::new(),
|
days: BTreeMap::new(),
|
||||||
new_next_recording_id: None,
|
new_next_recording_id: None,
|
||||||
|
new_retain_bytes: None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1103,6 +1142,102 @@ impl LockedDatabase {
|
|||||||
|
|
||||||
Ok(id)
|
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.
|
/// Gets the schema version from the given database connection.
|
||||||
|
132
src/dir.rs
132
src/dir.rs
@ -43,7 +43,7 @@ use std::fs;
|
|||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::os::unix::io::FromRawFd;
|
use std::os::unix::io::FromRawFd;
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -98,6 +98,16 @@ impl Fd {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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 {
|
impl SampleFileDir {
|
||||||
@ -153,6 +163,8 @@ impl SampleFileDir {
|
|||||||
Writer::open(f, uuid, prev, camera_id, video_sample_entry_id, channel)
|
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.
|
/// 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)
|
fn open_int(&self, uuid: Uuid, flags: libc::c_int, mode: libc::c_int)
|
||||||
-> Result<fs::File, io::Error> {
|
-> Result<fs::File, io::Error> {
|
||||||
@ -215,11 +227,10 @@ enum SyncerCommand {
|
|||||||
pub struct SyncerChannel(mpsc::Sender<SyncerCommand>);
|
pub struct SyncerChannel(mpsc::Sender<SyncerCommand>);
|
||||||
|
|
||||||
/// State of the worker thread.
|
/// State of the worker thread.
|
||||||
struct SyncerState {
|
struct Syncer {
|
||||||
dir: Arc<SampleFileDir>,
|
dir: Arc<SampleFileDir>,
|
||||||
to_unlink: Vec<Uuid>,
|
to_unlink: Vec<Uuid>,
|
||||||
to_mark_deleted: Vec<Uuid>,
|
to_mark_deleted: Vec<Uuid>,
|
||||||
cmds: mpsc::Receiver<SyncerCommand>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts a syncer for the given sample file directory.
|
/// 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> {
|
-> Result<(SyncerChannel, thread::JoinHandle<()>), Error> {
|
||||||
let to_unlink = dir.db.lock().list_reserved_sample_files()?;
|
let to_unlink = dir.db.lock().list_reserved_sample_files()?;
|
||||||
let (snd, rcv) = mpsc::channel();
|
let (snd, rcv) = mpsc::channel();
|
||||||
let mut state = SyncerState {
|
let mut syncer = Syncer {
|
||||||
dir: dir,
|
dir: dir,
|
||||||
to_unlink: to_unlink,
|
to_unlink: to_unlink,
|
||||||
to_mark_deleted: Vec::new(),
|
to_mark_deleted: Vec::new(),
|
||||||
cmds: rcv,
|
|
||||||
};
|
};
|
||||||
state.initial_rotation()?;
|
syncer.initial_rotation()?;
|
||||||
Ok((SyncerChannel(snd),
|
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 {
|
impl SyncerChannel {
|
||||||
@ -265,10 +332,10 @@ impl SyncerChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyncerState {
|
impl Syncer {
|
||||||
fn run(&mut self) {
|
fn run(&mut self, cmds: mpsc::Receiver<SyncerCommand>) {
|
||||||
loop {
|
loop {
|
||||||
match self.cmds.recv() {
|
match cmds.recv() {
|
||||||
Err(_) => return, // all senders have closed the channel; shutdown
|
Err(_) => return, // all senders have closed the channel; shutdown
|
||||||
Ok(SyncerCommand::AsyncSaveRecording(recording, f)) => self.save(recording, f),
|
Ok(SyncerCommand::AsyncSaveRecording(recording, f)) => self.save(recording, f),
|
||||||
Ok(SyncerCommand::AsyncAbandonRecording(uuid)) => self.abandon(uuid),
|
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.
|
/// Rotates files for all cameras and deletes stale reserved uuids from previous runs.
|
||||||
fn initial_rotation(&mut self) -> Result<(), Error> {
|
fn initial_rotation(&mut self) -> Result<(), Error> {
|
||||||
|
self.do_rotation(|db| {
|
||||||
let mut to_delete = Vec::new();
|
let mut to_delete = Vec::new();
|
||||||
{
|
|
||||||
let mut db = self.dir.db.lock();
|
|
||||||
for (camera_id, camera) in db.cameras_by_id() {
|
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()?;
|
let mut tx = db.tx()?;
|
||||||
tx.delete_recordings(&to_delete)?;
|
tx.delete_recordings(&to_delete)?;
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
}
|
to_delete
|
||||||
|
};
|
||||||
for row in to_delete {
|
for row in to_delete {
|
||||||
self.to_unlink.push(row.uuid);
|
self.to_unlink.push(row.uuid);
|
||||||
}
|
}
|
||||||
@ -345,7 +421,7 @@ impl SyncerState {
|
|||||||
let camera =
|
let camera =
|
||||||
db.cameras_by_id().get(&recording.camera_id)
|
db.cameras_by_id().get(&recording.camera_id)
|
||||||
.ok_or_else(|| Error::new(format!("no such camera {}", 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,
|
get_rows_to_delete(&db, recording.camera_id, camera,
|
||||||
recording.sample_file_bytes as i64, &mut to_delete)?;
|
recording.sample_file_bytes as i64, &mut to_delete)?;
|
||||||
}
|
}
|
||||||
let mut tx = db.tx()?;
|
let mut tx = db.tx()?;
|
||||||
@ -363,32 +439,6 @@ impl SyncerState {
|
|||||||
Ok(())
|
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
|
/// Tries to unlink all the uuids in `self.to_unlink`. Any which can't be unlinked will
|
||||||
/// be retained in the vec.
|
/// be retained in the vec.
|
||||||
fn try_unlink(&mut self) {
|
fn try_unlink(&mut self) {
|
||||||
|
@ -93,6 +93,7 @@ Commands:
|
|||||||
check Check database integrity
|
check Check database integrity
|
||||||
init Initialize a database
|
init Initialize a database
|
||||||
run Run the daemon: record from cameras and handle HTTP requests
|
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
|
ts Translate between human-readable and numeric timestamps
|
||||||
upgrade Upgrade the database to the latest schema
|
upgrade Upgrade the database to the latest schema
|
||||||
";
|
";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user