use std::{path::PathBuf, str::FromStr};
use anyhow::{Context, Result, anyhow};
use aws_config::meta::region::RegionProviderChain;
use aws_sdk_s3::config::Region;
use clap::Parser;
use cron::Schedule;
use log::info;
use crate::prelude::*;
const VALID_FILENAME_CHARS: &str = "!-_.*'()/";
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(value_hint = clap::ValueHint::DirPath, env = "AWSBCK_FOLDER")]
folder: PathBuf,
#[arg(short, long, value_name = "EXPR", env = "AWSBCK_CRON")]
cron: Option<String>,
#[arg(short, long, value_name = "NAME", env = "AWSBCK_FILENAME")]
filename: Option<String>,
#[arg(
short = 'r',
long = "region",
value_name = "REGION",
env = "AWS_REGION",
default_value_t = String::from("us-east-1")
)]
aws_region: String,
#[arg(
short = 'b',
long = "bucket",
value_name = "BUCKET",
env = "AWS_BUCKET"
)]
aws_bucket: String,
#[arg(long = "id", value_name = "KEY_ID", env = "AWS_ACCESS_KEY_ID")]
aws_key_id: String,
#[arg(
short = 'k',
long = "key",
value_name = "KEY",
env = "AWS_SECRET_ACCESS_KEY"
)]
aws_key: String,
}
pub(crate) struct Params {
pub(crate) folder: PathBuf,
pub(crate) schedule: Option<Schedule>,
pub(crate) filename: Option<String>,
pub(crate) aws_region: RegionProviderChain,
pub(crate) aws_bucket: String,
pub(crate) aws_key_id: String,
pub(crate) aws_key: String,
}
pub(crate) async fn parse_config() -> Result<Params> {
let params = Cli::parse();
let schedule = params
.cron
.map(|cron| {
Schedule::from_str(&cron)
.with_context(|| anyhow!("Could not parse cron expression '{cron}'"))
})
.transpose()?;
let folder = params
.folder
.canonicalize()
.with_context(|| anyhow!("Could not resolve path {}", params.folder.to_string_lossy()))?;
if !folder.is_dir() {
return Err(anyhow!("'{}' is not a folder", folder.to_string_lossy()));
}
let aws_region =
RegionProviderChain::first_try(Region::new(params.aws_region)).or_default_provider();
info!("Using AWS region: {}", aws_region.region().await.or_panic());
let filename = params
.filename
.map(sanitize_filename)
.filter(|s| !s.is_empty());
Ok(Params {
folder,
schedule,
filename,
aws_region,
aws_bucket: params.aws_bucket,
aws_key_id: params.aws_key_id,
aws_key: params.aws_key,
})
}
pub(crate) fn sanitize_filename(filename: impl Into<String>) -> String {
let mut filename: String = filename.into();
filename.retain(|c| c.is_ascii_alphanumeric() || VALID_FILENAME_CHARS.contains(c));
filename.chars().take(1000).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_filename() {
assert_eq!(&sanitize_filename("foo123"), "foo123");
assert_eq!(&sanitize_filename("foo bar"), "foobar");
assert_eq!(&sanitize_filename("foo/bar"), "foo/bar");
assert_eq!(&sanitize_filename("foo.tar.gz"), "foo.tar.gz");
assert_eq!(&sanitize_filename("٣৬¾①🦀"), "");
assert_eq!(&sanitize_filename("!-_.*'()/"), "!-_.*'()/");
assert_eq!(sanitize_filename("Bar1".repeat(256)), "Bar1".repeat(250));
}
}