Files
Stalwart/crates/utils/src/config/cron.rs
2024-03-28 17:21:32 +01:00

149 lines
5.5 KiB
Rust

/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::time::Duration;
use chrono::{Datelike, Local, TimeDelta, TimeZone, Timelike};
use super::utils::ParseValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SimpleCron {
Day { hour: u32, minute: u32 },
Week { day: u32, hour: u32, minute: u32 },
Hour { minute: u32 },
}
impl SimpleCron {
pub fn time_to_next(&self) -> Duration {
let now = Local::now();
let next = match self {
SimpleCron::Day { hour, minute } => {
let next = Local
.with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
.earliest()
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
if next < now {
next + TimeDelta::try_days(1).unwrap_or_default()
} else {
next
}
}
SimpleCron::Week { day, hour, minute } => {
let next = Local
.with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
.earliest()
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
if next < now {
next + TimeDelta::try_days(
(7 - now.weekday().number_from_monday() + *day).into(),
)
.unwrap_or_default()
} else {
next
}
}
SimpleCron::Hour { minute } => {
let next = Local
.with_ymd_and_hms(now.year(), now.month(), now.day(), now.hour(), *minute, 0)
.earliest()
.unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
if next < now {
next + TimeDelta::try_hours(1).unwrap_or_default()
} else {
next
}
}
};
(next - now).to_std().unwrap()
}
}
impl ParseValue for SimpleCron {
fn parse_value(value: &str) -> super::Result<Self> {
let mut hour = 0;
let mut minute = 0;
for (pos, value) in value.split(' ').enumerate() {
if pos == 0 {
minute = value
.parse::<u32>()
.map_err(|_| "Invalid cron key: failed to parse cron minute".to_string())?;
if !(0..=59).contains(&minute) {
return Err(format!(
"Invalid cron key: failed to parse minute, invalid value: {minute}"
));
}
} else if pos == 1 {
if value
.as_bytes()
.first()
.ok_or_else(|| "Invalid cron key: failed to parse cron hour".to_string())?
== &b'*'
{
return Ok(SimpleCron::Hour { minute });
} else {
hour = value
.parse::<u32>()
.map_err(|_| "Invalid cron key: failed to parse cron hour".to_string())?;
if !(0..=23).contains(&hour) {
return Err(format!(
"Invalid cron key: failed to parse hour, invalid value: {hour}"
));
}
}
} else if pos == 2 {
if value
.as_bytes()
.first()
.ok_or_else(|| "Invalid cron key: failed to parse cron weekday".to_string())?
== &b'*'
{
return Ok(SimpleCron::Day { hour, minute });
} else {
let day = value.parse::<u32>().map_err(|_| {
"Invalid cron key: failed to parse cron weekday".to_string()
})?;
if !(1..=7).contains(&hour) {
return Err(format!(
"Invalid cron key: failed to parse weekday, invalid value: {}, range is 1 (Monday) to 7 (Sunday).",
hour,
));
}
return Ok(SimpleCron::Week { day, hour, minute });
}
}
}
Err("Invalid cron key: parse cron expression.".to_string())
}
}
impl Default for SimpleCron {
fn default() -> Self {
SimpleCron::Hour { minute: 0 }
}
}