#![forbid(unsafe_code)]
#![deny(missing_docs)]
use clap::{
crate_authors,
crate_description,
crate_name,
crate_version,
App,
Arg,
ArgMatches,
};
use lazy_static::lazy_static;
use log::debug;
use rusoto_core::Region;
use std::str::FromStr;
#[cfg(feature = "s3")]
use url::Url;
#[cfg(feature = "cloudwatch")]
const DEFAULT_MODE: &str = "cloudwatch";
#[cfg(all(feature = "s3", not(feature = "cloudwatch")))]
const DEFAULT_MODE: &str = "s3";
#[cfg(feature = "s3")]
const DEFAULT_OBJECT_VERSIONS: &str = "current";
lazy_static! {
static ref DEFAULT_REGION: String = {
let region = Region::default();
region.name().into()
};
}
const DEFAULT_UNIT: &str = "binary";
const VALID_MODES: &[&str] = &[
#[cfg(feature = "cloudwatch")]
"cloudwatch",
#[cfg(feature = "s3")]
"s3",
];
const VALID_SIZE_UNITS: &[&str] = &[
"binary",
"bytes",
"decimal",
];
#[cfg(feature = "s3")]
const OBJECT_VERSIONS: &[&str] = &[
"all",
"current",
"multipart",
"non-current",
];
fn is_valid_aws_region(s: String) -> Result<(), String> {
match Region::from_str(&s) {
Ok(_) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
fn is_valid_aws_s3_bucket_name(s: String) -> Result<(), String> {
if s.is_empty() {
return Err("Bucket name cannot be empty".into());
}
if s.len() < 3 {
return Err("Bucket name is too short".into());
}
if s.len() > 255 {
return Err("Bucket name is too long".into());
}
Ok(())
}
#[cfg(feature = "s3")]
fn is_valid_endpoint(s: String) -> Result<(), String> {
if s.is_empty() {
return Err("Endpoint cannot be empty".into());
}
let url = match Url::parse(&s) {
Ok(u) => Ok(u),
Err(e) => Err(format!("Could not parse endpoint: {}", e)),
}?;
match url.scheme() {
"http" | "https" => Ok(()),
scheme => {
Err(format!("URL scheme must be http or https, found {}", scheme))
},
}?;
if let Some(hostname) = url.host_str() {
if hostname.contains("amazonaws.com") {
return Err("Endpoint cannot be used to specify AWS endpoints".into());
}
}
Ok(())
}
fn create_app<'a, 'b>() -> App<'a, 'b> {
debug!("Creating CLI app");
let app = App::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
.arg(
Arg::with_name("BUCKET")
.env("S3DU_BUCKET")
.hide_env_values(true)
.index(1)
.value_name("BUCKET")
.help("Bucket to retrieve size of, retrieves all if not passed")
.takes_value(true)
.validator(is_valid_aws_s3_bucket_name)
)
.arg(
Arg::with_name("MODE")
.env("S3DU_MODE")
.hide_env_values(true)
.long("mode")
.short("m")
.value_name("MODE")
.help("Use either CloudWatch or S3 to obtain bucket sizes")
.takes_value(true)
.default_value(DEFAULT_MODE)
.possible_values(VALID_MODES)
)
.arg(
Arg::with_name("REGION")
.env("AWS_REGION")
.hide_env_values(true)
.long("region")
.short("r")
.value_name("REGION")
.help("Set the AWS region to create the client in.")
.takes_value(true)
.default_value(&DEFAULT_REGION)
.validator(is_valid_aws_region)
)
.arg(
Arg::with_name("UNIT")
.env("S3DU_UNIT")
.hide_env_values(true)
.long("unit")
.short("u")
.value_name("UNIT")
.help("Sets the unit to use for size display")
.takes_value(true)
.default_value(DEFAULT_UNIT)
.possible_values(VALID_SIZE_UNITS)
);
#[cfg(feature = "s3")]
let app = app
.arg(
Arg::with_name("ENDPOINT")
.env("S3DU_ENDPOINT")
.hide_env_values(true)
.long("endpoint")
.short("e")
.value_name("URL")
.help("Sets a custom endpoint to connect to")
.takes_value(true)
.validator(is_valid_endpoint)
)
.arg(
Arg::with_name("OBJECT_VERSIONS")
.env("S3DU_OBJECT_VERSIONS")
.hide_env_values(true)
.long("object-versions")
.short("o")
.value_name("VERSIONS")
.help("Set which object versions to sum in S3 mode")
.takes_value(true)
.default_value(DEFAULT_OBJECT_VERSIONS)
.possible_values(OBJECT_VERSIONS)
);
app
}
pub fn parse_args<'a>() -> ArgMatches<'a> {
debug!("Parsing command line arguments");
create_app().get_matches()
}
#[cfg(test)]
mod tests {
use super::*;
use rusoto_core::Region;
use std::str::FromStr;
#[test]
fn test_is_valid_aws_region() {
let tests = vec![
("eu-central-1", true),
("eu-west-1", true),
("eu-west-2", true),
("int-space-station-1", false),
("nope-nope-42", false),
("us-east-1", true),
];
for test in tests {
let region = test.0;
let valid = test.1;
let region = Region::from_str(region);
assert_eq!(region.is_ok(), valid);
}
}
#[test]
fn test_is_valid_aws_s3_bucket_name() {
let long_valid = "a".repeat(65);
let long_invalid = "a".repeat(256);
let tests = vec![
("192.168.5.4", true),
("no", false),
("oh_no", true),
("th1s-1s-f1n3", true),
("valid", true),
("yes", true),
("Invalid", true),
("-invalid", true),
(&long_invalid, false),
(&long_valid, true),
];
for test in tests {
let name = test.0;
let valid = test.1;
let ret = is_valid_aws_s3_bucket_name(name.into());
assert_eq!(ret.is_ok(), valid);
}
}
#[cfg(feature = "s3")]
#[test]
fn test_is_valid_endpoint() {
let tests = vec![
("https://s3.eu-west-1.amazonaws.com", false),
("https://minio.example.org/endpoint", true),
("http://minio.example.org/endpoint", true),
("http://127.0.0.1:9000", true),
("../ohno", false),
("minio.example.org", false),
("", false),
("ftp://invalid.example.org", false),
("ftp://no@invalid.example.org", false),
("data:text/plain;invalid", false),
("unix:/var/run/invalid.socket", false),
];
for test in tests {
let url = test.0;
let valid = test.1;
let ret = is_valid_endpoint(url.into());
assert_eq!(ret.is_ok(), valid);
}
}
}