~gbmor/clinte

bb327d381e5626d96942a805e6a5f4d4a5a771d5 — Ben Morrison 1 year, 4 months ago e4af001
extensive rewrite to use json for storage:

sqlite3 requires the directory where the database resides to be
writeable by the user. This presents a problem on multiuser UNIX systems
where they may want to limit areas where users have write access.

This rewrite totally scraps the sqlite3 database in favor of a pretty
simple json file consisting of an array of posts. flock(2) locking is
used to synchronize access to the file and make sure two clients aren't
trying to write to it at once. The locking is fairly granular right now,
but later I may change it to using a single lock for the duration of
execution since race conditions are *possible*, if unlikely for the
purposes of clinte's intended use.
8 files changed, 359 insertions(+), 255 deletions(-)

M Cargo.lock
M Cargo.toml
M Makefile
M README.md
A clinte.json
M src/db.rs
M src/main.rs
M src/posts.rs
M Cargo.lock => Cargo.lock +157 -62
@@ 1,6 1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "addr2line"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "gimli 0.21.0 (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"


@@ 34,6 42,18 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "backtrace"
version = "0.3.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "addr2line 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
 "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
 "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
 "object 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "base64"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 87,9 107,11 @@ version = "1.0.0"
dependencies = [
 "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
 "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "fd-lock 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
 "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
 "rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
 "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)",
 "simplelog 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
 "users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
]


@@ 129,14 151,34 @@ dependencies = [
]

[[package]]
name = "fallible-iterator"
version = "0.2.0"
name = "failure"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "backtrace 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)",
 "failure_derive 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
name = "failure_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
 "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
 "syn 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
 "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "fd-lock"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
 "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
 "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "getrandom"


@@ 149,6 191,11 @@ dependencies = [
]

[[package]]
name = "gimli"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "hermit-abi"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 157,6 204,11 @@ dependencies = [
]

[[package]]
name = "itoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 167,20 219,6 @@ version = "0.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "libsqlite3-sys"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
 "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "linked-hash-map"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "log"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 189,19 227,6 @@ dependencies = [
]

[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "memchr"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "num-integer"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 219,9 244,25 @@ dependencies = [
]

[[package]]
name = "pkg-config"
version = "0.3.17"
name = "object"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "proc-macro2"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "quote"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "redox_syscall"


@@ 239,20 280,6 @@ dependencies = [
]

[[package]]
name = "rusqlite"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
 "fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
 "libsqlite3-sys 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
 "memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "rust-argon2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 264,6 291,44 @@ dependencies = [
]

[[package]]
name = "rustc-demangle"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "ryu"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "serde"
version = "1.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "serde_derive 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "serde_derive"
version = "1.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
 "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
 "syn 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "serde_json"
version = "1.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
 "ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
 "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "simplelog"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 279,6 344,27 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "syn"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
 "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
 "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "synstructure"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
 "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
 "syn 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
 "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "term"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 311,6 397,11 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "users"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 319,11 410,6 @@ dependencies = [
]

[[package]]
name = "vcpkg"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 348,11 434,13 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[metadata]
"checksum addr2line 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543"
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
"checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee"
"checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
"checksum backtrace 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)" = "0df2f85c8a2abbe3b7d7e748052fdd9b76a0458fdeb16ad4223f5eca78c7c130"
"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"


@@ 363,32 451,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
"checksum dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
"checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b"
"checksum fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
"checksum fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
"checksum failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
"checksum failure_derive 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
"checksum fd-lock 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a15bec795244d49f5ee3024bdc6c3883fb035f7f6601d4a4821c3d5d60784454"
"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
"checksum gimli 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
"checksum hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772"
"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
"checksum libsqlite3-sys 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5e5b95e89c330291768dc840238db7f9e204fd208511ab6319b56193a7f2ae25"
"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
"checksum memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223"
"checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
"checksum object 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9cbca9424c482ee628fa549d9c812e2cd22f1180b9222c9200fdfa6eb31aecb2"
"checksum proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "1502d12e458c49a4c9cbff560d0fe0060c252bc29799ed94ca2ed4bb665a0101"
"checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea"
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
"checksum redox_users 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1dc1887cbcd764cc066e2c08681a5615433ac3de9752838a9ec114613b118575"
"checksum rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2a194373ef527035645a1bc21b10dc2125f73497e6e155771233eb187aedd051"
"checksum rust-argon2 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "416f5109bdd413cec4f04c029297838e7604c993f8d1483b1d438f23bdc3eb35"
"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
"checksum ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1"
"checksum serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c"
"checksum serde_derive 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984"
"checksum serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)" = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2"
"checksum simplelog 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "05a3e303ace6adb0a60a9e9e2fbc6a33e1749d1e43587e2125f7efa9c5e107c5"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum syn 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "ef781e621ee763a2a40721a8861ec519cb76966aee03bb5d00adb6a31dc1c1de"
"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
"checksum term 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5"
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
"checksum users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c72f4267aea0c3ec6d07eaabea6ead7c5ddacfafc5e22bcf8d186706851fb4cf"
"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

M Cargo.toml => Cargo.toml +3 -1
@@ 17,9 17,11 @@ maintenance = { status = "stable" }

[dependencies]
chrono = "^0.4"
fd-lock = "^1.1"
lazy_static = "^1.4"
log = "^0.4"
rusqlite = "^0.20"
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
simplelog = "^0.7"
users = "^0.9"


M Makefile => Makefile +19 -4
@@ 4,6 4,9 @@ BINDIR?=$(_INSTDIR)/bin
DBDIR?=$(_INSTDIR)/clinte

clinte:
	@printf "\n%s\n" "Checking out latest tag..."
	git checkout $(git describe --tags --abbrev=0)

	@printf "\n%s\n" "Building clinte. This may take a minute or two."
	cargo build --release
	@printf "\n%s\n" "...Done!"


@@ 16,21 19,33 @@ clean:

.PHONY: update
update:
	@printf "\n%s\n" "Making sure we're on master..."
	git checkout master

	@printf "\n%s\n" "Updating from upstream repository..."
	git pull --rebase
	
	@printf "\n%s\n" "Checking out latest tag..."
	git checkout $(git describe --tags --abbrev=0)
	
	@printf "\n%s\n" "...Done!"

.PHONY: install
install:
	@printf "\n%s\n" "Installing clinte..."
	@printf "\n%s\n" "Creating directories..."
	mkdir -p $(BINDIR)
	mkdir -p $(DBDIR)

	@printf "\n%s\n" "Copying files..."
	install -m755 target/release/clinte $(BINDIR)
	touch $(DBDIR)/clinte.db
	chmod 666 $(DBDIR)/clinte.db
	chmod 777 $(DBDIR)
	install -m666 clinte.json $(DBDIR)
	
	@printf "\n%s\n" "...Done!"

.PHONY: upgrade
upgrade:
	@printf "\n%s\n" "Upgrading clinte..."
	install -m755 target/release/clinte $(BINDIR)
	@printf "\n%s\n" "...Done!"

.PHONY: test

M README.md => README.md +31 -12
@@ 1,35 1,29 @@
# clinte   [![Build Status](https://travis-ci.com/gbmor/clinte.svg?branch=master)](https://travis-ci.com/gbmor/clinte) [![codecov](https://codecov.io/gh/gbmor/clinte/branch/master/graph/badge.svg)](https://codecov.io/gh/gbmor/clinte)

Command-line community notice board. Post text-only notes for other users to see.
Command-line community notice board for public-access UNIX systems. Post text-only notes for other users to see.

## Features

- Username is tagged based on the executing user
- Shows the 15 most recent posts in descending order
- Able to go back and edit your own posts
- Able to edit or delete your own posts
- Title <= 30 chars
- Body <= 500 chars
- Calls `$EDITOR` when creating or modifying the body of a post
- If `$EDITOR` is unset, calls `nano`
- Stores posts in JSON
- Uses advisory locking via `flock(2)` to synchronize access to the posts file

[![Screenshot](https://github.com/gbmor/clinte/blob/master/assets/clinte.png)](https://github.com/gbmor/clinte/blob/master/assets/clinte.png)

## Installation

Current build dependencies are as follows:

- `rust >= 1.36`
- `libsqlite3-dev`

The installation for the build deps will vary based on your OS (`Linux, BSD`)

Clone the repository and jump into the directory:

```
$ git clone git://github.com/gbmor/clinte.git
...
$ cd clinte
$ git checkout $(git describe --tags --abbrev=0)
```

Run the makefile and install:


@@ 42,6 36,32 @@ $ make
$ sudo make install
```

`make` will automatically checkout the latest tag and build from there.

## Upgrading

**Note:** v1.0.0 used sqlite3, which presented some issues. v2.0.0 uses a json structure for posts,
as this will be safer on a multi-user system. When upgrading from v1.0.0 to v2.0.0, you won't be
able to save your posts without using a third-party tool to dump the `posts` table to json, and
manually adjusting it to fit the expected format (which can be seen in the included `clinte.json`).

*If upgrading from v1.0.0 -> v2.0.0, do a fresh install. The following applies to upgrading when
already running at least v2.0.0*

```
$ make update
$ make
$ make upgrade
```

This will:

* checkout `master`
* pull / rebase changes from upstream
* checkout the latest tag
* rebuild
* replace the `clinte` binary, but leave the posts file untouched.

## Usage

Issuing the program name itself will list


@@ 91,5 111,4 @@ Use this flag if something's going wrong. Additional information will be written

## Notes

`sqlite` expects the directory where the database lies to be writeable by the user. So, until I move this
to using another storage medium (maybe plain text?), keep that in mind.
The file where the posts are stored must be writeable by all users on the system. Keep this in mind.
\ No newline at end of file

A clinte.json => clinte.json +9 -0
@@ 0,0 1,9 @@
{
    "posts": [
        {
            "title": "Welcome to CLI NoTEs!",
            "author": "clinte!",
            "body": "Welcome to clinte! For usage, run 'clinte -h'"
        }
    ]
}
\ No newline at end of file

M src/db.rs => src/db.rs +78 -43
@@ 1,68 1,94 @@
use std::time;
use fd_lock::FdLock;
use serde::{Deserialize, Serialize};

use std::fs;
use std::fs::File;

use crate::conf;
use crate::error;

const DB_PATH: &str = "/usr/local/clinte/clinte.db";
#[cfg(test)]
pub const PATH: &str = "clinte.json";

#[cfg(not(test))]
pub const PATH: &str = "/usr/local/clinte/clinte.json";

#[derive(Debug)]
#[derive(Debug, Deserialize, Serialize)]
pub struct Post {
    pub id: u32,
    pub title: String,
    pub author: String,
    pub body: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Posts {
    pub posts: Vec<Post>,
}

#[derive(Debug)]
pub struct Conn {
    pub conn: rusqlite::Connection,
    pub conn: FdLock<std::fs::File>,
}

impl Conn {
    pub fn init(path: &str) -> rusqlite::Connection {
        let start = time::Instant::now();

    pub fn init(path: &str) -> Self {
        if *conf::DEBUG {
            log::info!("Connecting to database");
            log::info!("Opening clinte.json");
        }

        let conn = error::helper(
            rusqlite::Connection::open_with_flags(
                path,
                rusqlite::OpenFlags::SQLITE_OPEN_FULL_MUTEX
                    | rusqlite::OpenFlags::SQLITE_OPEN_CREATE
                    | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE,
            ),
            "Could not connect to DB",
        );
        let file = error::helper(File::open(path), "Couldn't open clinte.json");

        error::helper(
            conn.execute(
                "CREATE TABLE IF NOT EXISTS posts (
            id INTEGER PRIMARY KEY NOT NULL,
            title TEXT NOT NULL,
            author TEXT NOT NULL,
            body TEXT NOT NULL
        )",
                rusqlite::NO_PARAMS,
            ),
            "Could not initialize DB",
        );
        Self {
            conn: FdLock::new(file),
        }
    }
}

impl Posts {
    pub fn get_all(path: &str) -> Self {
        if *conf::DEBUG {
            log::info!(
                "Database connection established in {}ms",
                start.elapsed().as_millis()
            );
            log::info!("Retrieving posts...");
        }

        conn
        let mut db = Conn::init(path);
        let _guard = error::helper(db.conn.try_lock(), "Couldn't acquire lock on clinte.json");
        let strdata = error::helper(fs::read_to_string(PATH), "Couldn't read clinte.json");
        let out: Self = error::helper(serde_json::from_str(&strdata), "Couldn't parse clinte.json");

        out
    }

    pub fn new() -> Self {
        Conn {
            conn: Conn::init(DB_PATH),
        }
    pub fn replace(&mut self, n: usize, post: Post) {
        self.posts[n] = post;
    }

    pub fn get(&self, n: usize) -> &Post {
        &self.posts[n]
    }

    pub fn append(&mut self, post: Post) {
        self.posts.push(post);
    }

    pub fn delete(&mut self, n: usize) {
        self.posts.remove(n);
    }

    pub fn write(&self) {
        let strdata = error::helper(
            serde_json::to_string_pretty(&self),
            "Couldn't serialize posts",
        );

        let mut db_fd = Conn::init(PATH);
        let _guard = error::helper(
            db_fd.conn.try_lock(),
            "Couldn't acquire lock on clinte.json",
        );
        error::helper(
            fs::write(PATH, &strdata),
            "Couldn't write data to clinte.json",
        );
    }
}



@@ 71,10 97,19 @@ mod tests {
    use super::*;

    #[test]
    fn test_new() {
        let conn = Conn::init(":memory:");
        let mut stmt = conn.prepare("SELECT * FROM POSTS").unwrap();
    fn test_init() {
        let mut conn = Conn::init(PATH);
        conn.conn.try_lock().unwrap();
    }

    #[test]
    fn retrieve_posts_and_examine() {
        let all = Posts::get_all(PATH);
        assert_eq!(all.posts.len(), 1);

        stmt.query_map(rusqlite::NO_PARAMS, |_| Ok(())).unwrap();
        let post = all.get(0);
        assert_eq!(post.title, "Welcome to CLI NoTEs!");
        assert_eq!(post.author, "clinte!");
        assert_eq!(post.body, "Welcome to clinte! For usage, run 'clinte -h'");
    }
}

M src/main.rs => src/main.rs +6 -8
@@ 21,17 21,15 @@ fn main() {
    println!("a community notices system");
    println!();

    let db = db::Conn::new();

    if *conf::DEBUG {
        log::info!("Startup completed in {:?}ms", start.elapsed().as_millis());
    }

    if arg_matches.subcommand_matches("post").is_some() {
        log::info!("New post...");
        error::helper(posts::create(&db), "Error creating new post");
        error::helper(posts::create(), "Error creating new post");
    } else if let Some(updmatch) = arg_matches.subcommand_matches("update") {
        let id: u32 = if let Some(val) = updmatch.value_of("id") {
        let id: usize = if let Some(val) = updmatch.value_of("id") {
            error::helper(val.parse(), "Couldn't parse ID")
        } else {
            0


@@ 40,11 38,11 @@ fn main() {
        log::info!("Updating post ...");

        error::helper(
            posts::update_handler(&db, id),
            posts::update_handler(id),
            format!("Error updating post {}", id).as_ref(),
        );
    } else if let Some(delmatch) = arg_matches.subcommand_matches("delete") {
        let id: u32 = if let Some(val) = delmatch.value_of("id") {
        let id: usize = if let Some(val) = delmatch.value_of("id") {
            error::helper(val.parse(), "Couldn't parse ID")
        } else {
            0


@@ 53,10 51,10 @@ fn main() {
        log::info!("Deleting post");

        error::helper(
            posts::delete_handler(&db, id),
            posts::delete_handler(id),
            format!("Error deleting post {}", id).as_ref(),
        );
    }

    error::helper(posts::display(&db), "Error displaying posts");
    error::helper(posts::display(), "Error displaying posts");
}

M src/posts.rs => src/posts.rs +56 -125
@@ 5,17 5,6 @@ use crate::ed;
use crate::error;
use crate::user;

// Executes the sql statement that inserts a new post
// Broken off for unit testing.
pub fn exec_new(stmt: &mut rusqlite::Statement, title: &str, body: &str) -> error::Result<()> {
    stmt.execute_named(&[
        (":title", &title),
        (":author", &*user::NAME),
        (":body", &body),
    ])?;
    Ok(())
}

// Make sure nobody encodes narsty characters
// into a message to negatively affect other
// users


@@ 29,11 18,7 @@ fn str_to_utf8(str: &str) -> String {
}

// First handler for creating a new post.
pub fn create(db: &db::Conn) -> error::Result<()> {
    let mut stmt = db
        .conn
        .prepare("INSERT INTO posts (title, author, body) VALUES (:title, :author, :body)")?;

pub fn create() -> error::Result<()> {
    println!();
    println!("Title of the new post: ");



@@ 55,43 40,38 @@ pub fn create(db: &db::Conn) -> error::Result<()> {
    } else {
        &body_raw
    };

    let trimmed_body = body.trim();

    exec_new(&mut stmt, title, trimmed_body)?;
    let user = &*user::NAME;

    let mut all = db::Posts::get_all(db::PATH);
    let new = db::Post {
        author: user.into(),
        title: title.to_string(),
        body: trimmed_body.to_string(),
    };

    all.append(new);
    all.write();

    println!();
    Ok(())
}

// Shows the most recent posts.
pub fn display(db: &db::Conn) -> error::Result<()> {
    let mut stmt = db.conn.prepare("SELECT * FROM posts")?;
    let out = stmt.query_map(rusqlite::NO_PARAMS, |row| {
        let id: u32 = row.get(0)?;
        let title: String = row.get(1)?;
        let author: String = row.get(2)?;
        let body: String = row.get(3)?;
        Ok(db::Post {
            id,
            title,
            author,
            body,
        })
    })?;
pub fn display() -> error::Result<()> {
    let all = db::Posts::get_all(db::PATH);

    let mut postvec = Vec::new();
    out.for_each(|row| {
        if let Ok(post) = row {
            let newpost = format!(
                "{}. {} -> by {}\n{}\n\n",
                post.id,
                post.title.trim(),
                post.author,
                post.body.trim()
            );
            postvec.push(newpost);
        }
    all.posts.iter().enumerate().for_each(|(id, post)| {
        let newpost = format!(
            "{}. {} -> by {}\n{}\n\n",
            id + 1,
            post.title.trim(),
            post.author,
            post.body.trim()
        );
        postvec.push(newpost);
    });

    for (i, e) in postvec.iter().enumerate() {


@@ 104,8 84,8 @@ pub fn display(db: &db::Conn) -> error::Result<()> {
}

// First handler to update posts.
pub fn update_handler(db: &db::Conn, id: u32) -> error::Result<()> {
    let id_num_in = if id == 0 {
pub fn update_handler(id: usize) -> error::Result<()> {
    let mut id_num_in = if id == 0 {
        println!();
        println!("ID number of your post to edit?");
        let mut id_num_in = String::new();


@@ 115,31 95,29 @@ pub fn update_handler(db: &db::Conn, id: u32) -> error::Result<()> {
        id
    };

    let mut get_stmt = db.conn.prepare("SELECT * FROM posts WHERE id = :id")?;
    id_num_in -= 1;

    let row = get_stmt.query_row_named(&[(":id", &id_num_in)], |row| {
        let title: String = row.get(1)?;
        let author = row.get(2)?;
        let body = row.get(3)?;
        Ok(vec![title, author, body])
    })?;
    let user = &*user::NAME;
    let mut all = db::Posts::get_all(db::PATH);
    let post = all.get(id_num_in);

    if *user::NAME != row[1] {
    if *user != post.author {
        println!();
        println!("Users don't match. Can't update post!");
        println!();
        println!("Username mismatch - can't update_handler post!");
        return Ok(());
        std::process::exit(1);
    }

    let mut new_title = String::new();

    println!("Updating post {}", id_num_in);
    println!();
    println!("Current Title: {}", &row[0]);
    println!("Current Title: {}", post.title);
    println!();
    println!("Enter new title:");
    io::stdin().read_line(&mut new_title)?;

    let body_raw = str_to_utf8(&ed::call(&row[2]));
    let body_raw = str_to_utf8(&ed::call(&post.body));
    let body = if body_raw.len() > 500 {
        &body_raw[..500]
    } else {


@@ 148,38 126,24 @@ pub fn update_handler(db: &db::Conn, id: u32) -> error::Result<()> {

    let trimmed_body = body.trim();

    update(&new_title, &trimmed_body, id_num_in, &db)?;

    println!();
    Ok(())
}

// Allows editing of posts - called by main::update
pub fn update(new_title: &str, new_body: &str, id_num_in: u32, db: &db::Conn) -> error::Result<()> {
    let new_title = new_title.trim();
    let new_body = new_body.trim();

    let title_stmt = format!("UPDATE posts SET title = :title WHERE id = {}", id_num_in);
    let mut stmt = db.conn.prepare(&title_stmt)?;
    stmt.execute_named(&[(":title", &new_title)])?;
    let body_stmt = format!("UPDATE posts SET body = :body WHERE id = {}", id_num_in);
    let mut stmt = db.conn.prepare(&body_stmt)?;
    all.replace(
        id_num_in,
        db::Post {
            author: user.into(),
            title: new_title,
            body: trimmed_body.to_string(),
        },
    );

    stmt.execute_named(&[(":body", &new_body)])?;

    Ok(())
}

// Helper to just run a sql statement.
pub fn exec_stmt_no_params(stmt: &mut rusqlite::Statement) -> error::Result<()> {
    stmt.execute(rusqlite::NO_PARAMS)?;
    all.write();

    println!();
    Ok(())
}

// First handler to remove a post
pub fn delete_handler(db: &db::Conn, id: u32) -> error::Result<()> {
    let id_num_in: u32 = if id == 0 {
pub fn delete_handler(id: usize) -> error::Result<()> {
    let mut id_num_in = if id == 0 {
        println!();
        println!("ID of the post to delete?");
        let mut id_num_in = String::new();


@@ 190,53 154,20 @@ pub fn delete_handler(db: &db::Conn, id: u32) -> error::Result<()> {
        id
    };

    let del_stmt = format!("DELETE FROM posts WHERE id = {}", id_num_in);
    let get_stmt = format!("SELECT * FROM posts WHERE id = {}", id_num_in);

    let mut get_stmt = db.conn.prepare(&get_stmt)?;
    let mut del_stmt = db.conn.prepare(&del_stmt)?;
    id_num_in -= 1;

    let user_in_post: String = get_stmt.query_row(rusqlite::NO_PARAMS, |row| row.get(2))?;
    let mut all = db::Posts::get_all(db::PATH);
    let post = all.get(id_num_in);

    if *user::NAME != user_in_post {
    if *user::NAME != post.author {
        println!();
        println!("Users don't match. Can't delete!");
        println!("Users don't match. Can't delete post!");
        println!();
        return Ok(());
        std::process::exit(1);
    }

    exec_stmt_no_params(&mut del_stmt)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn post_new() {
        let db = db::Conn::init(":memory:");
        let db = db::Conn { conn: db };
        let mut stmt = db
            .conn
            .prepare("INSERT INTO posts (title, author, body) VALUES (:title, :author, :body)")
            .unwrap();

        let title = String::from("TEST TITLE");

        exec_new(&mut stmt, &title, "TEST BODY").unwrap();
        update("NEW TITLE", "TEST BODY", 1, &db).unwrap();
    all.delete(id_num_in);
    all.write();

        let mut stmt = db
            .conn
            .prepare("SELECT * FROM posts WHERE title = :title")
            .unwrap();

        let title = String::from("NEW TITLE");
        let out: String = stmt
            .query_row_named(&[(":title", &title)], |row| row.get::<usize, String>(1))
            .unwrap();

        assert_eq!("NEW TITLE", &out);
    }
    Ok(())
}