use std::{
fs::{read_dir, read_to_string},
path::{Path, PathBuf},
str::FromStr,
};
use log::{debug, error, warn};
use regex::Regex;
use serde::{Deserialize, Serialize};
mod config_env;
pub use config_env::{ConfigEnv, get_env_config};
const CONFIG_FILE_TOML: &'static str = ".toml";
const CONFIG_FILE_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmbsConfig {
pub pmbs: u32,
pub subvol: String,
pub keep: Vec<PmbsConfigKeep>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmbsConfigKeep {
pub time: String,
pub n: u32,
#[serde(skip)]
pub s: u64,
}
impl PmbsConfigKeep {
pub fn new_sn(s: u64, n: u32) -> Self {
Self {
time: "".into(),
n,
s,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PmbsConfigFile {
pub path: String,
pub config: PmbsConfig,
}
pub fn list_config(config: &ConfigEnv) -> Vec<PathBuf> {
let p = PathBuf::from(&config.dir_etc);
if p.is_dir() {
read_dir(p)
.unwrap()
.filter_map(|i| {
let f = i.unwrap();
let name = f.file_name().to_string_lossy().to_string();
if name.ends_with(CONFIG_FILE_TOML) {
let p = f.path();
if p.is_file() {
Some(PathBuf::from(p))
} else {
warn!("not regular file {}", p.to_string_lossy());
None
}
} else {
None
}
})
.collect()
} else {
warn!("config dir not exist {}", &config.dir_etc);
Vec::new()
}
}
fn read_config_toml(path: &Path) -> Option<PmbsConfigFile> {
match read_to_string(path) {
Ok(s) => match toml::from_str::<PmbsConfig>(&s) {
Ok(config) => Some(PmbsConfigFile {
path: path.file_name().unwrap().to_string_lossy().to_string(),
config,
}),
Err(e) => {
error!("can not parse toml {:?}", e);
None
}
},
Err(e) => {
error!("can not read file {:?}", e);
None
}
}
}
pub fn get_re_keep_time() -> Regex {
Regex::new(r"^[1-9][0-9_]*[mhd]$").unwrap()
}
fn time_to_s(time: &str) -> u64 {
let mut time = time.to_string();
let unit = time.split_off(time.len() - 1);
let time: u64 = FromStr::from_str(&time).unwrap();
let unit: u64 = match unit.as_str() {
"m" => 60,
"h" => 3600,
"d" => 86400,
_ => unreachable!(),
};
time * unit
}
fn check_config(c: &mut PmbsConfig) -> bool {
if c.pmbs != CONFIG_FILE_VERSION {
error!("bad config file version {}", c.pmbs);
return false;
}
if c.subvol.trim().len() < 1 {
error!("empty subvol path");
return false;
}
let p = PathBuf::from(&c.subvol);
if !p.is_dir() {
warn!("subvol not exist {}", c.subvol);
}
let re_time = get_re_keep_time();
for i in &mut c.keep {
if i.n < 1 {
error!("bad n = {}", i.n);
return false;
}
if !re_time.is_match(&i.time) {
error!("bad time = {}", i.time);
return false;
}
i.s = time_to_s(&i.time);
debug!("time {} = {}s", i.time, i.s);
}
if c.keep.len() < 1 {
warn!("empty keep rule !");
}
let mut sum_n: u32 = 0;
let mut last_time: Option<String> = None;
let mut last_s = 0;
for i in &c.keep {
if i.n > 200 {
warn!("too big n = {} !", i.n);
}
if i.s > (31 * 86400) {
warn!("too big time = {} !", i.time);
}
if let Some(time) = last_time {
if i.s <= last_s {
warn!("next rule time is shorter ! {} <= {}", i.time, time);
}
}
sum_n += i.n;
last_time = Some(i.time.clone());
last_s = i.s;
}
debug!("sum_n = {}", sum_n);
if sum_n > 500 {
warn!("too many rules ! {}", sum_n);
}
true
}
pub fn read_config(path: &Path) -> Option<PmbsConfigFile> {
debug!("read config {}", path.to_string_lossy());
match read_config_toml(path) {
Some(mut c) => {
if check_config(&mut c.config) {
Some(c)
} else {
error!("bad config file {}", path.to_string_lossy());
None
}
}
None => {
error!("bad config toml file {}", path.to_string_lossy());
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_time() {
assert_eq!(time_to_s("1m"), 60);
assert_eq!(time_to_s("5m"), 300);
assert_eq!(time_to_s("20m"), 1200);
assert_eq!(time_to_s("1h"), 3600);
assert_eq!(time_to_s("2h"), 7200);
assert_eq!(time_to_s("1d"), 8_6400);
assert_eq!(time_to_s("7d"), 60_4800);
assert_eq!(time_to_s("28d"), 241_9200);
}
}
#[cfg(test)]
mod test_re {
use super::*;
#[test]
fn re_keep_time_should_match() {
let re = get_re_keep_time();
assert_eq!(re.is_match("1m"), true);
assert_eq!(re.is_match("5m"), true);
assert_eq!(re.is_match("20m"), true);
assert_eq!(re.is_match("1h"), true);
assert_eq!(re.is_match("1d"), true);
assert_eq!(re.is_match("7d"), true);
assert_eq!(re.is_match("28d"), true);
assert_eq!(re.is_match("30d"), true);
assert_eq!(re.is_match("365d"), true);
assert_eq!(re.is_match("2000d"), true);
}
#[test]
fn re_keep_time_not_match() {
let re = get_re_keep_time();
assert_eq!(re.is_match(""), false);
assert_eq!(re.is_match("1"), false);
assert_eq!(re.is_match("234"), false);
assert_eq!(re.is_match("m"), false);
assert_eq!(re.is_match("h"), false);
assert_eq!(re.is_match("d"), false);
assert_eq!(re.is_match("2w"), false);
assert_eq!(re.is_match("1y"), false);
assert_eq!(re.is_match("42min"), false);
assert_eq!(re.is_match("5hours"), false);
assert_eq!(re.is_match("5s"), false);
assert_eq!(re.is_match("balabala"), false);
assert_eq!(re.is_match("x666"), false);
}
}