use std::path::PathBuf;
use anyhow::{anyhow, Context, Result};
use aws_config::meta::region::RegionProviderChain;
use aws_sdk_s3::Region;
use clap::{command, Parser};
use log::*;
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: Option<PathBuf>,
#[arg(short, long, value_name = "SECONDS", env = "AWSBCK_INTERVAL")]
interval: Option<u64>,
#[arg(short, long, value_name = "NAME", env = "AWSBCK_FILENAME")]
filename: Option<String>,
#[arg(
short = 'r',
long = "region",
value_name = "REGION",
env = "AWS_REGION"
)]
aws_region: Option<String>,
#[arg(
short = 'b',
long = "bucket",
value_name = "BUCKET",
env = "AWS_BUCKET"
)]
aws_bucket: Option<String>,
#[arg(long = "id", value_name = "KEY_ID", env = "AWS_ACCESS_KEY_ID")]
aws_key_id: Option<String>,
#[arg(
short = 'k',
long = "key",
value_name = "KEY",
env = "AWS_SECRET_ACCESS_KEY"
)]
aws_key: Option<String>,
}
pub(crate) struct Params {
pub(crate) folder: PathBuf,
pub(crate) interval: Option<u64>,
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 Some(folder) = params.folder else {
return Err(anyhow!("No folder path was provided"));
};
let folder = folder
.canonicalize()
.with_context(|| anyhow!("Could not resolve path {}", 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(params.aws_region.map(Region::new))
.or_default_provider()
.or_else(Region::new("us-east-1"));
info!("Using AWS region: {}", aws_region.region().await.or_panic());
let Some(aws_bucket) = params.aws_bucket else {
return Err(anyhow!("No AWS bucket name was provided"));
};
let Some(aws_key_id) = params.aws_key_id else {
return Err(anyhow!("No AWS key ID was provided"));
};
let Some(aws_key) = params.aws_key else {
return Err(anyhow!("No AWS secret key was provided"));
};
let filename = params
.filename
.map(sanitize_filename)
.filter(|s| !s.is_empty());
Ok(Params {
folder,
interval: params.interval,
filename,
aws_region,
aws_bucket,
aws_key_id,
aws_key,
})
}
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
}
#[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("!-_.*'()/"), "!-_.*'()/");
}
}