From a1945c630b9d9fe3f23def85a918019e05a1e109 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:52:25 -0500 Subject: [PATCH 01/20] feat: stub out application --- Cargo.lock | 305 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/cli.rs | 14 +++ src/lib.rs | 12 +++ src/main.rs | 9 +- 5 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 Cargo.lock create mode 100644 src/cli.rs create mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d266337 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,305 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fs_log_monitor" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.38.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terminal_size" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 42221dd..41a35b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.93" +clap = { version = "4.5.20", features = ["cargo", "env", "derive", "wrap_help"] } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..a9c0f62 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,14 @@ +use clap::Parser; + +#[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Default)] +#[command( + author, + version, + about, + long_about = "Monitors the FreeFileSync logs and reports errors" +)] +pub struct Cli { + /// Print state only and exit + #[arg(long, short)] + pub print_state_only: bool, // TODO 1: Implement debugging tool +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..97eef0c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +// TODO 1: Read log and see if it has any errors +// TODO 2: Send notification on errors detected +// TODO 3: Send still alive notification +// TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours + +mod cli; + +pub use cli::Cli; + +pub fn run(cli: &Cli) -> anyhow::Result<()> { + todo!() +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..4c20511 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,8 @@ -fn main() { - println!("Hello, world!"); +use clap::Parser; +use fs_log_monitor::{run, Cli}; + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + run(&cli)?; + Ok(()) } From 0c40057795235efb832af0afe3bd7136b03239f7 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:54:21 -0500 Subject: [PATCH 02/20] feat: add more stubs and notification Copy from conn_mon --- Cargo.lock | 912 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + src/cli.rs | 22 +- src/lib.rs | 30 +- src/main.rs | 9 +- src/notification.rs | 6 + src/notification/discord.rs | 48 ++ src/notification/email.rs | 82 ++++ src/state.rs | 31 ++ 9 files changed, 1137 insertions(+), 6 deletions(-) create mode 100644 src/notification.rs create mode 100644 src/notification/discord.rs create mode 100644 src/notification/email.rs create mode 100644 src/state.rs diff --git a/Cargo.lock b/Cargo.lock index d266337..fef3c24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,39 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anstream" version = "0.6.18" @@ -57,12 +90,64 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "cc" +version = "1.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown", + "stacker", +] + [[package]] name = "clap" version = "4.5.20" @@ -110,6 +195,49 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "email-encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + [[package]] name = "errno" version = "0.3.9" @@ -120,12 +248,94 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs_log_monitor" version = "0.1.0" dependencies = [ "anyhow", "clap", + "lettre", + "serde", + "serde_json", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", ] [[package]] @@ -134,12 +344,205 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows", +] + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lettre" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9" +dependencies = [ + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "percent-encoding", + "quoted_printable", + "socket2", + "tokio", + "url", +] + [[package]] name = "libc" version = "0.2.161" @@ -152,6 +555,167 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "proc-macro2" version = "1.0.89" @@ -161,6 +725,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +dependencies = [ + "cc", +] + [[package]] name = "quote" version = "1.0.37" @@ -170,6 +743,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustix" version = "0.38.39" @@ -183,6 +768,126 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "strsim" version = "0.11.1" @@ -200,6 +905,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "terminal_size" version = "0.4.0" @@ -210,18 +939,102 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "url" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -303,3 +1116,102 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 41a35b1..e67052c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] anyhow = "1.0.93" clap = { version = "4.5.20", features = ["cargo", "env", "derive", "wrap_help"] } +lettre = "0.11.10" +serde = { version = "1.0.214", features = ["derive"] } +serde_json = "1.0.132" diff --git a/src/cli.rs b/src/cli.rs index a9c0f62..0ad0413 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,10 +5,28 @@ use clap::Parser; author, version, about, - long_about = "Monitors the FreeFileSync logs and reports errors" + long_about = "Monitors the FreeFileSync logs and reports errors + + +Built under the assumption that it will be run infrequently (for instance retry policy will take quite a while)" )] + pub struct Cli { /// Print state only and exit #[arg(long, short)] - pub print_state_only: bool, // TODO 1: Implement debugging tool + pub print_state_only: bool, + + /// Specify the state file to use + #[arg( + default_value_t = String::from("state.ron") + )] + pub state_file: String, + + /// Create a new state file with the user supplied log folder + #[arg(long)] + pub init: Option, + + /// Send a test notification + #[arg(long)] + pub test_notification: Option, } diff --git a/src/lib.rs b/src/lib.rs index 97eef0c..148a514 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,40 @@ +// TODO 1: Get sample logs to work with from server // TODO 1: Read log and see if it has any errors // TODO 2: Send notification on errors detected // TODO 3: Send still alive notification // TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours mod cli; +mod notification; +mod state; +use std::path::Path; + +use anyhow::Context; pub use cli::Cli; +use notification::send_notification; +use state::State; pub fn run(cli: &Cli) -> anyhow::Result<()> { - todo!() + let mut state = State::load(&cli.state_file).context("failed to load state")?; + if cli.print_state_only { + println!("{state:?}"); + return Ok(()); + } + if let Some(msg) = &cli.test_notification { + send_notification(msg).context("sending test notification failed")?; + return Ok(()); + } + // TODO 1: Fill in body + if state.is_changed() { + state + .save(&cli.state_file) + .context("failed to save state")?; + } + Ok(()) +} + +pub fn init_state>(logs: P, state_file: P) -> anyhow::Result<()> { + let mut state = State::new(logs.as_ref().to_path_buf(), &state_file)?; + state.save(state_file) } diff --git a/src/main.rs b/src/main.rs index 4c20511..74810a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ +use anyhow::Context; use clap::Parser; -use fs_log_monitor::{run, Cli}; +use fs_log_monitor::{init_state, run, Cli}; fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - run(&cli)?; - Ok(()) + if let Some(logs_path) = &cli.init { + return init_state(logs_path, &cli.state_file).context("failed to initialize state"); + } + run(&cli) } diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..e8b8f09 --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,6 @@ +mod discord; +mod email; + +pub fn send_notification(msg: &str) -> anyhow::Result<()> { + todo!() +} diff --git a/src/notification/discord.rs b/src/notification/discord.rs new file mode 100644 index 0000000..d5f4edf --- /dev/null +++ b/src/notification/discord.rs @@ -0,0 +1,48 @@ +use std::{fs, time::Duration}; + +use anyhow::{bail, Context}; + +pub struct Discord { + url: String, +} + +impl Discord { + const RETRY_ATTEMPTS: u8 = 3; + const INTERVAL_BETWEEN_RETRY: Duration = Duration::from_secs(30); + + pub fn new() -> anyhow::Result { + let filename = "d.data"; + let url_suffix = fs::read_to_string(filename).with_context(|| { + format!("failed to read discord webhook url suffix from {filename:?}") + })?; + let url = format!("https://discord.com/api/webhooks/{url_suffix}"); + Ok(Self { url }) + } + + pub fn send(&self, msg: &str) -> anyhow::Result<()> { + for i in 0..Self::RETRY_ATTEMPTS { + // Wait before trying again + if i > 0 { + std::thread::sleep(Self::INTERVAL_BETWEEN_RETRY); + } + + todo!("Send message using blocking reqwest"); + + // match self + // .rt + // .block_on(self.do_send(msg)) + // .context("failed to send ") + // { + // Ok(()) => return Ok(()), + // Err(e) => error!( + // "attempt #{} failed to send via discord. Error: {e:?}", + // i + 1 + // ), + // } + } + bail!( + "failed to send via discord after {} attempts", + Self::RETRY_ATTEMPTS + ) + } +} diff --git a/src/notification/email.rs b/src/notification/email.rs new file mode 100644 index 0000000..ab09f5f --- /dev/null +++ b/src/notification/email.rs @@ -0,0 +1,82 @@ +use std::fs; + +use anyhow::Context; +use lettre::message::Mailbox; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{Message, SmtpTransport, Transport}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct EmailConfig { + from_name: String, + pass: String, + #[serde(default = "EmailConfig::default_from_email")] + from_email: String, + #[serde(default = "EmailConfig::default_to_email")] + to_email: String, + #[serde(default = "EmailConfig::default_subject")] + subject: String, +} + +impl EmailConfig { + fn default_from_email() -> String { + "wykies.notices@gmail.com".to_string() + } + fn default_to_email() -> String { + "it@wykies.com".to_string() + } + fn default_subject() -> String { + "Notification from connection monitor".to_string() + } +} + +pub struct Email { + from_mailbox: Mailbox, + to_mailbox: Mailbox, + subject: String, + transport: SmtpTransport, +} +impl Email { + pub fn new() -> anyhow::Result { + let filename = "e.data"; + let file_contents = fs::read_to_string(filename) + .with_context(|| format!("failed to read email settings from {filename:?}"))?; + let email_config: EmailConfig = serde_json::from_str(&file_contents) + .with_context(|| format!("failed to parse contents of {filename:?} as email config"))?; + let from_mailbox = Mailbox { + name: Some(email_config.from_name), + email: email_config + .from_email + .parse() + .context("failed to parse from email address")?, + }; + let to_mailbox: Mailbox = email_config + .to_email + .parse() + .context("failed to parse to email address")?; + let transport = SmtpTransport::relay("smtp.gmail.com") + .context("failed to build SmtpTransport")? + .credentials(Credentials::new(email_config.from_email, email_config.pass)) + .build(); + let subject = email_config.subject; + Ok(Self { + from_mailbox, + to_mailbox, + subject, + transport, + }) + } + + pub fn send(&self, msg: &str) -> anyhow::Result<()> { + let email = Message::builder() + .from(self.from_mailbox.clone()) + .to(self.to_mailbox.clone()) + .subject(self.subject.clone()) + .body(msg.to_string())?; + self.transport + .send(&email) + .context("failed to send email")?; + Ok(()) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..9bc61cb --- /dev/null +++ b/src/state.rs @@ -0,0 +1,31 @@ +use std::path::{Path, PathBuf}; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct State { + logs_dir: PathBuf, + #[serde(skip)] + is_changed: bool, +} +impl State { + pub fn is_changed(&self) -> bool { + self.is_changed + } + + pub(crate) fn save>(&mut self, path: P) -> anyhow::Result<()> { + todo!(); + self.is_changed = false; + Ok(()) + } + + pub(crate) fn load>(path: P) -> anyhow::Result { + todo!() + } + + pub(crate) fn new>(logs_dir: PathBuf, state_file: P) -> anyhow::Result { + Ok(Self { + logs_dir, + is_changed: Default::default(), + }) + } +} From a641a22a879d433acf84a7e0871465eeaa827b1d Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:10:02 -0500 Subject: [PATCH 03/20] feat: add alive msg time to state --- Cargo.lock | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/lib.rs | 16 +++---- src/state.rs | 14 ++++-- 4 files changed, 149 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fef3c24..27dff95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -123,6 +138,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "cc" version = "1.1.36" @@ -138,6 +159,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + [[package]] name = "chumsky" version = "0.9.3" @@ -283,6 +317,7 @@ name = "fs_log_monitor" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "clap", "lettre", "serde", @@ -367,6 +402,29 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -518,6 +576,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lettre" version = "0.11.10" @@ -633,6 +700,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.5" @@ -1016,6 +1092,61 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + [[package]] name = "windows" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index e67052c..7faf0fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.93" +chrono = { version = "0.4.38", default-features = false, features = ["clock", "serde"] } clap = { version = "4.5.20", features = ["cargo", "env", "derive", "wrap_help"] } lettre = "0.11.10" serde = { version = "1.0.214", features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs index 148a514..939a60e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,4 @@ // TODO 1: Get sample logs to work with from server -// TODO 1: Read log and see if it has any errors -// TODO 2: Send notification on errors detected -// TODO 3: Send still alive notification -// TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours mod cli; mod notification; @@ -13,10 +9,10 @@ use std::path::Path; use anyhow::Context; pub use cli::Cli; use notification::send_notification; -use state::State; +use state::AppState; pub fn run(cli: &Cli) -> anyhow::Result<()> { - let mut state = State::load(&cli.state_file).context("failed to load state")?; + let mut state = AppState::load(&cli.state_file).context("failed to load state")?; if cli.print_state_only { println!("{state:?}"); return Ok(()); @@ -25,7 +21,11 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { send_notification(msg).context("sending test notification failed")?; return Ok(()); } - // TODO 1: Fill in body + + // TODO 1: Read log and see if it has any errors + // TODO 2: Send notification on errors detected + // TODO 3: Send still alive notification + // TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours if state.is_changed() { state .save(&cli.state_file) @@ -35,6 +35,6 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { } pub fn init_state>(logs: P, state_file: P) -> anyhow::Result<()> { - let mut state = State::new(logs.as_ref().to_path_buf(), &state_file)?; + let mut state = AppState::new(logs.as_ref().to_path_buf()); state.save(state_file) } diff --git a/src/state.rs b/src/state.rs index 9bc61cb..1216885 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,13 +1,16 @@ use std::path::{Path, PathBuf}; +use chrono::{DateTime, Local}; + #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] -pub struct State { +pub struct AppState { + last_alive_message: DateTime, logs_dir: PathBuf, #[serde(skip)] is_changed: bool, } -impl State { +impl AppState { pub fn is_changed(&self) -> bool { self.is_changed } @@ -22,10 +25,11 @@ impl State { todo!() } - pub(crate) fn new>(logs_dir: PathBuf, state_file: P) -> anyhow::Result { - Ok(Self { + pub(crate) fn new(logs_dir: PathBuf) -> Self { + Self { + last_alive_message: Local::now(), logs_dir, is_changed: Default::default(), - }) + } } } From acdd4e464440d42f2c80206372effeefd0864890 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:31:13 -0500 Subject: [PATCH 04/20] feat: save and load state --- .gitignore | 1 + Cargo.lock | 26 ++++++++++++++++++++++++-- Cargo.toml | 1 + src/lib.rs | 2 +- src/state.rs | 45 +++++++++++++++++++++++++++++++++++++++------ 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..354d796 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +state.ron diff --git a/Cargo.lock b/Cargo.lock index 27dff95..3059aee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -137,6 +143,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bumpalo" @@ -262,7 +271,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" dependencies = [ - "base64", + "base64 0.22.1", "memchr", ] @@ -320,6 +329,7 @@ dependencies = [ "chrono", "clap", "lettre", + "ron", "serde", "serde_json", ] @@ -591,7 +601,7 @@ version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9" dependencies = [ - "base64", + "base64 0.22.1", "chumsky", "email-encoding", "email_address", @@ -825,6 +835,18 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/Cargo.toml b/Cargo.toml index 7faf0fe..144ce89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,6 @@ anyhow = "1.0.93" chrono = { version = "0.4.38", default-features = false, features = ["clock", "serde"] } clap = { version = "4.5.20", features = ["cargo", "env", "derive", "wrap_help"] } lettre = "0.11.10" +ron = "0.8.1" serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" diff --git a/src/lib.rs b/src/lib.rs index 939a60e..e216fe3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ use state::AppState; pub fn run(cli: &Cli) -> anyhow::Result<()> { let mut state = AppState::load(&cli.state_file).context("failed to load state")?; if cli.print_state_only { - println!("{state:?}"); + println!("{state:#?}"); return Ok(()); } if let Some(msg) = &cli.test_notification { diff --git a/src/state.rs b/src/state.rs index 1216885..765d2f5 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,11 +1,17 @@ -use std::path::{Path, PathBuf}; +use std::{ + fs, + io::Write as _, + path::{Path, PathBuf}, +}; -use chrono::{DateTime, Local}; +use anyhow::Context; +use chrono::{DateTime, Local, NaiveTime}; #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct AppState { - last_alive_message: DateTime, + last_alive_msg: DateTime, + alive_msg_time: Option, logs_dir: PathBuf, #[serde(skip)] is_changed: bool, @@ -16,18 +22,45 @@ impl AppState { } pub(crate) fn save>(&mut self, path: P) -> anyhow::Result<()> { - todo!(); + let s = ron::ser::to_string_pretty(&self, ron::ser::PrettyConfig::default()) + .context("failed to convert to ron")?; + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&path) + .with_context(|| { + format!("failed to open file to save AppState: {:?}", path.as_ref()) + })?; + file.write_all(s.as_bytes()).with_context(|| { + format!( + "failed to write to file to save AppState: {:?}", + path.as_ref() + ) + })?; self.is_changed = false; Ok(()) } pub(crate) fn load>(path: P) -> anyhow::Result { - todo!() + let s = fs::read_to_string(&path) + .with_context(|| format!("failed to read file for AppState: {:?}", path.as_ref()))?; + ron::from_str(&s).with_context(|| { + format!( + "failed to deserialize AppState from contents of {:?}", + path.as_ref(), + ) + }) } pub(crate) fn new(logs_dir: PathBuf) -> Self { Self { - last_alive_message: Local::now(), + last_alive_msg: Local::now(), + // TODO 4: Ensure there is a test to make sure this constant is correct + alive_msg_time: Some( + NaiveTime::from_hms_opt(7, 0, 0) + .expect("should be valid as it is set at build time"), + ), logs_dir, is_changed: Default::default(), } From 1cf5c56b10494c6a83b4828109fb531be33ba3f1 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:50:25 -0500 Subject: [PATCH 05/20] feat: implement check for if alive msg send due --- src/lib.rs | 13 ++++++++----- src/state.rs | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e216fe3..f068f0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,22 +12,25 @@ use notification::send_notification; use state::AppState; pub fn run(cli: &Cli) -> anyhow::Result<()> { - let mut state = AppState::load(&cli.state_file).context("failed to load state")?; + let mut app_state = AppState::load(&cli.state_file).context("failed to load state")?; if cli.print_state_only { - println!("{state:#?}"); + println!("{app_state:#?}"); return Ok(()); } if let Some(msg) = &cli.test_notification { send_notification(msg).context("sending test notification failed")?; return Ok(()); } + if app_state.alive_msg_due() { + let alive_msg = app_state.generate_alive_msg(); + send_notification(&alive_msg).context("failed to send alive message")?; + } // TODO 1: Read log and see if it has any errors // TODO 2: Send notification on errors detected - // TODO 3: Send still alive notification // TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours - if state.is_changed() { - state + if app_state.is_changed() { + app_state .save(&cli.state_file) .context("failed to save state")?; } diff --git a/src/state.rs b/src/state.rs index 765d2f5..4532f47 100644 --- a/src/state.rs +++ b/src/state.rs @@ -65,4 +65,25 @@ impl AppState { is_changed: Default::default(), } } + + pub(crate) fn generate_alive_msg(&mut self) -> String { + self.last_alive_msg = Local::now(); + "FS Log Monitor still working".to_string() + } + + /// Due if message not sent for the day and past the time to send the message + pub(crate) fn alive_msg_due(&self) -> bool { + if let Some(send_time) = self.alive_msg_time { + let now = Local::now(); + if self.last_alive_msg.date_naive() != now.date_naive() { + now.time() >= send_time + } else { + // Same date as last message no due yet + false + } + } else { + // Always false not set to be sent + false + } + } } From 1cc36ab825b6fa5213b4845ce6833f57c84de2c9 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:30:54 -0500 Subject: [PATCH 06/20] feat: start setup to test notifications --- bacon.toml | 89 +++++++++++++++++++++++++++++++++++++ src/notification.rs | 7 ++- src/notification/discord.rs | 2 +- src/notification/email.rs | 2 +- 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 bacon.toml diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 0000000..162b19a --- /dev/null +++ b/bacon.toml @@ -0,0 +1,89 @@ +# This is a configuration file for the bacon tool +# +# Bacon repository: https://github.com/Canop/bacon +# Complete help on configuration: https://dystroy.org/bacon/config/ +# You can also check bacon's own bacon.toml file +# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml + +default_job = "check" + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false + +[jobs.clippy] +command = [ + "cargo", + "clippy", + "--all-targets", + "--color", + "always", +] +need_stdout = false + +# This job lets you run +# - all tests: bacon test +# - a specific test: bacon test -- config::test_default_files +# - the tests of a package: bacon test -- -- -p config +[jobs.test] +command = [ + "cargo", + "test", + "--color", + "always", + "--", + "--color", + "always", # see https://github.com/Canop/bacon/issues/124 +] +need_stdout = true + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# *if* it makes sense for this crate. +# Don't forget the `--color always` part or the errors won't be +# properly parsed. +# If your program never stops (eg a server), you may set `background` +# to false to have the cargo run output immediately displayed instead +# of waiting for program's end. +[jobs.run] +command = [ + "cargo", + "run", + "--color", + "always", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +[jobs.run_notify] +command = [ + "cargo", + "run", + "--color", + "always", + "--", + "--test-notification", + "bacon test notification", +] +need_stdout = true +allow_warnings = true +background = true + +[keybindings] +n = "job:run_notify" diff --git a/src/notification.rs b/src/notification.rs index e8b8f09..9010151 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -2,5 +2,10 @@ mod discord; mod email; pub fn send_notification(msg: &str) -> anyhow::Result<()> { - todo!() + match discord::Discord::send(msg) { + Ok(()) => return Ok(()), + Err(e) => eprintln!("{e:?}"), + }; + + email::Email::send(msg) } diff --git a/src/notification/discord.rs b/src/notification/discord.rs index d5f4edf..8d3755d 100644 --- a/src/notification/discord.rs +++ b/src/notification/discord.rs @@ -19,7 +19,7 @@ impl Discord { Ok(Self { url }) } - pub fn send(&self, msg: &str) -> anyhow::Result<()> { + pub fn send(msg: &str) -> anyhow::Result<()> { for i in 0..Self::RETRY_ATTEMPTS { // Wait before trying again if i > 0 { diff --git a/src/notification/email.rs b/src/notification/email.rs index ab09f5f..4b077df 100644 --- a/src/notification/email.rs +++ b/src/notification/email.rs @@ -68,7 +68,7 @@ impl Email { }) } - pub fn send(&self, msg: &str) -> anyhow::Result<()> { + pub fn send(msg: &str) -> anyhow::Result<()> { let email = Message::builder() .from(self.from_mailbox.clone()) .to(self.to_mailbox.clone()) From 3f4fbe9e120c23d5d7cff1555ff6414c786328b5 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:35:14 -0500 Subject: [PATCH 07/20] feat: extract config dir to pass for notifications --- src/lib.rs | 27 ++++++++++++++++++++++----- src/notification.rs | 8 +++++--- src/notification/discord.rs | 4 ++-- src/notification/email.rs | 24 ++++++++++++++---------- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f068f0b..0bd63e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,26 +4,27 @@ mod cli; mod notification; mod state; -use std::path::Path; +use std::path::{Path, PathBuf}; -use anyhow::Context; +use anyhow::{anyhow, Context}; pub use cli::Cli; use notification::send_notification; use state::AppState; pub fn run(cli: &Cli) -> anyhow::Result<()> { - let mut app_state = AppState::load(&cli.state_file).context("failed to load state")?; + let (state_file, config_folder) = get_canonical_folder_and_filename(&cli.state_file)?; + let mut app_state = AppState::load(state_file).context("failed to load state")?; if cli.print_state_only { println!("{app_state:#?}"); return Ok(()); } if let Some(msg) = &cli.test_notification { - send_notification(msg).context("sending test notification failed")?; + send_notification(msg, &config_folder).context("sending test notification failed")?; return Ok(()); } if app_state.alive_msg_due() { let alive_msg = app_state.generate_alive_msg(); - send_notification(&alive_msg).context("failed to send alive message")?; + send_notification(&alive_msg, &config_folder).context("failed to send alive message")?; } // TODO 1: Read log and see if it has any errors @@ -37,6 +38,22 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { Ok(()) } +/// Converts the input into it's canonical form and based on the assumption that it is a file also returns the parent folder +fn get_canonical_folder_and_filename>( + file_path: P, +) -> anyhow::Result<(PathBuf, PathBuf)> { + // TODO 4: Test what this error looks like and if we need to add more context + let canonical_file_path = file_path.as_ref().canonicalize()?; + let parent_folder = canonical_file_path + .parent() + .ok_or(anyhow!( + "failed to get parent folder for file: {}", + canonical_file_path.display() + ))? + .to_path_buf(); + Ok((canonical_file_path, parent_folder)) +} + pub fn init_state>(logs: P, state_file: P) -> anyhow::Result<()> { let mut state = AppState::new(logs.as_ref().to_path_buf()); state.save(state_file) diff --git a/src/notification.rs b/src/notification.rs index 9010151..6d1c538 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -1,11 +1,13 @@ +use std::path::Path; + mod discord; mod email; -pub fn send_notification(msg: &str) -> anyhow::Result<()> { - match discord::Discord::send(msg) { +pub fn send_notification(msg: &str, config_folder: &Path) -> anyhow::Result<()> { + match discord::Discord::send(msg, config_folder) { Ok(()) => return Ok(()), Err(e) => eprintln!("{e:?}"), }; - email::Email::send(msg) + email::Email::send(msg, config_folder) } diff --git a/src/notification/discord.rs b/src/notification/discord.rs index 8d3755d..01f4744 100644 --- a/src/notification/discord.rs +++ b/src/notification/discord.rs @@ -1,4 +1,4 @@ -use std::{fs, time::Duration}; +use std::{fs, path::Path, time::Duration}; use anyhow::{bail, Context}; @@ -19,7 +19,7 @@ impl Discord { Ok(Self { url }) } - pub fn send(msg: &str) -> anyhow::Result<()> { + pub fn send(msg: &str, config_folder: &Path) -> anyhow::Result<()> { for i in 0..Self::RETRY_ATTEMPTS { // Wait before trying again if i > 0 { diff --git a/src/notification/email.rs b/src/notification/email.rs index 4b077df..4f6f3b1 100644 --- a/src/notification/email.rs +++ b/src/notification/email.rs @@ -1,4 +1,5 @@ use std::fs; +use std::path::Path; use anyhow::Context; use lettre::message::Mailbox; @@ -38,9 +39,9 @@ pub struct Email { transport: SmtpTransport, } impl Email { - pub fn new() -> anyhow::Result { - let filename = "e.data"; - let file_contents = fs::read_to_string(filename) + pub fn new(config_folder: &Path) -> anyhow::Result { + let filename = config_folder.join("e.data"); + let file_contents = fs::read_to_string(&filename) .with_context(|| format!("failed to read email settings from {filename:?}"))?; let email_config: EmailConfig = serde_json::from_str(&file_contents) .with_context(|| format!("failed to parse contents of {filename:?} as email config"))?; @@ -68,14 +69,17 @@ impl Email { }) } - pub fn send(msg: &str) -> anyhow::Result<()> { - let email = Message::builder() - .from(self.from_mailbox.clone()) - .to(self.to_mailbox.clone()) - .subject(self.subject.clone()) + pub fn send(msg: &str, config_folder: &Path) -> anyhow::Result<()> { + let email = Self::new(config_folder)?; + + let email_msg = Message::builder() + .from(email.from_mailbox.clone()) + .to(email.to_mailbox.clone()) + .subject(email.subject.clone()) .body(msg.to_string())?; - self.transport - .send(&email) + email + .transport + .send(&email_msg) .context("failed to send email")?; Ok(()) } From 28c407e75879b01f0b4cc54974df09e9e868adb3 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:58:06 -0500 Subject: [PATCH 08/20] feat: convert discord to use blocking reqwest --- Cargo.lock | 508 +++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/notification/discord.rs | 39 +-- 3 files changed, 531 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3059aee..ab35c69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,12 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -153,6 +159,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + [[package]] name = "cc" version = "1.1.36" @@ -187,7 +199,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "stacker", ] @@ -281,6 +293,21 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -297,6 +324,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -329,11 +362,22 @@ dependencies = [ "chrono", "clap", "lettre", + "reqwest", "ron", "serde", "serde_json", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -346,6 +390,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -360,6 +410,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -367,12 +418,42 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -383,6 +464,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + [[package]] name = "heck" version = "0.5.0" @@ -406,12 +493,124 @@ dependencies = [ "windows", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -574,6 +773,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.1", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -835,6 +1050,65 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ron" version = "0.8.1" @@ -866,6 +1140,45 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.18" @@ -936,6 +1249,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -967,6 +1292,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -992,6 +1323,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.87" @@ -1003,6 +1340,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -1014,6 +1360,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.13.0" @@ -1054,6 +1421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", + "bytes", "libc", "mio", "pin-project-lite", @@ -1061,12 +1429,83 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.3" @@ -1108,6 +1547,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1140,6 +1588,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.95" @@ -1169,6 +1629,16 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows" version = "0.52.0" @@ -1188,6 +1658,36 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1347,6 +1847,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index 144ce89..1b176cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1.0.93" chrono = { version = "0.4.38", default-features = false, features = ["clock", "serde"] } clap = { version = "4.5.20", features = ["cargo", "env", "derive", "wrap_help"] } lettre = "0.11.10" +reqwest = { version = "0.12.9", features = ["blocking"] } ron = "0.8.1" serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" diff --git a/src/notification/discord.rs b/src/notification/discord.rs index 01f4744..b0400d0 100644 --- a/src/notification/discord.rs +++ b/src/notification/discord.rs @@ -1,6 +1,7 @@ use std::{fs, path::Path, time::Duration}; use anyhow::{bail, Context}; +use serde_json::json; pub struct Discord { url: String, @@ -10,9 +11,9 @@ impl Discord { const RETRY_ATTEMPTS: u8 = 3; const INTERVAL_BETWEEN_RETRY: Duration = Duration::from_secs(30); - pub fn new() -> anyhow::Result { - let filename = "d.data"; - let url_suffix = fs::read_to_string(filename).with_context(|| { + pub fn new(config_folder: &Path) -> anyhow::Result { + let filename = config_folder.join("d.data"); + let url_suffix = fs::read_to_string(&filename).with_context(|| { format!("failed to read discord webhook url suffix from {filename:?}") })?; let url = format!("https://discord.com/api/webhooks/{url_suffix}"); @@ -20,25 +21,21 @@ impl Discord { } pub fn send(msg: &str, config_folder: &Path) -> anyhow::Result<()> { + let discord = Self::new(config_folder)?; + for i in 0..Self::RETRY_ATTEMPTS { // Wait before trying again if i > 0 { std::thread::sleep(Self::INTERVAL_BETWEEN_RETRY); } - todo!("Send message using blocking reqwest"); - - // match self - // .rt - // .block_on(self.do_send(msg)) - // .context("failed to send ") - // { - // Ok(()) => return Ok(()), - // Err(e) => error!( - // "attempt #{} failed to send via discord. Error: {e:?}", - // i + 1 - // ), - // } + match send_blocking_reqwest(msg, &discord.url) { + Ok(()) => return Ok(()), + Err(e) => eprintln!( + "attempt #{} failed to send via discord. Error: {e:?}", + i + 1 + ), + } } bail!( "failed to send via discord after {} attempts", @@ -46,3 +43,13 @@ impl Discord { ) } } + +fn send_blocking_reqwest(msg: &str, url: &str) -> anyhow::Result<()> { + let resp = reqwest::blocking::Client::new() + .request(reqwest::Method::POST, url) + .header("Content-Type", "application/json") + .body(json!({ "content": msg }).to_string()) + .send()?; + dbg!(resp); + Ok(()) +} From 6f3110cc9af09571ac73057f79284e6deea49c16 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:13:58 -0500 Subject: [PATCH 09/20] feat: add output to confirm successful run --- .gitignore | 1 + src/lib.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 354d796..e9c87b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target state.ron +/*.data \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 0bd63e1..6f837e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { } if let Some(msg) = &cli.test_notification { send_notification(msg, &config_folder).context("sending test notification failed")?; + println!("TEST NOTIFICATION SENT"); return Ok(()); } if app_state.alive_msg_due() { @@ -35,6 +36,7 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { .save(&cli.state_file) .context("failed to save state")?; } + println!("RUN COMPLETED"); Ok(()) } From d84bca1901959e8acfb6661d7e5c5fc1587a42a9 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:30:10 -0500 Subject: [PATCH 10/20] fix: correct check for success --- bacon.toml | 2 +- src/notification/discord.rs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/bacon.toml b/bacon.toml index 162b19a..f570c7d 100644 --- a/bacon.toml +++ b/bacon.toml @@ -83,7 +83,7 @@ command = [ ] need_stdout = true allow_warnings = true -background = true +background = false [keybindings] n = "job:run_notify" diff --git a/src/notification/discord.rs b/src/notification/discord.rs index b0400d0..453e6ef 100644 --- a/src/notification/discord.rs +++ b/src/notification/discord.rs @@ -26,13 +26,17 @@ impl Discord { for i in 0..Self::RETRY_ATTEMPTS { // Wait before trying again if i > 0 { + eprintln!( + "Going to sleep for {} seconds before retrying discord", + Self::INTERVAL_BETWEEN_RETRY.as_secs() + ); std::thread::sleep(Self::INTERVAL_BETWEEN_RETRY); } match send_blocking_reqwest(msg, &discord.url) { Ok(()) => return Ok(()), Err(e) => eprintln!( - "attempt #{} failed to send via discord. Error: {e:?}", + "attempt #{} failed to send via discord with msg: {e:?}", i + 1 ), } @@ -50,6 +54,13 @@ fn send_blocking_reqwest(msg: &str, url: &str) -> anyhow::Result<()> { .header("Content-Type", "application/json") .body(json!({ "content": msg }).to_string()) .send()?; - dbg!(resp); - Ok(()) + if resp.status().is_success() { + Ok(()) + } else { + let unexpected_response_value = + resp.error_for_status().context("error response returned")?; + + // Assumption the code above will always result in an error being returned + bail!("not a successful status but conversion to error still failed? Response: {unexpected_response_value:?}") + } } From e09957cb105b2676ad2ac35f9abbf41a2f5c4afc Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:44:54 -0500 Subject: [PATCH 11/20] feat: add context for failed canonicalization --- src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6f837e1..ccb2f57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,8 +44,16 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { fn get_canonical_folder_and_filename>( file_path: P, ) -> anyhow::Result<(PathBuf, PathBuf)> { - // TODO 4: Test what this error looks like and if we need to add more context - let canonical_file_path = file_path.as_ref().canonicalize()?; + let canonical_file_path = file_path.as_ref().canonicalize().with_context(|| { + format!( + "failed to get canonical version of: {:?} in working directory: {:?}", + file_path.as_ref(), + match std::env::current_dir() { + Ok(cwd) => cwd.to_string_lossy().to_string(), + Err(e) => format!("[failed {e}]"), + } + ) + })?; let parent_folder = canonical_file_path .parent() .ok_or(anyhow!( From 5d43e18a2c1edabe36791091ab2e58fd0d3662d8 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:54:39 -0500 Subject: [PATCH 12/20] test: add sample log files --- src/lib.rs | 2 - ...ileName 2024-10-15 092845.903 [Error].html | 131 ++++++++++ .../ProfileName 2024-10-15 093609.877.html | 72 ++++++ ...ileName 2024-11-08 140913.247 [Error].html | 232 ++++++++++++++++++ ...eName 2024-11-08 145021.053 [Stopped].html | 153 ++++++++++++ 5 files changed, 588 insertions(+), 2 deletions(-) create mode 100644 test/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html create mode 100644 test/sample_logs/ProfileName 2024-10-15 093609.877.html create mode 100644 test/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html create mode 100644 test/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html diff --git a/src/lib.rs b/src/lib.rs index ccb2f57..3ea6118 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -// TODO 1: Get sample logs to work with from server - mod cli; mod notification; mod state; diff --git a/test/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html b/test/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html new file mode 100644 index 0000000..70db4de --- /dev/null +++ b/test/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html @@ -0,0 +1,131 @@ + + + + + + [FreeFileSync] ProfileName ❌️ + + + +
ProfileName  10/15/2024  09:28:45 AM
+ +
+
+ + Completed with errors +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
Errors and warnings:
+
+ + + + + + +
09:30:19 AMError:Cannot open file "/home/bob/missing_file.pdf".
+ENOENT: No such file or directory [stat]
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
09:28:47 AMInfo:Comparison finished: 196 items found — Time elapsed: 00:00:00
09:28:48 AMInfo:Synchronizing folder pair: Two way <->
+    ProfileName:/sample@gmail.com/bob
+    /home/bob
09:28:48 AMInfo:Updating file "ProfileName:/sample@gmail.com/bob/22.pdf"
09:28:48 AMInfo:Cannot open file "/home/bob/missing_file.pdf".
+ENOENT: No such file or directory [stat]
+-> Automatic retry
09:29:18 AMInfo:Updating file "ProfileName:/sample@gmail.com/bob/22.pdf"
09:29:19 AMInfo:Cannot open file "/home/bob/missing_file.pdf".
+ENOENT: No such file or directory [stat]
+-> Automatic retry
09:29:49 AMInfo:Updating file "ProfileName:/sample@gmail.com/bob/22.pdf"
09:29:49 AMInfo:Cannot open file "/home/bob/missing_file.pdf".
+ENOENT: No such file or directory [stat]
+-> Automatic retry
09:30:19 AMInfo:Updating file "gdrive:/sample@gmail.com/bob/22.pdf"
09:30:19 AMError:Cannot open file "/home/bob/missing_file.pdf".
+ENOENT: No such file or directory [stat]
+ +
+
+ + Ubuntu 22.04 – administrator (FS) – VMware Virtual Platform – VMware, Inc. +
+ + diff --git a/test/sample_logs/ProfileName 2024-10-15 093609.877.html b/test/sample_logs/ProfileName 2024-10-15 093609.877.html new file mode 100644 index 0000000..a3eeb34 --- /dev/null +++ b/test/sample_logs/ProfileName 2024-10-15 093609.877.html @@ -0,0 +1,72 @@ + + + + + + [FreeFileSync] ProfileName ✔️ + + + +
ProfileName  10/15/2024  09:36:09 AM
+ +
+
+ + Completed successfully +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
09:36:11 AMInfo:Comparison finished: 196 items found — Time elapsed: 00:00:00
09:36:12 AMInfo:Synchronizing folder pair: Two way <->
+    ProfileName:/sample@gmail.com/bob
+    /home/bob
09:36:12 AMInfo:Creating file "ProfileName:/sample@gmail.com/bob/new.txt"
09:36:14 AMInfo:Creating file "ProfileName:/sample@gmail.com/bob/new2.txt"
+ +
+
+ + Ubuntu 22.04 – administrator (FS) – VMware Virtual Platform – VMware, Inc. +
+ + diff --git a/test/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html b/test/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html new file mode 100644 index 0000000..c11adbf --- /dev/null +++ b/test/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html @@ -0,0 +1,232 @@ + + + + + + [FreeFileSync] ProfileName ❌️ + + + +
ProfileName  2024-11-08  02:09:13 PM
+ +
+
+ + Completed with errors +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
Errors and warnings:
+
+ + + + + + + + + + + + + + + + + + + + + +
02:12:18 PMError:Cannot read file "ProfileName:/sample@gmail.com/bob/logs.7z".
+CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]
02:12:18 PMError:Cannot delete file "/home/bob/logs-ad8f.ffs_tmp".
+ENOENT: No such file or directory [unlink]
02:12:37 PMError:Cannot read file "ProfileName:/sample@gmail.com/bob/log2.7z".
+CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]
02:12:37 PMError:Cannot delete file "/home/bob/log2-62c0.ffs_tmp".
+ENOENT: No such file or directory [unlink]
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
02:09:16 PMInfo:Comparison finished: 1,890,726 items found — Time elapsed: 00:00:00
02:11:47 PMInfo:Synchronizing folder pair: Two way <->
+    ProfileName:/sample@gmail.com
+    /home/bob
02:11:47 PMInfo:Creating folder "/home/TempProfileNameRoot/Test 3"
02:11:47 PMInfo:Moving file
+"/home/Test 3/Copy of man.txt" to
+"/home/TempProfileNameRoot/Test 3/Copy of man.txt"
02:11:47 PMInfo:Moving folder "/home/Test 3" to the recycle bin
02:11:47 PMInfo:Moving file "/home/bob/191292.pdf" to the recycle bin
02:11:47 PMInfo:Moving file "/home/bob/191293.pdf" to the recycle bin
02:11:47 PMInfo:Moving file "/home/bob/191306.pdf" to the recycle bin
02:11:47 PMInfo:Updating file "/home/bob/s1.txt"
02:11:49 PMInfo:Creating folder "/home/bob/Scans Cashier"
02:11:49 PMInfo:Creating file "/home/bob/191323.pdf"
02:11:50 PMInfo:Creating file "/home/bob/191330.pdf"
02:11:51 PMInfo:Creating file "/home/bob/191342.pdf"
02:11:51 PMInfo:Creating file "/home/bob/2024-11-07 #373.pdf"
02:12:08 PMInfo:Creating file "/home/bob/logs.7z"
02:12:18 PMError:Cannot read file "ProfileName:/sample@gmail.com/bob/logs.7z".
+CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]
02:12:18 PMError:Cannot delete file "/home/bob/logs-ad8f.ffs_tmp".
+ENOENT: No such file or directory [unlink]
02:12:27 PMInfo:Creating file "/home/bob/log2.7z"
02:12:37 PMError:Cannot read file "ProfileName:/sample@gmail.com/bob/log2.7z".
+CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]
02:12:37 PMError:Cannot delete file "/home/bob/log2-62c0.ffs_tmp".
+ENOENT: No such file or directory [unlink]
02:13:04 PMInfo:Creating file "/home/bob/Scans Cashier/191292.pdf"
02:13:05 PMInfo:Creating file "/home/bob/Scans Cashier/191293.pdf"
02:13:07 PMInfo:Creating file "/home/bob/Scans Cashier/191298.pdf"
02:13:08 PMInfo:Creating file "/home/bob/Scans Cashier/191299.pdf"
02:13:08 PMInfo:Creating file "/home/bob/Scans Cashier/191306.pdf"
02:13:10 PMInfo:Creating file "/home/bob/Scans Cashier/191309.pdf"
02:13:11 PMInfo:Creating file "/home/bob/Scans Cashier/191311.pdf"
+ +
+
+ + Ubuntu 22.04 – bob – 20W6001NUS – DELL +
+ + diff --git a/test/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html b/test/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html new file mode 100644 index 0000000..fbd9dad --- /dev/null +++ b/test/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html @@ -0,0 +1,153 @@ + + + + + + [FreeFileSync] ProfileName ❌️ + + + +
ProfileName  11/08/2024  02:50:21 PM
+ +
+
+ + Stopped +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
Errors and warnings:
+
+ + + + + + +
02:52:22 PMError:Stopped
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
02:50:23 PMInfo:Comparison finished: 5,146 items found — Time elapsed: 00:00:01
02:50:25 PMInfo:Synchronizing folder pair: Two way <->
+    ProfileName:/sample@gmail.com/bob
+    /home/administrator/data/bob
02:50:25 PMInfo:Moving folder "ProfileName:/sample@gmail.com/bob/Test2" to the recycle bin
02:50:25 PMInfo:Creating folder "ProfileName:/sample@gmail.com/bob/Logs"
02:50:26 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2023-09-11 210556.515.html"
02:50:27 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 000101.419.html"
02:50:29 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 082243.400.html"
02:50:30 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 083340.283.html"
02:50:31 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 084346.269.html"
02:50:32 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 084523.939.html"
02:50:34 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 084558.559.html"
02:50:35 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 084821.964.html"
02:50:37 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 085051.343.html"
02:52:15 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 121303.543.html"
02:52:16 PMInfo:Creating file "ProfileName:/sample@gmail.com/bob/Logs/ProfileName 2024-10-09 122203.691.html"
02:52:22 PMError:Stopped
+ +
+
+ + Ubuntu 22.04 – administrator (FS) – VMware Virtual Platform – VMware, Inc. +
+ + From e535ae936601da53e2da523c43615b9b447fcdc1 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:15:31 -0500 Subject: [PATCH 13/20] feat: wire up parsing iterating log directory --- src/lib.rs | 84 ++++++++++++++++++++++++++++++++++++++++++++++--- src/log_info.rs | 37 ++++++++++++++++++++++ src/state.rs | 17 +++++++++- 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 src/log_info.rs diff --git a/src/lib.rs b/src/lib.rs index 3ea6118..2ad2e1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,16 @@ mod cli; +mod log_info; mod notification; mod state; -use std::path::{Path, PathBuf}; +use std::{ + fs::read_dir, + path::{Path, PathBuf}, +}; use anyhow::{anyhow, Context}; pub use cli::Cli; +pub use log_info::LogInfo; use notification::send_notification; use state::AppState; @@ -26,9 +31,18 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { send_notification(&alive_msg, &config_folder).context("failed to send alive message")?; } - // TODO 1: Read log and see if it has any errors - // TODO 2: Send notification on errors detected - // TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours + match process_logs_folder(&mut app_state).context("error processing logs") { + Ok(log_infos) => { + if !log_infos.is_empty() { + let err_msg = build_err_msg_from_logs(log_infos); + send_notification(&err_msg, &config_folder) + .context("failed to send notification of errors")? + } + } + Err(e) => send_notification(&e.to_string(), &config_folder) + .context("failed to send notification of processing failure")?, + } + if app_state.is_changed() { app_state .save(&cli.state_file) @@ -38,6 +52,68 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { Ok(()) } +fn build_err_msg_from_logs(log_infos: Vec) -> String { + const MAX_MSG_LEN: usize = 2000; + let log_count = log_infos.len(); + // Stores the running count of `Errors and warnings` found + let mut entry_count = 0; + let mut result = String::new(); + let separator = "---\n"; + for log_info in log_infos { + entry_count += log_info.errors_and_warnings.len(); + result.push_str(separator); + result.push_str(&log_info.to_string()); + } + result.push_str(separator); + let summary = format!("{log_count} logs with {entry_count} error and warnings"); + result.push_str(&summary); + result.push_str(separator); + if result.len() > MAX_MSG_LEN { + // Exceeded limit for discord, only send summary + result = summary; + result.push_str(separator); + result.push_str("DETAILS TOO LONG TO INCLUDE **TRUNCATED**"); + result.push_str(separator); + } + result +} + +/// Returns a list of the new logs with errors +fn process_logs_folder(app_state: &mut AppState) -> anyhow::Result> { + let mut result = Vec::new(); + let mut latest_timestamp = app_state.latest_log_datetime(); + // TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours + for dir_entry in read_dir(app_state.logs_dir()) + .with_context(|| format!("failed to read log folder: {:?}", app_state.logs_dir()))? + { + let dir_entry = dir_entry.with_context(|| { + format!("failed to read entry in folder: {:?}", app_state.logs_dir()) + })?; + + if dir_entry + .file_type() + .with_context(|| format!("failed to get file type for: {:?}", dir_entry.path()))? + .is_file() + { + let mut log_info = LogInfo::new(dir_entry.file_name().to_string_lossy())?; + if log_info.date_time > app_state.latest_log_datetime() { + if log_info.date_time > latest_timestamp { + // Save latest timestamp found + latest_timestamp = log_info.date_time; + } + if log_info.abnormal_outcome.is_some() { + log_info.extract_errors(&dir_entry.path())?; + result.push(log_info); + } + } + } + } + if latest_timestamp > app_state.latest_log_datetime() { + app_state.set_latest_log_datetime(latest_timestamp); + } + Ok(result) +} + /// Converts the input into it's canonical form and based on the assumption that it is a file also returns the parent folder fn get_canonical_folder_and_filename>( file_path: P, diff --git a/src/log_info.rs b/src/log_info.rs new file mode 100644 index 0000000..a20c4a8 --- /dev/null +++ b/src/log_info.rs @@ -0,0 +1,37 @@ +use std::{fmt::Display, path::Path}; + +use chrono::NaiveDateTime; + +pub struct LogInfo { + pub date_time: NaiveDateTime, + pub abnormal_outcome: Option, + pub errors_and_warnings: Vec, +} + +impl LogInfo { + pub fn new>(file_name: S) -> anyhow::Result { + let date_time = todo!(); + let abnormal_outcome = todo!(); + Ok(Self { + date_time, + abnormal_outcome, + errors_and_warnings: Default::default(), + }) + } + + pub fn extract_errors(&mut self, file_path: &Path) -> anyhow::Result<()> { + todo!("read log file and extract errors") + } +} + +impl Display for LogInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} errors: {}\n{}\n", + self.date_time.format("%Y-%m-%d %H:%M:%S"), + self.errors_and_warnings.len(), + self.errors_and_warnings.join("\n") + ) + } +} diff --git a/src/state.rs b/src/state.rs index 4532f47..e4fe854 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::Context; -use chrono::{DateTime, Local, NaiveTime}; +use chrono::{DateTime, Local, NaiveDateTime, NaiveTime}; #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] @@ -13,6 +13,7 @@ pub struct AppState { last_alive_msg: DateTime, alive_msg_time: Option, logs_dir: PathBuf, + latest_log_datetime: NaiveDateTime, #[serde(skip)] is_changed: bool, } @@ -61,6 +62,7 @@ impl AppState { NaiveTime::from_hms_opt(7, 0, 0) .expect("should be valid as it is set at build time"), ), + latest_log_datetime: Local::now().naive_local(), logs_dir, is_changed: Default::default(), } @@ -86,4 +88,17 @@ impl AppState { false } } + + pub fn logs_dir(&self) -> &Path { + &self.logs_dir + } + + pub fn latest_log_datetime(&self) -> NaiveDateTime { + self.latest_log_datetime + } + + pub fn set_latest_log_datetime(&mut self, value: NaiveDateTime) { + self.is_changed = true; + self.latest_log_datetime = value; + } } From 9575108e149f401a6a27858a3d1e0329aebae33f Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:57:40 -0500 Subject: [PATCH 14/20] feat: parse log filenames --- Cargo.lock | 39 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/lib.rs | 4 ++-- src/log_info.rs | 26 +++++++++++++++++++++----- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab35c69..6e52168 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -362,6 +371,7 @@ dependencies = [ "chrono", "clap", "lettre", + "regex", "reqwest", "ron", "serde", @@ -1050,6 +1060,35 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "reqwest" version = "0.12.9" diff --git a/Cargo.toml b/Cargo.toml index 1b176cc..a82252a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1.0.93" chrono = { version = "0.4.38", default-features = false, features = ["clock", "serde"] } clap = { version = "4.5.20", features = ["cargo", "env", "derive", "wrap_help"] } lettre = "0.11.10" +regex = "1.11.1" reqwest = { version = "0.12.9", features = ["blocking"] } ron = "0.8.1" serde = { version = "1.0.214", features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs index 2ad2e1b..61ea68a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { Ok(()) } -fn build_err_msg_from_logs(log_infos: Vec) -> String { +pub fn build_err_msg_from_logs(log_infos: Vec) -> String { const MAX_MSG_LEN: usize = 2000; let log_count = log_infos.len(); // Stores the running count of `Errors and warnings` found @@ -79,7 +79,7 @@ fn build_err_msg_from_logs(log_infos: Vec) -> String { } /// Returns a list of the new logs with errors -fn process_logs_folder(app_state: &mut AppState) -> anyhow::Result> { +pub fn process_logs_folder(app_state: &mut AppState) -> anyhow::Result> { let mut result = Vec::new(); let mut latest_timestamp = app_state.latest_log_datetime(); // TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours diff --git a/src/log_info.rs b/src/log_info.rs index a20c4a8..23dd22c 100644 --- a/src/log_info.rs +++ b/src/log_info.rs @@ -1,6 +1,8 @@ -use std::{fmt::Display, path::Path}; +use std::{fmt::Display, path::Path, sync::OnceLock}; +use anyhow::{bail, Context}; use chrono::NaiveDateTime; +use regex::Regex; pub struct LogInfo { pub date_time: NaiveDateTime, @@ -10,8 +12,21 @@ pub struct LogInfo { impl LogInfo { pub fn new>(file_name: S) -> anyhow::Result { - let date_time = todo!(); - let abnormal_outcome = todo!(); + static CELL_RE: OnceLock = OnceLock::new(); + let re = CELL_RE.get_or_init(|| { + Regex::new(r"(\d\d\d\d-\d\d-\d\d \d\d\d\d\d\d)\.\d\d\d ?(\[.+\])?\.html") + .expect("failed to compile regex") + }); + + let Some(captures) = re.captures(file_name.as_ref()) else { + // Assumption: Only log files are present in the log folder + bail!("regex failed to match filename: {}", file_name.as_ref()) + }; + // Regex matched and can only match if first capture group is found as it is not optional + let date_time_str = captures.get(1).expect("required for match").as_str(); + let abnormal_outcome = captures.get(2).map(|x| x.as_str().to_string()); + let date_time = NaiveDateTime::parse_from_str(date_time_str, "%F %H%M%S") + .with_context(|| format!("failed to parse date from {date_time_str:?}"))?; Ok(Self { date_time, abnormal_outcome, @@ -28,8 +43,9 @@ impl Display for LogInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{} errors: {}\n{}\n", - self.date_time.format("%Y-%m-%d %H:%M:%S"), + "{} {} errors: {}\n{}\n", + self.abnormal_outcome.as_deref().unwrap_or("[ - ]"), + self.date_time.format("%F %H:%M:%S"), self.errors_and_warnings.len(), self.errors_and_warnings.join("\n") ) From 158b5bce385222aaef175f9176f56c8c694cea20 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:31:08 -0500 Subject: [PATCH 15/20] test: fix tests folder name --- .../sample_logs/ProfileName 2024-10-15 092845.903 [Error].html | 0 .../sample_logs/ProfileName 2024-10-15 093609.877.html | 0 .../sample_logs/ProfileName 2024-11-08 140913.247 [Error].html | 0 .../sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {test => tests}/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html (100%) rename {test => tests}/sample_logs/ProfileName 2024-10-15 093609.877.html (100%) rename {test => tests}/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html (100%) rename {test => tests}/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html (100%) diff --git a/test/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html b/tests/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html similarity index 100% rename from test/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html rename to tests/sample_logs/ProfileName 2024-10-15 092845.903 [Error].html diff --git a/test/sample_logs/ProfileName 2024-10-15 093609.877.html b/tests/sample_logs/ProfileName 2024-10-15 093609.877.html similarity index 100% rename from test/sample_logs/ProfileName 2024-10-15 093609.877.html rename to tests/sample_logs/ProfileName 2024-10-15 093609.877.html diff --git a/test/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html b/tests/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html similarity index 100% rename from test/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html rename to tests/sample_logs/ProfileName 2024-11-08 140913.247 [Error].html diff --git a/test/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html b/tests/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html similarity index 100% rename from test/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html rename to tests/sample_logs/ProfileName 2024-11-08 145021.053 [Stopped].html From bc7465f6ea980b9f764887e364527872e2c37e37 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:50:42 -0500 Subject: [PATCH 16/20] test: test log filtering --- src/lib.rs | 2 +- src/state.rs | 4 ++-- tests/log_parsing.rs | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 tests/log_parsing.rs diff --git a/src/lib.rs b/src/lib.rs index 61ea68a..cc9a04a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ use anyhow::{anyhow, Context}; pub use cli::Cli; pub use log_info::LogInfo; use notification::send_notification; -use state::AppState; +pub use state::AppState; pub fn run(cli: &Cli) -> anyhow::Result<()> { let (state_file, config_folder) = get_canonical_folder_and_filename(&cli.state_file)?; diff --git a/src/state.rs b/src/state.rs index e4fe854..8bdcea9 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::Context; use chrono::{DateTime, Local, NaiveDateTime, NaiveTime}; -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct AppState { last_alive_msg: DateTime, @@ -54,7 +54,7 @@ impl AppState { }) } - pub(crate) fn new(logs_dir: PathBuf) -> Self { + pub fn new(logs_dir: PathBuf) -> Self { Self { last_alive_msg: Local::now(), // TODO 4: Ensure there is a test to make sure this constant is correct diff --git a/tests/log_parsing.rs b/tests/log_parsing.rs new file mode 100644 index 0000000..afe542d --- /dev/null +++ b/tests/log_parsing.rs @@ -0,0 +1,24 @@ +use std::path::{Path, PathBuf}; + +use chrono::Local; +use fs_log_monitor::{process_logs_folder, AppState}; + +fn samples_folder() -> PathBuf { + Path::new("tests").join("sample_logs").canonicalize().unwrap() +} + +#[test] +fn no_files_pass_filter() { + let before_app_state_created = Local::now().naive_local(); + let mut actual = AppState::new(samples_folder()); + let expected = actual.clone(); + assert!( + actual.latest_log_datetime() >= before_app_state_created, + "date assumed to be now or later so that the sample logs from the past should not be included" + ); + + let logs = process_logs_folder(&mut actual).unwrap(); + assert!(logs.is_empty(), "all samples should be in the past and filtered out"); + + assert_eq!(actual, expected, "no changes expected to AppState created") +} From 56509a68d8cfe2a7a99997db42c3e34678c09d66 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 21:32:10 -0500 Subject: [PATCH 17/20] test: add test for output --- .gitignore | 3 +- Cargo.lock | 224 +++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 + src/log_info.rs | 1 + src/state.rs | 7 ++ tests/log_parsing.rs | 31 ++++-- 6 files changed, 255 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index e9c87b2..a192807 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target state.ron -/*.data \ No newline at end of file +/*.data +*.snap.new diff --git a/Cargo.lock b/Cargo.lock index 6e52168..981dbe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -153,6 +159,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -162,6 +174,15 @@ dependencies = [ "serde", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -259,6 +280,18 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -275,6 +308,35 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -302,6 +364,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -370,10 +438,11 @@ dependencies = [ "anyhow", "chrono", "clap", + "insta", "lettre", "regex", "reqwest", - "ron", + "ron 0.8.1", "serde", "serde_json", ] @@ -428,6 +497,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -793,6 +872,22 @@ dependencies = [ "hashbrown 0.15.1", ] +[[package]] +name = "insta" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "pest", + "pest_derive", + "ron 0.7.1", + "serde", + "similar", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -820,6 +915,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lettre" version = "0.11.10" @@ -851,6 +952,12 @@ version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -965,7 +1072,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -1009,6 +1116,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.15" @@ -1148,6 +1300,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + [[package]] name = "ron" version = "0.8.1" @@ -1155,7 +1318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags", + "bitflags 2.6.0", "serde", "serde_derive", ] @@ -1172,7 +1335,7 @@ version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1239,7 +1402,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -1300,12 +1463,29 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "slab" version = "0.4.9" @@ -1405,7 +1585,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] @@ -1443,6 +1623,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1533,6 +1733,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" diff --git a/Cargo.toml b/Cargo.toml index a82252a..2eca2fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,6 @@ reqwest = { version = "0.12.9", features = ["blocking"] } ron = "0.8.1" serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" + +[dev-dependencies] +insta = { version = "1.41.1", features = ["redactions", "ron", "serde"] } diff --git a/src/log_info.rs b/src/log_info.rs index 23dd22c..edc06f3 100644 --- a/src/log_info.rs +++ b/src/log_info.rs @@ -4,6 +4,7 @@ use anyhow::{bail, Context}; use chrono::NaiveDateTime; use regex::Regex; +#[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct LogInfo { pub date_time: NaiveDateTime, pub abnormal_outcome: Option, diff --git a/src/state.rs b/src/state.rs index 8bdcea9..70c6c29 100644 --- a/src/state.rs +++ b/src/state.rs @@ -68,6 +68,13 @@ impl AppState { } } + pub fn new_with_min_dates(logs_dir: PathBuf) -> Self { + let mut result = Self::new(logs_dir); + result.last_alive_msg = NaiveDateTime::MIN.and_local_timezone(Local).unwrap(); + result.latest_log_datetime = NaiveDateTime::MIN; + result + } + pub(crate) fn generate_alive_msg(&mut self) -> String { self.last_alive_msg = Local::now(); "FS Log Monitor still working".to_string() diff --git a/tests/log_parsing.rs b/tests/log_parsing.rs index afe542d..5afec78 100644 --- a/tests/log_parsing.rs +++ b/tests/log_parsing.rs @@ -1,10 +1,10 @@ use std::path::{Path, PathBuf}; use chrono::Local; -use fs_log_monitor::{process_logs_folder, AppState}; +use fs_log_monitor::{build_err_msg_from_logs, process_logs_folder, AppState}; fn samples_folder() -> PathBuf { - Path::new("tests").join("sample_logs").canonicalize().unwrap() + Path::new("tests").join("sample_logs") } #[test] @@ -13,12 +13,29 @@ fn no_files_pass_filter() { let mut actual = AppState::new(samples_folder()); let expected = actual.clone(); assert!( - actual.latest_log_datetime() >= before_app_state_created, + actual.latest_log_datetime() >= before_app_state_created, "date assumed to be now or later so that the sample logs from the past should not be included" ); - - let logs = process_logs_folder(&mut actual).unwrap(); - assert!(logs.is_empty(), "all samples should be in the past and filtered out"); - + + let log_infos = process_logs_folder(&mut actual).unwrap(); + assert!( + log_infos.is_empty(), + "all samples should be in the past and filtered out" + ); + assert_eq!(actual, expected, "no changes expected to AppState created") } + +#[test] +fn output_snapshot() { + let mut app_state = AppState::new_with_min_dates(samples_folder()); + + let logs_infos = process_logs_folder(&mut app_state).unwrap(); + insta::assert_ron_snapshot!(logs_infos); + insta::assert_ron_snapshot!(app_state, { + ".last_alive_msg" => "date_time", + }); + + let msg = build_err_msg_from_logs(logs_infos); + insta::assert_snapshot!(msg); +} From afb48092275aaba7185912d07aa7bd3c19e51616 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:11:07 -0500 Subject: [PATCH 18/20] feat: extract messages from logs --- src/lib.rs | 2 +- src/log_info.rs | 143 +++++++++++++++++- src/state.rs | 1 - .../log_parsing__output_snapshot-2.snap | 10 ++ .../log_parsing__output_snapshot-3.snap | 19 +++ .../log_parsing__output_snapshot.snap | 30 ++++ 6 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 tests/snapshots/log_parsing__output_snapshot-2.snap create mode 100644 tests/snapshots/log_parsing__output_snapshot-3.snap create mode 100644 tests/snapshots/log_parsing__output_snapshot.snap diff --git a/src/lib.rs b/src/lib.rs index cc9a04a..60daf6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,7 @@ pub fn build_err_msg_from_logs(log_infos: Vec) -> String { result.push_str(&log_info.to_string()); } result.push_str(separator); - let summary = format!("{log_count} logs with {entry_count} error and warnings"); + let summary = format!("{log_count} logs with {entry_count} error and warnings\n"); result.push_str(&summary); result.push_str(separator); if result.len() > MAX_MSG_LEN { diff --git a/src/log_info.rs b/src/log_info.rs index edc06f3..11fe161 100644 --- a/src/log_info.rs +++ b/src/log_info.rs @@ -1,6 +1,13 @@ -use std::{fmt::Display, path::Path, sync::OnceLock}; +use std::{ + fmt::Display, + fs, + io::{self, BufRead as _}, + ops::ControlFlow, + path::Path, + sync::OnceLock, +}; -use anyhow::{bail, Context}; +use anyhow::{anyhow, bail, Context}; use chrono::NaiveDateTime; use regex::Regex; @@ -35,8 +42,119 @@ impl LogInfo { }) } + /// Expects to receive the input without the surrounding tags but including inner tags to be replaced + fn add_error_or_warning(&mut self, msg: String) { + let msg = msg.replace(""", "\""); + let msg = msg.replace("
", "; "); + self.errors_and_warnings.push(msg); + } + pub fn extract_errors(&mut self, file_path: &Path) -> anyhow::Result<()> { - todo!("read log file and extract errors") + let mut extract_state = ExtractState::FindTableStart; + for line in read_lines(file_path)? { + let line = line.with_context(|| format!("failed to read line in {:?}", file_path))?; + extract_state = match extract_state { + ExtractState::FindTableStart => find_start_of_table(&line), + ExtractState::FindMsg => match self.check_for_start_of_msg(line) { + ControlFlow::Continue(value) => value, + ControlFlow::Break(value) => return value, + }, + ExtractState::ReadingMsg(count, partial_msg) => { + self.continue_reading_msg(count, partial_msg, line)? + } + } + } + bail!("unexpected end of file") + } + + fn check_for_start_of_msg( + &mut self, + line: String, + ) -> ControlFlow, ExtractState> { + let line_trimmed = line.trim_start(); + if !line_trimmed.starts_with("") { + // Not the start of the message check for end of table or go to next line + if line_trimmed.starts_with("") { + ControlFlow::Break(Ok(())) + } else { + // Go to next line + ControlFlow::Continue(ExtractState::FindMsg) + } + } else { + // Check for case of single line + static CELL_RE_SINGLE_LINE: OnceLock = OnceLock::new(); + let re = CELL_RE_SINGLE_LINE + .get_or_init(|| Regex::new(r"(.+)<\/td>").expect("failed to compile regex")); + + if let Some(captures) = re.captures(&line) { + // Single line found + self.add_error_or_warning( + captures + .get(1) + .expect("required for match") + .as_str() + .to_string(), + ); + ControlFlow::Continue(ExtractState::FindMsg) // Look for next msg + } else { + // First part of a multiline message + static CELL_RE_START_OF_MSG: OnceLock = OnceLock::new(); + let re = CELL_RE_START_OF_MSG + .get_or_init(|| Regex::new(r"(.+)").expect("failed to compile regex")); + if let Some(captures) = re.captures(&line) { + // Single line found + let first_part_of_msg = captures + .get(1) + .expect("required for match") + .as_str() + .to_string(); + ControlFlow::Continue(ExtractState::ReadingMsg(1, first_part_of_msg)) + } else { + ControlFlow::Break(Err(anyhow!("something wrong with the logic, we checked that this line starts a message but then... it doesn't now?"))) + } + } + } + } + + fn continue_reading_msg( + &mut self, + count: u8, + mut partial_msg: String, + line: String, + ) -> anyhow::Result { + static CELL_RE_LAST_LINE: OnceLock = OnceLock::new(); + let re = CELL_RE_LAST_LINE + .get_or_init(|| Regex::new(r"(.*)<\/td>").expect("failed to compile regex")); + + if let Some(captures) = re.captures(&line) { + partial_msg.push_str( + &captures + .get(1) + .map(|x| x.as_str().to_string()) + .unwrap_or_default(), + ); + self.add_error_or_warning(partial_msg); + Ok(ExtractState::FindMsg) + } else { + // Not last line add value and keep reading + if count >= ExtractState::MAX_MSG_LINES { + bail!("something is wrong found too many lines in msg. Past {} which was with line: {line:?}", ExtractState::MAX_MSG_LINES) + } + partial_msg.push_str(&line); + Ok(ExtractState::ReadingMsg(count + 1, partial_msg)) + } + } +} + +fn find_start_of_table(line: &str) -> ExtractState { + static CELL_RE_TABLE_START: OnceLock = OnceLock::new(); + let re = CELL_RE_TABLE_START.get_or_init(|| { + Regex::new(r">(path: P) -> anyhow::Result>> { + let file = fs::File::open(&path) + .with_context(|| format!("failed to open file: {:?}", path.as_ref()))?; + Ok(io::BufReader::new(file).lines()) +} + +enum ExtractState { + FindTableStart, + FindMsg, + ReadingMsg(u8, String), +} + +impl ExtractState { + /// Detect error if message is more than 5 lines long. Actually expecting exactly 2. + const MAX_MSG_LINES: u8 = 5; +} diff --git a/src/state.rs b/src/state.rs index 70c6c29..9a52e57 100644 --- a/src/state.rs +++ b/src/state.rs @@ -57,7 +57,6 @@ impl AppState { pub fn new(logs_dir: PathBuf) -> Self { Self { last_alive_msg: Local::now(), - // TODO 4: Ensure there is a test to make sure this constant is correct alive_msg_time: Some( NaiveTime::from_hms_opt(7, 0, 0) .expect("should be valid as it is set at build time"), diff --git a/tests/snapshots/log_parsing__output_snapshot-2.snap b/tests/snapshots/log_parsing__output_snapshot-2.snap new file mode 100644 index 0000000..80d196f --- /dev/null +++ b/tests/snapshots/log_parsing__output_snapshot-2.snap @@ -0,0 +1,10 @@ +--- +source: tests/log_parsing.rs +expression: app_state +--- +AppState( + last_alive_msg: "date_time", + alive_msg_time: Some("07:00:00"), + logs_dir: "tests/sample_logs", + latest_log_datetime: "2024-11-08T14:50:21", +) diff --git a/tests/snapshots/log_parsing__output_snapshot-3.snap b/tests/snapshots/log_parsing__output_snapshot-3.snap new file mode 100644 index 0000000..f864439 --- /dev/null +++ b/tests/snapshots/log_parsing__output_snapshot-3.snap @@ -0,0 +1,19 @@ +--- +source: tests/log_parsing.rs +expression: msg +--- +--- +[Error] 2024-11-08 14:09:13 errors: 4 +Cannot read file "ProfileName:/sample@gmail.com/bob/logs.7z".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform] +Cannot delete file "/home/bob/logs-ad8f.ffs_tmp".; ENOENT: No such file or directory [unlink] +Cannot read file "ProfileName:/sample@gmail.com/bob/log2.7z".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform] +Cannot delete file "/home/bob/log2-62c0.ffs_tmp".; ENOENT: No such file or directory [unlink] +--- +[Error] 2024-10-15 09:28:45 errors: 1 +Cannot open file "/home/bob/missing_file.pdf".; ENOENT: No such file or directory [stat] +--- +[Stopped] 2024-11-08 14:50:21 errors: 1 +Stopped +--- +3 logs with 6 error and warnings +--- diff --git a/tests/snapshots/log_parsing__output_snapshot.snap b/tests/snapshots/log_parsing__output_snapshot.snap new file mode 100644 index 0000000..e5ed19b --- /dev/null +++ b/tests/snapshots/log_parsing__output_snapshot.snap @@ -0,0 +1,30 @@ +--- +source: tests/log_parsing.rs +expression: logs_infos +--- +[ + LogInfo( + date_time: "2024-11-08T14:09:13", + abnormal_outcome: Some("[Error]"), + errors_and_warnings: [ + "Cannot read file \"ProfileName:/sample@gmail.com/bob/logs.7z\".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]", + "Cannot delete file \"/home/bob/logs-ad8f.ffs_tmp\".; ENOENT: No such file or directory [unlink]", + "Cannot read file \"ProfileName:/sample@gmail.com/bob/log2.7z\".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]", + "Cannot delete file \"/home/bob/log2-62c0.ffs_tmp\".; ENOENT: No such file or directory [unlink]", + ], + ), + LogInfo( + date_time: "2024-10-15T09:28:45", + abnormal_outcome: Some("[Error]"), + errors_and_warnings: [ + "Cannot open file \"/home/bob/missing_file.pdf\".; ENOENT: No such file or directory [stat]", + ], + ), + LogInfo( + date_time: "2024-11-08T14:50:21", + abnormal_outcome: Some("[Stopped]"), + errors_and_warnings: [ + "Stopped", + ], + ), +] From 2b59f767bff452cde81023f46d9f92bd01c7cacb Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:28:20 -0500 Subject: [PATCH 19/20] feat: add notification if no recent logs found --- src/lib.rs | 6 +++++- src/state.rs | 15 +++++++++++++++ .../snapshots/log_parsing__output_snapshot-2.snap | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 60daf6c..7c819ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,11 @@ pub fn run(cli: &Cli) -> anyhow::Result<()> { .context("failed to send notification of processing failure")?, } + if let Some(msg) = app_state.generate_inactivity_msg() { + send_notification(&msg, &config_folder) + .context("failed to send notification of inactivity in logs")? + } + if app_state.is_changed() { app_state .save(&cli.state_file) @@ -82,7 +87,6 @@ pub fn build_err_msg_from_logs(log_infos: Vec) -> String { pub fn process_logs_folder(app_state: &mut AppState) -> anyhow::Result> { let mut result = Vec::new(); let mut latest_timestamp = app_state.latest_log_datetime(); - // TODO 3: Send notification if no logs detected in over 24 hours or over 6 hours and uptime is less than 24 hours for dir_entry in read_dir(app_state.logs_dir()) .with_context(|| format!("failed to read log folder: {:?}", app_state.logs_dir()))? { diff --git a/src/state.rs b/src/state.rs index 9a52e57..fec02bc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -14,6 +14,7 @@ pub struct AppState { alive_msg_time: Option, logs_dir: PathBuf, latest_log_datetime: NaiveDateTime, + allowed_num_hours_without_log: Option, #[serde(skip)] is_changed: bool, } @@ -61,6 +62,7 @@ impl AppState { NaiveTime::from_hms_opt(7, 0, 0) .expect("should be valid as it is set at build time"), ), + allowed_num_hours_without_log: Some(24), latest_log_datetime: Local::now().naive_local(), logs_dir, is_changed: Default::default(), @@ -107,4 +109,17 @@ impl AppState { self.is_changed = true; self.latest_log_datetime = value; } + + pub(crate) fn generate_inactivity_msg(&self) -> Option { + let allowed_hours = self.allowed_num_hours_without_log?; + let num_hours_since_log = Local::now() + .naive_local() + .signed_duration_since(self.latest_log_datetime) + .num_hours(); + if num_hours_since_log > allowed_hours { + Some(format!("Most recent log found ({}) exceeds the allowed number of hours ({allowed_hours}) without a log. Currently {num_hours_since_log} hours without a log.", self.latest_log_datetime.format("%F %T"))) + } else { + None + } + } } diff --git a/tests/snapshots/log_parsing__output_snapshot-2.snap b/tests/snapshots/log_parsing__output_snapshot-2.snap index 80d196f..1a8e4a8 100644 --- a/tests/snapshots/log_parsing__output_snapshot-2.snap +++ b/tests/snapshots/log_parsing__output_snapshot-2.snap @@ -7,4 +7,5 @@ AppState( alive_msg_time: Some("07:00:00"), logs_dir: "tests/sample_logs", latest_log_datetime: "2024-11-08T14:50:21", + allowed_num_hours_without_log: Some(24), ) From 8b3480a95eb9c5e772e42fe93b14ffcd0c3dbcd5 Mon Sep 17 00:00:00 2001 From: One <43485962+c-git@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:46:11 -0500 Subject: [PATCH 20/20] feat: sort log by date --- src/lib.rs | 4 ++++ .../snapshots/log_parsing__output_snapshot-3.snap | 6 +++--- tests/snapshots/log_parsing__output_snapshot.snap | 14 +++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7c819ba..e5e27a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,10 @@ pub fn process_logs_folder(app_state: &mut AppState) -> anyhow::Result app_state.latest_log_datetime() { app_state.set_latest_log_datetime(latest_timestamp); } + + // Sort output to show errors in age order + result.sort_by_key(|x| x.date_time); + Ok(result) } diff --git a/tests/snapshots/log_parsing__output_snapshot-3.snap b/tests/snapshots/log_parsing__output_snapshot-3.snap index f864439..c03d286 100644 --- a/tests/snapshots/log_parsing__output_snapshot-3.snap +++ b/tests/snapshots/log_parsing__output_snapshot-3.snap @@ -3,15 +3,15 @@ source: tests/log_parsing.rs expression: msg --- --- +[Error] 2024-10-15 09:28:45 errors: 1 +Cannot open file "/home/bob/missing_file.pdf".; ENOENT: No such file or directory [stat] +--- [Error] 2024-11-08 14:09:13 errors: 4 Cannot read file "ProfileName:/sample@gmail.com/bob/logs.7z".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform] Cannot delete file "/home/bob/logs-ad8f.ffs_tmp".; ENOENT: No such file or directory [unlink] Cannot read file "ProfileName:/sample@gmail.com/bob/log2.7z".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform] Cannot delete file "/home/bob/log2-62c0.ffs_tmp".; ENOENT: No such file or directory [unlink] --- -[Error] 2024-10-15 09:28:45 errors: 1 -Cannot open file "/home/bob/missing_file.pdf".; ENOENT: No such file or directory [stat] ---- [Stopped] 2024-11-08 14:50:21 errors: 1 Stopped --- diff --git a/tests/snapshots/log_parsing__output_snapshot.snap b/tests/snapshots/log_parsing__output_snapshot.snap index e5ed19b..bcbeb79 100644 --- a/tests/snapshots/log_parsing__output_snapshot.snap +++ b/tests/snapshots/log_parsing__output_snapshot.snap @@ -4,20 +4,20 @@ expression: logs_infos --- [ LogInfo( - date_time: "2024-11-08T14:09:13", + date_time: "2024-10-15T09:28:45", abnormal_outcome: Some("[Error]"), errors_and_warnings: [ - "Cannot read file \"ProfileName:/sample@gmail.com/bob/logs.7z\".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]", - "Cannot delete file \"/home/bob/logs-ad8f.ffs_tmp\".; ENOENT: No such file or directory [unlink]", - "Cannot read file \"ProfileName:/sample@gmail.com/bob/log2.7z\".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]", - "Cannot delete file \"/home/bob/log2-62c0.ffs_tmp\".; ENOENT: No such file or directory [unlink]", + "Cannot open file \"/home/bob/missing_file.pdf\".; ENOENT: No such file or directory [stat]", ], ), LogInfo( - date_time: "2024-10-15T09:28:45", + date_time: "2024-11-08T14:09:13", abnormal_outcome: Some("[Error]"), errors_and_warnings: [ - "Cannot open file \"/home/bob/missing_file.pdf\".; ENOENT: No such file or directory [stat]", + "Cannot read file \"ProfileName:/sample@gmail.com/bob/logs.7z\".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]", + "Cannot delete file \"/home/bob/logs-ad8f.ffs_tmp\".; ENOENT: No such file or directory [unlink]", + "Cannot read file \"ProfileName:/sample@gmail.com/bob/log2.7z\".; CURLE_OPERATION_TIMEDOUT: Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds [curl_easy_perform]", + "Cannot delete file \"/home/bob/log2-62c0.ffs_tmp\".; ENOENT: No such file or directory [unlink]", ], ), LogInfo(