1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//! Utility to backup a folder to AWS S3, once or periodically.
#![warn(missing_docs)]
#![warn(clippy::pedantic)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::expect_used)]
use std::{env, sync::Arc};
use anyhow::{Context, Result};
use chrono::Utc;
use dotenvy::dotenv;
use log::{error, info};
use tokio::{
task,
time::{self, Instant},
};
use backup::backup;
use config::parse_config;
use crate::prelude::*;
mod aws;
mod backup;
mod config;
mod prelude;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
dotenv().ok(); // load .env file if present
// set default logging level. we ignore info logs from aws
if env::var("RUST_LOG").is_err() {
env::set_var(
"RUST_LOG",
"awsbck=info,aws_config=warn,aws_credential_types=warn,tracing=warn",
);
};
env_logger::init();
// parse config from env and/or cli
let params = Arc::new(parse_config().await?);
// check if we run the backup once, or periodically forever
match ¶ms.schedule {
Some(schedule) => {
info!(
"Will backup \"{}\" on cron schedule: \"{}\"",
params.folder.to_string_lossy(),
schedule.to_string()
);
// spawn a routine that will run the backup periodically
let task = task::spawn({
let params = Arc::clone(¶ms);
async move {
loop {
// we checked that the schedule exists above, so we can unwrap it
let Some(deadline) =
params.schedule.as_ref().or_panic().upcoming(Utc).next()
else {
error!("Could not get next execution time for cron schedule");
return;
};
info!("Next backup scheduled for {}", deadline.to_rfc2822());
// tokio's sleep_until` expect an `Instant` and not a Utc::DateTime, let's convert.
// first we get the duration between now and the deadline
let Ok(wait_time) = (deadline - Utc::now()).to_std() else {
error!("Could not convert duration to std Duration");
return;
};
// then we add it to the current instant
let Some(deadline) = Instant::now().checked_add(wait_time) else {
error!("Could not convert deadline to tokio Instant");
return;
};
// and finally we sleep until the next cron execution time
time::sleep_until(deadline).await;
// run the backup
match backup(¶ms).await {
Ok(()) => {
info!("Backup succeeded");
}
Err(e) => {
// we handle errors here to keep the loop running
error!("Backup error: {e:#}");
}
}
}
}
});
let ctrl_c = tokio::spawn(async move {
tokio::signal::ctrl_c().await.or_panic();
});
// loop forever, unless ctrl-c is called
tokio::select! {
_ = ctrl_c => {
info!("Ctrl-C received, exiting");
}
_ = task => {
info!("Backup task exited, exiting");
}
}
}
None => {
// run backup only once, immediately
info!("Backing up \"{}\" once", params.folder.to_string_lossy());
backup(¶ms).await.context("Backup error")?;
info!("Backup succeeded");
}
}
Ok(())
}