Compare commits
2 Commits
2c2788f172
...
a58a04c350
Author | SHA1 | Date |
---|---|---|
|
a58a04c350 | |
|
53bc0644a6 |
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
|
@ -1,9 +1,10 @@
|
|||
[package]
|
||||
name = "mizule"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = [
|
||||
"Dustin C. Hatch <dustin@hatch.name>",
|
||||
]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
clap = "2"
|
||||
|
@ -14,6 +15,15 @@ serde_json = "1.0"
|
|||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.matrix-sdk]
|
||||
version = "^0.3.0"
|
||||
default-features = false
|
||||
features = ["markdown", "native-tls"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "^1.10.0"
|
||||
features = ["rt", "macros"]
|
||||
|
|
38
src/cache.rs
38
src/cache.rs
|
@ -1,12 +1,14 @@
|
|||
use chrono::Duration;
|
||||
use chrono;
|
||||
use chrono::Duration;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CacheEntry {
|
||||
|
@ -32,6 +34,7 @@ impl CacheEntry {
|
|||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Cache {
|
||||
paths: HashMap<String, CacheEntry>,
|
||||
device_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -71,6 +74,7 @@ impl Cache {
|
|||
pub fn new() -> Self {
|
||||
Self {
|
||||
paths: HashMap::new(),
|
||||
device_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,9 +108,41 @@ impl Cache {
|
|||
}
|
||||
|
||||
pub fn save(&self, path: &str) -> Result<(), Error> {
|
||||
let path = PathBuf::from(path);
|
||||
match path.parent() {
|
||||
Some(p) => {
|
||||
if !p.is_dir() {
|
||||
fs::create_dir_all(p)?;
|
||||
}
|
||||
},
|
||||
None => {},
|
||||
};
|
||||
let mut file = fs::File::create(path)?;
|
||||
let contents = serde_json::to_string(&self)?;
|
||||
file.write_all(&contents.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn device_id(&self) -> Option<&str> {
|
||||
match &self.device_id {
|
||||
Some(p) => Some(p),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_device_id(&mut self, device_id: &str) {
|
||||
self.device_id = Some(device_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cache_dir() -> PathBuf {
|
||||
let mut dir = match env::var("XDG_CACHE_HOME") {
|
||||
Ok(v) => PathBuf::from(&v),
|
||||
Err(_) => match env::var("HOME") {
|
||||
Ok(v) => [v, ".cache".into()].iter().collect(),
|
||||
Err(_) => ".".into(),
|
||||
},
|
||||
};
|
||||
dir.push(env!("CARGO_PKG_NAME"));
|
||||
dir
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
Io(io::Error),
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl ConfigError {
|
||||
pub fn message(&self) -> String {
|
||||
match *self {
|
||||
Self::Io(ref e) => format!("{}", e),
|
||||
Self::Json(ref e) => format!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for ConfigError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::Io(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ConfigError {
|
||||
fn from(error: serde_json::Error) -> Self {
|
||||
Self::Json(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.message())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub ttl: i64,
|
||||
pub homeserver: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub room: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ttl: 30,
|
||||
homeserver: "".into(),
|
||||
username: "".into(),
|
||||
password: "".into(),
|
||||
room: "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(path: Option<&str>) -> Result<Self, ConfigError> {
|
||||
let path = match path {
|
||||
Some(p) => PathBuf::from(p),
|
||||
None => {
|
||||
let mut p = get_config_dir();
|
||||
p.push("config.json");
|
||||
p
|
||||
}
|
||||
};
|
||||
if let Ok(mut file) = fs::File::open(path) {
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let config = serde_json::from_str(&contents);
|
||||
match config {
|
||||
Ok(c) => Ok(c),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
} else {
|
||||
Ok(Self::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config_dir() -> PathBuf {
|
||||
let mut dir = match env::var("XDG_CONFIG_HOME") {
|
||||
Ok(v) => PathBuf::from(&v),
|
||||
Err(_) => match env::var("HOME") {
|
||||
Ok(v) => [v, ".config".into()].iter().collect(),
|
||||
Err(_) => ".".into(),
|
||||
},
|
||||
};
|
||||
dir.push(env!("CARGO_PKG_NAME"));
|
||||
dir
|
||||
}
|
226
src/main.rs
226
src/main.rs
|
@ -1,35 +1,27 @@
|
|||
extern crate chrono;
|
||||
extern crate clap;
|
||||
extern crate libudev;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
|
||||
use chrono::Duration;
|
||||
use clap::App;
|
||||
use clap::Arg;
|
||||
use matrix_sdk::events::room::message::{
|
||||
MessageEventContent, MessageType, TextMessageEventContent,
|
||||
};
|
||||
use matrix_sdk::events::AnyMessageEventContent;
|
||||
use matrix_sdk::identifiers::RoomIdOrAliasId;
|
||||
use matrix_sdk::reqwest::Url;
|
||||
use matrix_sdk::{Client, ClientConfig, SyncSettings};
|
||||
use std::convert::TryFrom;
|
||||
use std::env;
|
||||
use std::io::prelude::*;
|
||||
use std::num::ParseIntError;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use tokio;
|
||||
|
||||
mod cache;
|
||||
mod config;
|
||||
mod error;
|
||||
mod mountinfo;
|
||||
|
||||
use mountinfo::get_fs_uuid;
|
||||
|
||||
const CACHE_FILENAME: &'static str = "mizule.json";
|
||||
|
||||
fn validate_int(v: String) -> Result<(), String> {
|
||||
let i: Result<i64, ParseIntError> = v.parse();
|
||||
match i {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Invalid number: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
let matches = App::new("Mizule")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.author(env!("CARGO_PKG_AUTHORS"))
|
||||
|
@ -39,93 +31,115 @@ fn main() {
|
|||
.help("Path to the mounted filesystem to check")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("ttl")
|
||||
.short("t")
|
||||
.long("ttl")
|
||||
.takes_value(true)
|
||||
.validator(validate_int)
|
||||
.default_value("30")
|
||||
.help("Number of days before warning"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("mailto")
|
||||
.short("m")
|
||||
.long("mailto")
|
||||
.alias("mail-to")
|
||||
.takes_value(true)
|
||||
.help("Send warning to this email address"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let mountpoint = matches.value_of("mountpoint").unwrap();
|
||||
let ttl: i64 = matches.value_of("ttl").unwrap().parse().unwrap();
|
||||
let ttl = Duration::days(ttl);
|
||||
let mailto = matches.value_of("mailto");
|
||||
match get_fs_uuid(mountpoint) {
|
||||
Ok(uuid) => {
|
||||
check_and_notify(mountpoint, &uuid, ttl, mailto);
|
||||
}
|
||||
let config = match config::Config::load(None) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Error getting filesystem UUID: {}", e);
|
||||
eprintln!("Error loading configuration: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_and_notify(
|
||||
mountpoint: &str,
|
||||
uuid: &str,
|
||||
ttl: Duration,
|
||||
mailto: Option<&str>,
|
||||
) {
|
||||
let mut cache_path = match env::var("XDG_CACHE_HOME") {
|
||||
Ok(v) => PathBuf::from(&v),
|
||||
Err(_) => match env::var("HOME") {
|
||||
Ok(v) => [v, ".cache".into()].iter().collect(),
|
||||
Err(_) => ".".into(),
|
||||
},
|
||||
};
|
||||
cache_path.push(CACHE_FILENAME);
|
||||
let cache_path = cache_path.to_str().unwrap();
|
||||
|
||||
let cache = cache::Cache::load(cache_path);
|
||||
match cache {
|
||||
Ok(mut cache) => {
|
||||
if let Some(entry) = cache.get(&mountpoint) {
|
||||
if entry.uuid() != uuid {
|
||||
cache.update(&mountpoint, &uuid);
|
||||
if let Err(e) = cache.save(&cache_path) {
|
||||
eprintln!("Failed to save cache: {}", e);
|
||||
}
|
||||
} else if entry.expired(ttl) {
|
||||
notify(mountpoint, uuid, entry.changed(), mailto);
|
||||
}
|
||||
} else {
|
||||
cache.update(&mountpoint, &uuid);
|
||||
if let Err(e) = cache.save(&cache_path) {
|
||||
eprintln!("Failed to save cache: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut cache_path = cache::get_cache_dir();
|
||||
cache_path.push("cache.json");
|
||||
let cache_path = cache_path.to_str().unwrap();
|
||||
let mut cache = match cache::Cache::load(cache_path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading cache: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mountpoint = matches.value_of("mountpoint").unwrap();
|
||||
let fsuuid = match get_fs_uuid(mountpoint) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(e) => {
|
||||
eprintln!("Error getting filesystem UUID: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
if check_and_notify(mountpoint, &fsuuid, &config, &mut cache).await {
|
||||
if let Err(e) = cache.save(&cache_path) {
|
||||
eprintln!("Failed to save cache: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify(
|
||||
async fn check_and_notify(
|
||||
mountpoint: &str,
|
||||
uuid: &str,
|
||||
config: &config::Config,
|
||||
cache: &mut cache::Cache,
|
||||
) -> bool {
|
||||
let mut changed = false;
|
||||
let ttl = Duration::days(config.ttl);
|
||||
if let Some(entry) = cache.get(&mountpoint) {
|
||||
if entry.uuid() != uuid {
|
||||
cache.update(&mountpoint, &uuid);
|
||||
changed = true;
|
||||
} else if entry.expired(ttl) {
|
||||
changed =
|
||||
notify(mountpoint, uuid, entry.changed(), config, cache).await;
|
||||
}
|
||||
} else {
|
||||
cache.update(&mountpoint, &uuid);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
async fn notify(
|
||||
mountpoint: &str,
|
||||
uuid: &str,
|
||||
changed: chrono::DateTime<chrono::Utc>,
|
||||
mailto: Option<&str>,
|
||||
) {
|
||||
config: &config::Config,
|
||||
cache: &mut cache::Cache,
|
||||
) -> bool {
|
||||
let mut cache_changed = false;
|
||||
|
||||
let room_id = RoomIdOrAliasId::try_from(config.room.clone()).unwrap();
|
||||
let server_name = room_id.server_name().to_owned();
|
||||
let server_names = [server_name];
|
||||
|
||||
let clientconfig = ClientConfig::new().store_path(cache::get_cache_dir());
|
||||
let client = Client::new_with_config(
|
||||
Url::parse(&config.homeserver).expect("Invalid homeserver URL"),
|
||||
clientconfig,
|
||||
)
|
||||
.unwrap();
|
||||
let res = client
|
||||
.login(
|
||||
&config.username,
|
||||
&config.password,
|
||||
cache.device_id(),
|
||||
Some(env!("CARGO_PKG_NAME")),
|
||||
)
|
||||
.await
|
||||
.expect("Login failed");
|
||||
if cache.device_id().is_none() {
|
||||
cache.set_device_id(res.device_id.as_str());
|
||||
cache_changed = true;
|
||||
}
|
||||
let res = client
|
||||
.join_room_by_id_or_alias(&room_id, &server_names)
|
||||
.await
|
||||
.expect("Failed to join room");
|
||||
client
|
||||
.sync_once(SyncSettings::default())
|
||||
.await
|
||||
.expect("Failed to sync with Matrix server");
|
||||
let room = client.get_joined_room(&res.room_id).unwrap();
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let delta = now - changed;
|
||||
let message = format!(
|
||||
concat!(
|
||||
"The filesystem mounted at {} (UUID {}) ",
|
||||
"The filesystem mounted at `{}` (UUID {}) ",
|
||||
"was last changed on {} ({} days ago)",
|
||||
),
|
||||
mountpoint,
|
||||
|
@ -134,37 +148,13 @@ fn notify(
|
|||
delta.num_days()
|
||||
);
|
||||
println!("{}", message);
|
||||
if let Some(mailto) = mailto {
|
||||
println!("Sending notification to {}", mailto);
|
||||
let subject = format!(
|
||||
"{}: {} needs to be changed!",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
mountpoint
|
||||
);
|
||||
if let Err(e) = sendmail(mailto, &subject, &message) {
|
||||
eprintln!("Failed to send email: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sendmail(
|
||||
mailto: &str,
|
||||
subject: &str,
|
||||
message: &str,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut cmd = process::Command::new("sendmail")
|
||||
.arg("-t")
|
||||
.stdin(process::Stdio::piped())
|
||||
.spawn()?;
|
||||
{
|
||||
let mut stdin = cmd.stdin.take().unwrap();
|
||||
stdin.write_all(format!("To: {}\n", mailto).as_bytes())?;
|
||||
stdin.write_all(format!("Subject: {}\n", subject).as_bytes())?;
|
||||
stdin.write_all("\n".as_bytes())?;
|
||||
stdin.write_all(message.as_bytes())?;
|
||||
stdin.write_all("\n".as_bytes())?;
|
||||
stdin.flush()?;
|
||||
}
|
||||
cmd.wait()?;
|
||||
Ok(())
|
||||
let content =
|
||||
AnyMessageEventContent::RoomMessage(MessageEventContent::new(
|
||||
MessageType::Text(TextMessageEventContent::markdown(message)),
|
||||
));
|
||||
room.send(content, None)
|
||||
.await
|
||||
.expect("Failed to send Matrix notification");
|
||||
return cache_changed;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::io;
|
|||
use std::io::prelude::*;
|
||||
use std::path;
|
||||
|
||||
use error::Error;
|
||||
use crate::error::Error;
|
||||
|
||||
pub struct MountInfo {
|
||||
pub mount_id: u32,
|
||||
|
|
Loading…
Reference in New Issue