diff --git a/Cargo.lock b/Cargo.lock index fe9e655..cb03ecb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,19 @@ dependencies = [ "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ansi_term" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "arc-swap" version = "0.3.11" @@ -40,6 +53,16 @@ dependencies = [ "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "atty" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.57 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "autocfg" version = "0.1.4" @@ -116,6 +139,17 @@ dependencies = [ "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bstr" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-automata 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "build_const" version = "0.2.1" @@ -161,6 +195,20 @@ dependencies = [ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -305,6 +353,26 @@ dependencies = [ "syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "csv" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bstr 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "csv-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cursive" version = "0.12.0" @@ -376,6 +444,16 @@ dependencies = [ "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.57 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "docopt" version = "1.1.0" @@ -397,6 +475,11 @@ name = "either" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "encode_unicode" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "encoding_rs" version = "0.8.17" @@ -601,6 +684,14 @@ name = "hashbrown" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hmac" version = "0.7.0" @@ -842,6 +933,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "memchr" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.57 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "memmap" @@ -973,6 +1067,7 @@ dependencies = [ "odds 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "prettydiff 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)", "protobuf-codegen-pure 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)", "regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1155,6 +1250,11 @@ dependencies = [ "libc 0.2.57 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "odds" version = "0.3.1" @@ -1308,6 +1408,29 @@ name = "pkg-config" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "prettydiff" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "prettytable-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -1518,6 +1641,25 @@ name = "redox_syscall" version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_users" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "reffers" version = "0.5.1" @@ -1538,6 +1680,14 @@ dependencies = [ "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex-automata" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex-syntax" version = "0.6.6" @@ -1658,6 +1808,11 @@ name = "ryu" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "ryu" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "safemem" version = "0.3.0" @@ -1872,11 +2027,36 @@ name = "strsim" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "strsim" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "structopt" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "structopt-derive" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "subtle" version = "1.0.0" @@ -1935,6 +2115,16 @@ dependencies = [ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "term" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "term_size" version = "0.3.1" @@ -1945,6 +2135,25 @@ dependencies = [ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "termion" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.57 (registry+https://github.com/rust-lang/crates.io-index)", + "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "thread_local" version = "0.3.6" @@ -2292,6 +2501,11 @@ name = "vcpkg" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "version_check" version = "0.1.5" @@ -2361,10 +2575,13 @@ dependencies = [ [metadata] "checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" "checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c" +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" "checksum arc-swap 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "bc4662175ead9cd84451d5c35070517777949a2ed84551764129cedb88384841" "checksum argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f67b0b6a86dae6e67ff4ca2b6201396074996379fba2b92ff649126f37cb392" "checksum array-macro 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7c4ff37a25fb442a1fecfd399be0dde685558bca30fb998420532889a36852d2" "checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71" +"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" "checksum autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0e49efa51329a5fd37e7c79db4621af617cd4e3e5bc224939808d076077077bf" "checksum backtrace 0.3.26 (registry+https://github.com/rust-lang/crates.io-index)" = "1a13fc43f04daf08ab4f71e3d27e1fc27fc437d3e95ac0063a796d92fb40f39b" "checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" @@ -2374,6 +2591,7 @@ dependencies = [ "checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" "checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" "checksum block-padding 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6d4dc3af3ee2e12f3e5d224e5e1e3d73668abbeb69e566d361f7d5563a4fdf09" +"checksum bstr 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc0572e02f76cb335f309b19e0a0d585b4f62788f7d26de2a13a836a637385f" "checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" "checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" "checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb" @@ -2381,6 +2599,7 @@ dependencies = [ "checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d" "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" "checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" +"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" "checksum cookie 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5" @@ -2397,15 +2616,19 @@ dependencies = [ "checksum crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" "checksum cstr 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "19f7a08ed4ecd7e077d4cee63937473e6f7cf57b702a9114ef41751b2cbc0f60" "checksum cstr-macros 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0f12dd847ec773fc98d75edba5394cb87d0f35e7ee548a4c81849ca6374b3d48" +"checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d" +"checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c" "checksum cursive 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b7ecc7282b5361471b607c26f44148205607e26d48a2fc65bd16e7619b1ebb78" "checksum darling 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fcfbcb0c5961907597a7d1148e3af036268f2b773886b8bb3eeb1e1281d3d3d6" "checksum darling_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6afc018370c3bff3eb51f89256a6bdb18b4fdcda72d577982a14954a7a0b402c" "checksum darling_macro 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c6d8dac1c6f1d29a41c4712b4400f878cb4fcc4c7628f298dd75038e024998d1" "checksum data-encoding 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4f47ca1860a761136924ddd2422ba77b2ea54fe8cc75b9040804a0d9d32ad97" "checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c" +"checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" "checksum docopt 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969" "checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" "checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b" +"checksum encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90b2c9496c001e8cb61827acdefad780795c42264c137744cae6f7d9e3450abd" "checksum encoding_rs 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)" = "4155785c79f2f6701f185eb2e6b4caf0555ec03477cb4c70db67b465311620ed" "checksum enum-map 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ccd9b2d5e0eb5c2ff851791e2af90ab4531b1168cfc239d1c0bf467e60ba3c89" "checksum enum-map-derive 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "153f6e8a8b2868e2fedf921b165f30229edcccb74d6a9bb1ccf0480ef61cd07e" @@ -2432,6 +2655,7 @@ dependencies = [ "checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592" "checksum h2 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "876d91114d78abbde2e1910e3b2d9d0fd1d89b769e20816dfb68d77992cf4158" "checksum hashbrown 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "29fba9abe4742d586dfd0c06ae4f7e73a1c2d86b856933509b269d82cdf06e18" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hmac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f127a908633569f208325f86f71255d3363c79721d7f9fe31cd5569908819771" "checksum http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "eed324f0f0daf6ec10c474f150505af2c143f251722bf9dbd1261bd1f2ee2c1a" "checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" @@ -2481,6 +2705,7 @@ dependencies = [ "checksum num-rational 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4e96f040177bb3da242b5b1ecf3f54b5d5af3efbbfb18608977a5d2767b22f10" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" "checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba" +"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" "checksum odds 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a9a18d7081eb052145753e982d7b8de495f15f74636d0d963f09116581eab665" "checksum opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93f5bb2e8e8dec81642920ccff6b61f1eb94fa3020c5a325c9851ff604152409" "checksum openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)" = "97c140cbb82f3b3468193dd14c1b88def39f341f68257f8a7fe8ed9ed3f628a5" @@ -2498,6 +2723,8 @@ dependencies = [ "checksum phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" "checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" "checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" +"checksum prettydiff 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5240be0c9ea1bc7887819a36264cb9475eb71c58749808e5b989c8c1fdc67acf" +"checksum prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum procedural-masquerade 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9a1574a51c3fd37b26d2c0032b649d08a7d51d4cca9c41bbc5bf7118fa4509d0" "checksum protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)" = "" @@ -2522,8 +2749,11 @@ dependencies = [ "checksum rawslice 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "22b23b9f57ea250c6db4b21e2897b43ff08209217ca8260469fae6c0f9ad7e25" "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" "checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe5204c3a17e97dde73f285d49be585df59ed84b50a872baf416e73b62c3828" "checksum reffers 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0c3b765c544398b56cb85f1c77c6e1d963930b8b5c9678b2cc93195795a6fc32" "checksum regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8f0a0bcab2fd7d1d7c54fa9eae6f43eddeb9ce2e7352f8518a814a4f65d60c58" +"checksum regex-automata 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3ed09217220c272b29ef237a974ad58515bde75f194e3ffa7e6d0bf0f3b01f86" "checksum regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dcfd8681eebe297b81d98498869d4aae052137651ad7b96822f09ceb690d0a96" "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" "checksum reqwest 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)" = "e57803405f8ea0eb041c1567dac36127e0c8caa1251c843cb03d43fd767b3d50" @@ -2535,6 +2765,7 @@ dependencies = [ "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "b96a9549dc8d48f2c283938303c4b5a77aa29bfbc5b54b084fb1630408899a8f" +"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" "checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9" "checksum schannel 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "f2f6abf258d99c3c1c5c2131d99d064e94b7b3dd5f416483057f308fea253339" "checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" @@ -2562,14 +2793,20 @@ dependencies = [ "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" "checksum string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b639411d0b9c738748b5397d5ceba08e648f4f1992231aa859af1a017f31f60b" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" +"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum strsim 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "032c03039aae92b350aad2e3779c352e104d919cb192ba2fabbd7b831ce4f0f6" +"checksum structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" +"checksum structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "53010261a84b37689f9ed7d395165029f9cc7abb9f56bbfe86bee2597ed25107" "checksum subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" "checksum syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)" = "261ae9ecaa397c42b960649561949d69311f08eeaea86a65696e6e46517cf741" "checksum syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)" = "a1393e4a97a19c01e900df2aec855a29f71cf02c402e2f443b8d2747c25c5dbe" "checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" "checksum tempfile 3.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7dc4738f2e68ed2855de5ac9cdbe05c9216773ecde4739b2f095002ab03a13ef" +"checksum term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" "checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327" +"checksum termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330" +"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum tokio 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "ec2ffcf4bcfc641413fa0f1427bf8f91dfc78f56a6559cbf50e04837ae442a87" @@ -2607,6 +2844,7 @@ dependencies = [ "checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" "checksum uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" "checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "797464475f30ddb8830cc529aaaae648d581f99e2036a928877dfde027ddf6b3" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" diff --git a/db/Cargo.toml b/db/Cargo.toml index 332c4f0..ac602e8 100644 --- a/db/Cargo.toml +++ b/db/Cargo.toml @@ -27,6 +27,7 @@ mylog = { git = "https://github.com/scottlamb/mylog" } odds = { version = "0.3.1", features = ["std-vec"] } openssl = "0.10" parking_lot = { version = "0.8", features = [] } +prettydiff = "0.3.1" protobuf = { git = "https://github.com/stepancheg/rust-protobuf" } regex = "1.0" rusqlite = "0.18" diff --git a/db/check.rs b/db/check.rs index 72c244a..0f0fb83 100644 --- a/db/check.rs +++ b/db/check.rs @@ -30,6 +30,7 @@ //! Subcommand to check the database and sample file dir for errors. +use crate::compare; use crate::db::{self, CompositeId, FromSqlUuid}; use crate::dir; use crate::raw; @@ -48,6 +49,15 @@ pub struct Options { } pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<(), Error> { + // Compare schemas. + { + let mut expected = rusqlite::Connection::open_in_memory()?; + db::init(&mut expected)?; + if let Some(diffs) = compare::get_diffs("actual", conn, "expected", &expected)? { + println!("{}", &diffs); + } + } + let db_uuid = raw::get_db_uuid(&conn)?; // Scan directories. diff --git a/db/compare.rs b/db/compare.rs new file mode 100644 index 0000000..7700ef5 --- /dev/null +++ b/db/compare.rs @@ -0,0 +1,168 @@ +// This file is part of Moonfire NVR, a security camera network video recorder. +// Copyright (C) 2019 Scott Lamb +// +// 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 . + +use failure::Error; +use prettydiff::diff_slice; +use rusqlite::params; +use std::fmt::Write; + +#[derive(Debug, PartialEq)] +struct Column { + cid: u32, + name: String, + type_: String, + notnull: bool, + dflt_value: rusqlite::types::Value, + pk: u32, +} + +impl std::fmt::Display for Column { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug, Eq, PartialEq)] +struct Index { + seq: u32, + name: String, + unique: bool, + origin: String, + partial: bool, +} + +impl std::fmt::Display for Index { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug, Eq, PartialEq)] +struct IndexColumn { + seqno: u32, + cid: u32, + name: String, +} + +impl std::fmt::Display for IndexColumn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Returns a sorted vec of table names in the given connection. +fn get_tables(c: &rusqlite::Connection) -> Result, rusqlite::Error> { + c.prepare("select name from sqlite_master where type = 'table' order by name")? + .query_map(params![], |r| r.get(0))? + .collect() +} + +/// Returns a vec of columns in the given table. +fn get_table_columns(c: &rusqlite::Connection, table: &str) + -> Result, rusqlite::Error> { + c.prepare_cached("select * from pragma_table_info(?)")? + .query_map(params![table], |r| Ok(Column { + cid: r.get(0)?, + name: r.get(1)?, + type_: r.get(2)?, + notnull: r.get(3)?, + dflt_value: r.get(4)?, + pk: r.get(5)?, + }))? + .collect() +} + +/// Returns a vec of indices associated with the given table. +fn get_indices(c: &rusqlite::Connection, table: &str) -> Result, rusqlite::Error> { + c.prepare_cached("select * from pragma_index_list(?)")? + .query_map(params![table], |r| Ok(Index { + seq: r.get(0)?, + name: r.get(1)?, + unique: r.get(2)?, + origin: r.get(3)?, + partial: r.get(4)?, + }))? + .collect() +} + +/// Returns a vec of all the columns in the given index. +fn get_index_columns(c: &rusqlite::Connection, index: &str) + -> Result, rusqlite::Error> { + c.prepare_cached("select * from pragma_index_info(?)")? + .query_map(params![index], |r| Ok(IndexColumn { + seqno: r.get(0)?, + cid: r.get(1)?, + name: r.get(2)?, + }))? + .collect() +} + +pub fn get_diffs(n1: &str, c1: &rusqlite::Connection, n2: &str, c2: &rusqlite::Connection) + -> Result, Error> { + let mut diffs = String::new(); + + // Compare table list. + let tables1 = get_tables(c1)?; + let tables2 = get_tables(c2)?; + if tables1 != tables2 { + write!(&mut diffs, "table list mismatch, {} vs {}:\n{}", + n1, n2, diff_slice(&tables1, &tables2))?; + } + + // Compare columns and indices for each table. + for t in &tables1 { + let columns1 = get_table_columns(c1, &t)?; + let columns2 = get_table_columns(c2, &t)?; + if columns1 != columns2 { + write!(&mut diffs, "table {:?} column, {} vs {}:\n{}", + t, n1, n2, diff_slice(&columns1, &columns2))?; + } + + let mut indices1 = get_indices(c1, &t)?; + let mut indices2 = get_indices(c2, &t)?; + indices1.sort_by(|a, b| a.name.cmp(&b.name)); + indices2.sort_by(|a, b| a.name.cmp(&b.name)); + if indices1 != indices2 { + write!(&mut diffs, "table {:?} indices, {} vs {}:\n{}", + t, n1, n2, diff_slice(&indices1, &indices2))?; + } + + for i in &indices1 { + let ic1 = get_index_columns(c1, &i.name)?; + let ic2 = get_index_columns(c2, &i.name)?; + if ic1 != ic2 { + write!(&mut diffs, "table {:?} index {:?} columns {} vs {}:\n{}", + t, i, n1, n2, diff_slice(&ic1, &ic2))?; + } + } + } + + Ok(if diffs.is_empty() { None } else { Some(diffs) }) +} diff --git a/db/lib.rs b/db/lib.rs index 2a65b37..fd480d8 100644 --- a/db/lib.rs +++ b/db/lib.rs @@ -33,6 +33,7 @@ pub mod auth; pub mod check; mod coding; +mod compare; pub mod db; pub mod dir; mod raw; diff --git a/db/upgrade/mod.rs b/db/upgrade/mod.rs index ca21a16..7e168ec 100644 --- a/db/upgrade/mod.rs +++ b/db/upgrade/mod.rs @@ -35,7 +35,7 @@ use crate::db; use failure::{Error, bail}; use log::info; -use rusqlite::types::ToSql; +use rusqlite::params; mod v0_to_v1; mod v1_to_v2; @@ -55,13 +55,13 @@ pub struct Args<'a> { fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> { assert!(!requested.contains(';')); // quick check for accidental sql injection. - let actual = conn.query_row(&format!("pragma journal_mode = {}", requested), &[] as &[&dyn ToSql], + let actual = conn.query_row(&format!("pragma journal_mode = {}", requested), params![], |row| row.get::<_, String>(0))?; info!("...database now in journal_mode {} (requested {}).", actual, requested); Ok(()) } -pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> { +fn upgrade(args: &Args, target_ver: i32, conn: &mut rusqlite::Connection) -> Result<(), Error> { let upgraders = [ v0_to_v1::run, v1_to_v2::run, @@ -73,7 +73,7 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> { { assert_eq!(upgraders.len(), db::EXPECTED_VERSION as usize); let old_ver = - conn.query_row("select max(id) from version", &[] as &[&dyn ToSql], + conn.query_row("select max(id) from version", params![], |row| row.get(0))?; if old_ver > db::EXPECTED_VERSION { bail!("Database is at version {}, later than expected {}", @@ -81,29 +81,35 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> { } else if old_ver < 0 { bail!("Database is at negative version {}!", old_ver); } - info!("Upgrading database from version {} to version {}...", old_ver, db::EXPECTED_VERSION); + info!("Upgrading database from version {} to version {}...", old_ver, target_ver); set_journal_mode(&conn, args.flag_preset_journal).unwrap(); - for ver in old_ver .. db::EXPECTED_VERSION { + for ver in old_ver .. target_ver { info!("...from version {} to version {}", ver, ver + 1); let tx = conn.transaction()?; upgraders[ver as usize](&args, &tx)?; tx.execute(r#" insert into version (id, unix_time, notes) values (?, cast(strftime('%s', 'now') as int32), ?) - "#, &[&(ver + 1) as &dyn ToSql, &UPGRADE_NOTES])?; + "#, params![&(ver + 1), &UPGRADE_NOTES])?; tx.commit()?; } } + Ok(()) +} + +pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> { // Enforce foreign keys. This is on by default with --features=bundled (as rusqlite // compiles the SQLite3 amalgamation with -DSQLITE_DEFAULT_FOREIGN_KEYS=1). Ensure it's // always on. Note that our foreign keys are immediate rather than deferred, so we have to // be careful about the order of operations during the upgrade. - conn.execute("pragma foreign_keys = on", &[] as &[&dyn ToSql])?; + conn.execute("pragma foreign_keys = on", params![])?; // Make the database actually durable. - conn.execute("pragma fullfsync = on", &[] as &[&dyn ToSql])?; - conn.execute("pragma synchronous = 2", &[] as &[&dyn ToSql])?; + conn.execute("pragma fullfsync = on", params![])?; + conn.execute("pragma synchronous = 2", params![])?; + + upgrade(args, db::EXPECTED_VERSION, conn)?; // WAL is the preferred journal mode for normal operation; it reduces the number of syncs // without compromising safety. @@ -116,5 +122,55 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> { "#).unwrap(); } info!("...done."); + Ok(()) } + +#[cfg(test)] +mod tests { + use crate::compare; + use super::*; + + fn new_conn() -> Result { + let conn = rusqlite::Connection::open_in_memory()?; + conn.execute("pragma foreign_keys = on", params![])?; + conn.execute("pragma fullfsync = on", params![])?; + conn.execute("pragma synchronous = 2", params![])?; + Ok(conn) + } + + fn compare(c: &rusqlite::Connection, ver: i32, fresh_sql: &str) -> Result<(), Error> { + let fresh = new_conn()?; + fresh.execute_batch(fresh_sql)?; + if let Some(diffs) = compare::get_diffs("upgraded", &c, "fresh", &fresh)? { + panic!("Version {}: differences found:\n{}", ver, diffs); + } + Ok(()) + } + + /// Upgrades and compares schemas. + /// Doesn't (yet) compare any actual data. + #[test] + fn upgrade_and_compare() -> Result<(), Error> { + let tmpdir = tempdir::TempDir::new("moonfire-nvr-test").unwrap(); + let path = tmpdir.path().to_str().unwrap().to_owned(); + let mut upgraded = new_conn()?; + upgraded.execute_batch(include_str!("v0.sql"))?; + + for (ver, fresh_sql) in &[(1, Some(include_str!("v1.sql"))), + (2, None), // transitional; don't compare schemas. + (3, Some(include_str!("v3.sql"))), + (4, None), // transitional; don't compare schemas. + (4, Some(include_str!("../schema.sql")))] { + upgrade(&Args { + flag_sample_file_dir: Some(&path), + flag_preset_journal: "delete", + flag_no_vacuum: false, + }, *ver, &mut upgraded)?; + if let Some(f) = fresh_sql { + compare(&upgraded, *ver, f)?; + } + } + Ok(()) + } +} diff --git a/db/upgrade/v0.sql b/db/upgrade/v0.sql new file mode 100644 index 0000000..6c223e5 --- /dev/null +++ b/db/upgrade/v0.sql @@ -0,0 +1,159 @@ +-- This file is part of Moonfire NVR, a security camera digital video recorder. +-- Copyright (C) 2016 Scott Lamb +-- +-- 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 . +-- +-- schema.sql: SQLite3 database schema for Moonfire NVR. +-- See also design/schema.md. + +--pragma journal_mode = wal; + +-- This table tracks the schema version. +-- There is one row for the initial database creation (inserted below, after the +-- create statements) and one for each upgrade procedure (if any). +create table version ( + id integer primary key, + + -- The unix time as of the creation/upgrade, as determined by + -- cast(strftime('%s', 'now') as int). + unix_time integer not null, + + -- Optional notes on the creation/upgrade; could include the binary version. + notes text +); + +create table camera ( + id integer primary key, + uuid blob unique,-- not null check (length(uuid) = 16), + + -- A short name of the camera, used in log messages. + short_name text,-- not null, + + -- A short description of the camera. + description text, + + -- The host (or IP address) to use in rtsp:// URLs when accessing the camera. + host text, + + -- The username to use when accessing the camera. + -- If empty, no username or password will be supplied. + username text, + + -- The password to use when accessing the camera. + password text, + + -- The path (starting with "/") to use in rtsp:// URLs to reference this + -- camera's "main" (full-quality) video stream. + main_rtsp_path text, + + -- The path (starting with "/") to use in rtsp:// URLs to reference this + -- camera's "sub" (low-bandwidth) video stream. + sub_rtsp_path text, + + -- The number of bytes of video to retain, excluding the currently-recording + -- file. Older files will be deleted as necessary to stay within this limit. + retain_bytes integer not null check (retain_bytes >= 0) +); + +-- Each row represents a single completed recorded segment of video. +-- Recordings are typically ~60 seconds; never more than 5 minutes. +create table recording ( + id integer primary key, + camera_id integer references camera (id) not null, + + sample_file_bytes integer not null check (sample_file_bytes > 0), + + -- The starting time of the recording, in 90 kHz units since + -- 1970-01-01 00:00:00 UTC. Currently on initial connection, this is taken + -- from the local system time; on subsequent recordings, it exactly + -- matches the previous recording's end time. + start_time_90k integer not null check (start_time_90k > 0), + + -- The duration of the recording, in 90 kHz units. + duration_90k integer not null + check (duration_90k >= 0 and duration_90k < 5*60*90000), + + -- The number of 90 kHz units the local system time is ahead of the + -- recording; negative numbers indicate the local system time is behind + -- the recording. Large values would indicate that the local time has jumped + -- during recording or that the local time and camera time frequencies do + -- not match. + local_time_delta_90k integer not null, + + video_samples integer not null check (video_samples > 0), + video_sync_samples integer not null check (video_samples > 0), + video_sample_entry_id integer references video_sample_entry (id), + + sample_file_uuid blob not null check (length(sample_file_uuid) = 16), + sample_file_sha1 blob not null check (length(sample_file_sha1) = 20), + video_index blob not null check (length(video_index) > 0) +); + +create index recording_cover on recording ( + -- Typical queries use "where camera_id = ? order by start_time_90k (desc)?". + camera_id, + start_time_90k, + + -- These fields are not used for ordering; they cover most queries so + -- that only database verification and actual viewing of recordings need + -- to consult the underlying row. + duration_90k, + video_samples, + video_sync_samples, + video_sample_entry_id, + sample_file_bytes +); + +-- Files in the sample file directory which may be present but should simply be +-- discarded on startup. (Recordings which were never completed or have been +-- marked for completion.) +create table reserved_sample_files ( + uuid blob primary key check (length(uuid) = 16), + state integer not null -- 0 (writing) or 1 (deleted) +) without rowid; + +-- A concrete box derived from a ISO/IEC 14496-12 section 8.5.2 +-- VisualSampleEntry box. Describes the codec, width, height, etc. +create table video_sample_entry ( + id integer primary key, + + -- A SHA-1 hash of |bytes|. + sha1 blob unique not null check (length(sha1) = 20), + + -- The width and height in pixels; must match values within + -- |sample_entry_bytes|. + width integer not null check (width > 0), + height integer not null check (height > 0), + + -- The serialized box, including the leading length and box type (avcC in + -- the case of H.264). + data blob not null check (length(data) > 86) +); + +insert into version (id, unix_time, notes) + values (0, cast(strftime('%s', 'now') as int), 'db creation'); diff --git a/db/upgrade/v0_to_v1.rs b/db/upgrade/v0_to_v1.rs index 17f3e5a..299001e 100644 --- a/db/upgrade/v0_to_v1.rs +++ b/db/upgrade/v0_to_v1.rs @@ -43,7 +43,7 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> alter table camera rename to old_camera; create table camera ( id integer primary key, - uuid blob unique, + uuid blob unique not null check (length(uuid) = 16), short_name text not null, description text, host text, diff --git a/db/upgrade/v1.sql b/db/upgrade/v1.sql new file mode 100644 index 0000000..fe55230 --- /dev/null +++ b/db/upgrade/v1.sql @@ -0,0 +1,205 @@ +-- This file is part of Moonfire NVR, a security camera digital video recorder. +-- Copyright (C) 2016 Scott Lamb +-- +-- 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 . +-- +-- schema.sql: SQLite3 database schema for Moonfire NVR. +-- See also design/schema.md. + +-- This table tracks the schema version. +-- There is one row for the initial database creation (inserted below, after the +-- create statements) and one for each upgrade procedure (if any). +create table version ( + id integer primary key, + + -- The unix time as of the creation/upgrade, as determined by + -- cast(strftime('%s', 'now') as int). + unix_time integer not null, + + -- Optional notes on the creation/upgrade; could include the binary version. + notes text +); + +create table camera ( + id integer primary key, + uuid blob unique not null check (length(uuid) = 16), + + -- A short name of the camera, used in log messages. + short_name text not null, + + -- A short description of the camera. + description text, + + -- The host (or IP address) to use in rtsp:// URLs when accessing the camera. + host text, + + -- The username to use when accessing the camera. + -- If empty, no username or password will be supplied. + username text, + + -- The password to use when accessing the camera. + password text, + + -- The path (starting with "/") to use in rtsp:// URLs to reference this + -- camera's "main" (full-quality) video stream. + main_rtsp_path text, + + -- The path (starting with "/") to use in rtsp:// URLs to reference this + -- camera's "sub" (low-bandwidth) video stream. + sub_rtsp_path text, + + -- The number of bytes of video to retain, excluding the currently-recording + -- file. Older files will be deleted as necessary to stay within this limit. + retain_bytes integer not null check (retain_bytes >= 0), + + -- The low 32 bits of the next recording id to assign for this camera. + -- Typically this is the maximum current recording + 1, but it does + -- not decrease if that recording is deleted. + next_recording_id integer not null check (next_recording_id >= 0) +); + +-- Each row represents a single completed recorded segment of video. +-- Recordings are typically ~60 seconds; never more than 5 minutes. +create table recording ( + -- The high 32 bits of composite_id are taken from the camera's id, which + -- improves locality. The low 32 bits are taken from the camera's + -- next_recording_id (which should be post-incremented in the same + -- transaction). It'd be simpler to use a "without rowid" table and separate + -- fields to make up the primary key, but + -- points out that "without rowid" + -- is not appropriate when the average row size is in excess of 50 bytes. + -- recording_cover rows (which match this id format) are typically 1--5 KiB. + composite_id integer primary key, + + -- This field is redundant with id above, but used to enforce the reference + -- constraint and to structure the recording_start_time index. + camera_id integer not null references camera (id), + + -- The offset of this recording within a run. 0 means this was the first + -- recording made from a RTSP session. The start of the run has id + -- (id-run_offset). + run_offset integer not null, + + -- flags is a bitmask: + -- + -- * 1, or "trailing zero", indicates that this recording is the last in a + -- stream. As the duration of a sample is not known until the next sample + -- is received, the final sample in this recording will have duration 0. + flags integer not null, + + sample_file_bytes integer not null check (sample_file_bytes > 0), + + -- The starting time of the recording, in 90 kHz units since + -- 1970-01-01 00:00:00 UTC. Currently on initial connection, this is taken + -- from the local system time; on subsequent recordings, it exactly + -- matches the previous recording's end time. + start_time_90k integer not null check (start_time_90k > 0), + + -- The duration of the recording, in 90 kHz units. + duration_90k integer not null + check (duration_90k >= 0 and duration_90k < 5*60*90000), + + -- The number of 90 kHz units the local system time is ahead of the + -- recording; negative numbers indicate the local system time is behind + -- the recording. Large absolute values would indicate that the local time + -- has jumped during recording or that the local time and camera time + -- frequencies do not match. + local_time_delta_90k integer not null, + + video_samples integer not null check (video_samples > 0), + video_sync_samples integer not null check (video_sync_samples > 0), + video_sample_entry_id integer references video_sample_entry (id), + + check (composite_id >> 32 = camera_id) +); + +create index recording_cover on recording ( + -- Typical queries use "where camera_id = ? order by start_time_90k". + camera_id, + start_time_90k, + + -- These fields are not used for ordering; they cover most queries so + -- that only database verification and actual viewing of recordings need + -- to consult the underlying row. + duration_90k, + video_samples, + video_sync_samples, + video_sample_entry_id, + sample_file_bytes, + run_offset, + flags +); + +-- Large fields for a recording which are not needed when simply listing all +-- of the recordings in a given range. In particular, when serving a byte +-- range within a .mp4 file, the recording_playback row is needed for the +-- recording(s) corresponding to that particular byte range, needed, but the +-- recording rows suffice for all other recordings in the .mp4. +create table recording_playback ( + -- See description on recording table. + composite_id integer primary key references recording (composite_id), + + -- The binary representation of the sample file's uuid. The canonical text + -- representation of this uuid is the filename within the sample file dir. + sample_file_uuid blob not null check (length(sample_file_uuid) = 16), + + -- The sha1 hash of the contents of the sample file. + sample_file_sha1 blob not null check (length(sample_file_sha1) = 20), + + -- See design/schema.md#video_index for a description of this field. + video_index blob not null check (length(video_index) > 0) +); + +-- Files in the sample file directory which may be present but should simply be +-- discarded on startup. (Recordings which were never completed or have been +-- marked for completion.) +create table reserved_sample_files ( + uuid blob primary key check (length(uuid) = 16), + state integer not null -- 0 (writing) or 1 (deleted) +) without rowid; + +-- A concrete box derived from a ISO/IEC 14496-12 section 8.5.2 +-- VisualSampleEntry box. Describes the codec, width, height, etc. +create table video_sample_entry ( + id integer primary key, + + -- A SHA-1 hash of |bytes|. + sha1 blob unique not null check (length(sha1) = 20), + + -- The width and height in pixels; must match values within + -- |sample_entry_bytes|. + width integer not null check (width > 0), + height integer not null check (height > 0), + + -- The serialized box, including the leading length and box type (avcC in + -- the case of H.264). + data blob not null check (length(data) > 86) +); + +insert into version (id, unix_time, notes) + values (1, cast(strftime('%s', 'now') as int), 'db creation'); diff --git a/db/upgrade/v1_to_v2.rs b/db/upgrade/v1_to_v2.rs index aef7bec..b509026 100644 --- a/db/upgrade/v1_to_v2.rs +++ b/db/upgrade/v1_to_v2.rs @@ -98,6 +98,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> last_use_peer_addr blob, use_count not null default 0 ) without rowid; + create index user_session_uid on user_session (user_id); "#)?; let db_uuid = ::uuid::Uuid::new_v4(); let db_uuid_bytes = &db_uuid.as_bytes()[..]; diff --git a/db/upgrade/v3.sql b/db/upgrade/v3.sql new file mode 100644 index 0000000..6038c41 --- /dev/null +++ b/db/upgrade/v3.sql @@ -0,0 +1,400 @@ +-- This file is part of Moonfire NVR, a security camera digital video recorder. +-- Copyright (C) 2016 Scott Lamb +-- +-- 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 . +-- +-- schema.sql: SQLite3 database schema for Moonfire NVR. +-- See also design/schema.md. + +-- Database metadata. There should be exactly one row in this table. +create table meta ( + uuid blob not null check (length(uuid) = 16) +); + +-- This table tracks the schema version. +-- There is one row for the initial database creation (inserted below, after the +-- create statements) and one for each upgrade procedure (if any). +create table version ( + id integer primary key, + + -- The unix time as of the creation/upgrade, as determined by + -- cast(strftime('%s', 'now') as int). + unix_time integer not null, + + -- Optional notes on the creation/upgrade; could include the binary version. + notes text +); + +-- Tracks every time the database has been opened in read/write mode. +-- This is used to ensure directories are in sync with the database (see +-- schema.proto:DirMeta), to disambiguate uncommitted recordings, and +-- potentially to understand time problems. +create table open ( + id integer primary key, + uuid blob unique not null check (length(uuid) = 16), + + -- Information about when / how long the database was open. These may be all + -- null, for example in the open that represents all information written + -- prior to database version 3. + + -- System time when the database was opened, in 90 kHz units since + -- 1970-01-01 00:00:00Z excluding leap seconds. + start_time_90k integer, + + -- System time when the database was closed or (on crash) last flushed. + end_time_90k integer, + + -- How long the database was open. This is end_time_90k - start_time_90k if + -- there were no time steps or leap seconds during this time. + duration_90k integer +); + +create table sample_file_dir ( + id integer primary key, + path text unique not null, + uuid blob unique not null check (length(uuid) = 16), + + -- The last (read/write) open of this directory which fully completed. + -- See schema.proto:DirMeta for a more complete description. + last_complete_open_id integer references open (id) +); + +create table camera ( + id integer primary key, + uuid blob unique not null check (length(uuid) = 16), + + -- A short name of the camera, used in log messages. + short_name text not null, + + -- A short description of the camera. + description text, + + -- The host (or IP address) to use in rtsp:// URLs when accessing the camera. + host text, + + -- The username to use when accessing the camera. + -- If empty, no username or password will be supplied. + username text, + + -- The password to use when accessing the camera. + password text +); + +create table stream ( + id integer primary key, + camera_id integer not null references camera (id), + sample_file_dir_id integer references sample_file_dir (id), + type text not null check (type in ('main', 'sub')), + + -- If record is true, the stream should start recording when moonfire + -- starts. If false, no new recordings will be made, but old recordings + -- will not be deleted. + record integer not null check (record in (1, 0)), + + -- The path (starting with "/") to use in rtsp:// URLs to for this stream. + rtsp_path text not null, + + -- The number of bytes of video to retain, excluding the currently-recording + -- file. Older files will be deleted as necessary to stay within this limit. + retain_bytes integer not null check (retain_bytes >= 0), + + -- Flush the database when the first instant of completed recording is this + -- many seconds old. A value of 0 means that every completed recording will + -- cause an immediate flush. Higher values may allow flushes to be combined, + -- reducing SSD write cycles. For example, if all streams have a flush_if_sec + -- >= x sec, there will be: + -- + -- * at most one flush per x sec in total + -- * at most x sec of completed but unflushed recordings per stream. + -- * at most x completed but unflushed recordings per stream, in the worst + -- case where a recording instantly fails, waits the 1-second retry delay, + -- then fails again, forever. + flush_if_sec integer not null, + + -- The low 32 bits of the next recording id to assign for this stream. + -- Typically this is the maximum current recording + 1, but it does + -- not decrease if that recording is deleted. + next_recording_id integer not null check (next_recording_id >= 0), + + unique (camera_id, type) +); + +-- Each row represents a single completed recorded segment of video. +-- Recordings are typically ~60 seconds; never more than 5 minutes. +create table recording ( + -- The high 32 bits of composite_id are taken from the stream's id, which + -- improves locality. The low 32 bits are taken from the stream's + -- next_recording_id (which should be post-incremented in the same + -- transaction). It'd be simpler to use a "without rowid" table and separate + -- fields to make up the primary key, but + -- points out that "without rowid" + -- is not appropriate when the average row size is in excess of 50 bytes. + -- recording_cover rows (which match this id format) are typically 1--5 KiB. + composite_id integer primary key, + + -- The open in which this was committed to the database. For a given + -- composite_id, only one recording will ever be committed to the database, + -- but in-memory state may reflect a recording which never gets committed. + -- This field allows disambiguation in etags and such. + open_id integer not null references open (id), + + -- This field is redundant with id above, but used to enforce the reference + -- constraint and to structure the recording_start_time index. + stream_id integer not null references stream (id), + + -- The offset of this recording within a run. 0 means this was the first + -- recording made from a RTSP session. The start of the run has id + -- (id-run_offset). + run_offset integer not null, + + -- flags is a bitmask: + -- + -- * 1, or "trailing zero", indicates that this recording is the last in a + -- stream. As the duration of a sample is not known until the next sample + -- is received, the final sample in this recording will have duration 0. + flags integer not null, + + sample_file_bytes integer not null check (sample_file_bytes > 0), + + -- The starting time of the recording, in 90 kHz units since + -- 1970-01-01 00:00:00 UTC excluding leap seconds. Currently on initial + -- connection, this is taken from the local system time; on subsequent + -- recordings, it exactly matches the previous recording's end time. + start_time_90k integer not null check (start_time_90k > 0), + + -- The duration of the recording, in 90 kHz units. + duration_90k integer not null + check (duration_90k >= 0 and duration_90k < 5*60*90000), + + video_samples integer not null check (video_samples > 0), + video_sync_samples integer not null check (video_sync_samples > 0), + video_sample_entry_id integer references video_sample_entry (id), + + check (composite_id >> 32 = stream_id) +); + +create index recording_cover on recording ( + -- Typical queries use "where stream_id = ? order by start_time_90k". + stream_id, + start_time_90k, + + -- These fields are not used for ordering; they cover most queries so + -- that only database verification and actual viewing of recordings need + -- to consult the underlying row. + open_id, + duration_90k, + video_samples, + video_sync_samples, + video_sample_entry_id, + sample_file_bytes, + run_offset, + flags +); + +-- Fields which are only needed to check/correct database integrity problems +-- (such as incorrect timestamps). +create table recording_integrity ( + -- See description on recording table. + composite_id integer primary key references recording (composite_id), + + -- The number of 90 kHz units the local system's monotonic clock has + -- advanced more than the stated duration of recordings in a run since the + -- first recording ended. Negative numbers indicate the local system time is + -- behind the recording. + -- + -- The first recording of a run (that is, one with run_offset=0) has null + -- local_time_delta_90k because errors are assumed to + -- be the result of initial buffering rather than frequency mismatch. + -- + -- This value should be near 0 even on long runs in which the camera's clock + -- and local system's clock frequency differ because each recording's delta + -- is used to correct the durations of the next (up to 500 ppm error). + local_time_delta_90k integer, + + -- The number of 90 kHz units the local system's monotonic clock had + -- advanced since the database was opened, as of the start of recording. + -- TODO: fill this in! + local_time_since_open_90k integer, + + -- The difference between start_time_90k+duration_90k and a wall clock + -- timestamp captured at end of this recording. This is meaningful for all + -- recordings in a run, even the initial one (run_offset=0), because + -- start_time_90k is derived from the wall time as of when recording + -- starts, not when it ends. + -- TODO: fill this in! + wall_time_delta_90k integer, + + -- The sha1 hash of the contents of the sample file. + sample_file_sha1 blob check (length(sample_file_sha1) <= 20) +); + +-- Large fields for a recording which are needed ony for playback. +-- In particular, when serving a byte range within a .mp4 file, the +-- recording_playback row is needed for the recording(s) corresponding to that +-- particular byte range, needed, but the recording rows suffice for all other +-- recordings in the .mp4. +create table recording_playback ( + -- See description on recording table. + composite_id integer primary key references recording (composite_id), + + -- See design/schema.md#video_index for a description of this field. + video_index blob not null check (length(video_index) > 0) + + -- audio_index could be added here in the future. +); + +-- Files which are to be deleted (may or may not still exist). +-- Note that besides these files, for each stream, any recordings >= its +-- next_recording_id should be discarded on startup. +create table garbage ( + -- This is _mostly_ redundant with composite_id, which contains the stream + -- id and thus a linkage to the sample file directory. Listing it here + -- explicitly means that streams can be deleted without losing the + -- association of garbage to directory. + sample_file_dir_id integer not null references sample_file_dir (id), + + -- See description on recording table. + composite_id integer not null, + + -- Organize the table first by directory, as that's how it will be queried. + primary key (sample_file_dir_id, composite_id) +) without rowid; + +-- A concrete box derived from a ISO/IEC 14496-12 section 8.5.2 +-- VisualSampleEntry box. Describes the codec, width, height, etc. +create table video_sample_entry ( + id integer primary key, + + -- A SHA-1 hash of |bytes|. + sha1 blob unique not null check (length(sha1) = 20), + + -- The width and height in pixels; must match values within + -- |sample_entry_bytes|. + width integer not null check (width > 0), + height integer not null check (height > 0), + + -- The codec in RFC-6381 format, such as "avc1.4d001f". + rfc6381_codec text not null, + + -- The serialized box, including the leading length and box type (avcC in + -- the case of H.264). + data blob not null check (length(data) > 86) +); + +create table user ( + id integer primary key, + username unique not null, + + -- Bitwise mask of flags: + -- 1: disabled. If set, no method of authentication for this user will succeed. + flags integer not null, + + -- If set, a hash for password authentication, as generated by `libpasta::hash_password`. + password_hash text, + + -- A counter which increments with every password reset or clear. + password_id integer not null default 0, + + -- Updated lazily on database flush; reset when password_id is incremented. + -- This could be used to automatically disable the password on hitting a threshold. + password_failure_count integer not null default 0, + + -- If set, a Unix UID that is accepted for authentication when using HTTP over + -- a Unix domain socket. (Additionally, the UID running Moonfire NVR can authenticate + -- as anyone; there's no point in trying to do otherwise.) This might be an easy + -- bootstrap method once configuration happens through a web UI rather than text UI. + unix_uid integer +); + +-- A single session, whether for browser or robot use. +-- These map at the HTTP layer to an "s" cookie (exact format described +-- elsewhere), which holds the session id and an encrypted sequence number for +-- replay protection. +create table user_session ( + -- The session id is a 48-byte blob. This is the unencoded, unsalted Blake2b-192 + -- (24 bytes) of the unencoded session id. Much like `password_hash`, a + -- hash is used here so that a leaked database backup can't be trivially used + -- to steal credentials. + session_id_hash blob primary key not null, + + user_id integer references user (id) not null, + + -- A 32-byte random number. Used to derive keys for the replay protection + -- and CSRF tokens. + seed blob not null, + + -- A bitwise mask of flags, currently all properties of the HTTP cookie + -- used to hold the session: + -- 1: HttpOnly + -- 2: Secure + -- 4: SameSite=Lax + -- 8: SameSite=Strict - 4 must also be set. + flags integer not null, + + -- The domain of the HTTP cookie used to store this session. The outbound + -- `Set-Cookie` header never specifies a scope, so this matches the `Host:` of + -- the inbound HTTP request (minus the :port, if any was specified). + domain text, + + -- An editable description which might describe the device/program which uses + -- this session, such as "Chromebook", "iPhone", or "motion detection worker". + description text, + + creation_password_id integer, -- the id it was created from, if created via password + creation_time_sec integer not null, -- sec since epoch + creation_user_agent text, -- User-Agent header from inbound HTTP request. + creation_peer_addr blob, -- IPv4 or IPv6 address, or null for Unix socket. + + revocation_time_sec integer, -- sec since epoch + revocation_user_agent text, -- User-Agent header from inbound HTTP request. + revocation_peer_addr blob, -- IPv4 or IPv6 address, or null for Unix socket/no peer. + + -- A value indicating the reason for revocation, with optional additional + -- text detail. Enumeration values: + -- 0: logout link clicked (i.e. from within the session itself) + -- + -- This might be extended for a variety of other reasons: + -- x: user revoked (while authenticated in another way) + -- x: password change invalidated all sessions created with that password + -- x: expired (due to fixed total time or time inactive) + -- x: evicted (due to too many sessions) + -- x: suspicious activity + revocation_reason integer, + revocation_reason_detail text, + + -- Information about requests which used this session, updated lazily on database flush. + last_use_time_sec integer, -- sec since epoch + last_use_user_agent text, -- User-Agent header from inbound HTTP request. + last_use_peer_addr blob, -- IPv4 or IPv6 address, or null for Unix socket. + use_count not null default 0 +) without rowid; + +create index user_session_uid on user_session (user_id); + +insert into version (id, unix_time, notes) + values (3, cast(strftime('%s', 'now') as int), 'db creation'); diff --git a/db/upgrade/v3_to_v4.rs b/db/upgrade/v3_to_v4.rs index 9bb32bf..8af214f 100644 --- a/db/upgrade/v3_to_v4.rs +++ b/db/upgrade/v3_to_v4.rs @@ -185,6 +185,10 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> drop table old_recording; drop table old_stream; drop table old_camera; + + -- This was supposed to be present in version 2, but the upgrade procedure used to miss it. + -- Catch up so we know a version 4 database is right. + create index if not exists user_session_uid on user_session (user_id); "#)?; Ok(()) }