use failure::*;
use glob::Pattern;
use regex::Regex;
use rusoto_core::Region;
use std::str::FromStr;
use structopt::clap::AppSettings;
use structopt::StructOpt;
#[derive(StructOpt, Debug, Clone)]
#[structopt(
name = "s3find",
raw(
global_settings = "&[AppSettings::ColoredHelp, AppSettings::NeedsLongHelp, AppSettings::NeedsSubcommandHelp]"
),
after_help = r#"
The authorization flow is the following chain:
* use credentials from arguments provided by users
* use environment variable credentials: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
* use credentials via aws file profile.
Profile can be set via environment variable AWS_PROFILE
Profile file can be set via environment variable AWS_SHARED_CREDENTIALS_FILE
* use AWS instance IAM profile
* use AWS container IAM profile
"#
)]
pub struct FindOpt {
#[structopt(name = "path")]
pub path: S3path,
#[structopt(
name = "aws_access_key",
long = "aws-access-key",
raw(requires_all = r#"&["aws_secret_key"]"#)
)]
pub aws_access_key: Option<String>,
#[structopt(
name = "aws_secret_key",
long = "aws-secret-key",
raw(requires_all = r#"&["aws_access_key"]"#)
)]
pub aws_secret_key: Option<String>,
#[structopt(name = "aws_region", long = "aws-region", default_value = "us-east-1")]
pub aws_region: Region,
#[structopt(name = "npatern", long = "name", raw(number_of_values = "1"))]
pub name: Vec<NameGlob>,
#[structopt(name = "ipatern", long = "iname", raw(number_of_values = "1"))]
pub iname: Vec<InameGlob>,
#[structopt(name = "rpatern", long = "regex", raw(number_of_values = "1"))]
pub regex: Vec<Regex>,
#[structopt(
name = "time",
long = "mtime",
raw(number_of_values = "1", allow_hyphen_values = "true"),
long_help = r#"Modification time for match, a time period:
+5d - for period from now-5d to now
-5d - for period before now-5d
Possible time units are as follows:
s - seconds
m - minutes
h - hours
d - days
w - weeks
Can be multiple, but should be overlaping"#
)]
pub mtime: Vec<FindTime>,
#[structopt(
name = "bytes_size",
long = "size",
raw(number_of_values = "1", allow_hyphen_values = "true"),
long_help = r#"File size for match:
5k - exact match 5k,
+5k - bigger than 5k,
-5k - smaller than 5k,
Possible file size units are as follows:
k - kilobytes (1024 bytes)
M - megabytes (1024 kilobytes)
G - gigabytes (1024 megabytes)
T - terabytes (1024 gigabytes)
P - petabytes (1024 terabytes)"#
)]
pub size: Vec<FindSize>,
#[structopt(subcommand)]
pub cmd: Option<Cmd>,
}
#[derive(StructOpt, Debug, PartialEq, Clone)]
pub enum Cmd {
#[structopt(name = "-exec")]
Exec {
#[structopt(name = "utility")]
utility: String,
},
#[structopt(name = "-print")]
Print,
#[structopt(name = "-delete")]
Delete,
#[structopt(name = "-download")]
Download {
#[structopt(long = "force", short = "f")]
force: bool,
#[structopt(name = "destination")]
destination: String,
},
#[structopt(name = "-copy")]
Copy {
#[structopt(name = "destination")]
destination: S3path,
#[structopt(long = "flat", short = "f")]
flat: bool,
},
#[structopt(name = "-move")]
Move {
#[structopt(name = "destination")]
destination: S3path,
#[structopt(long = "flat", short = "f")]
flat: bool,
},
#[structopt(name = "-ls")]
Ls,
#[structopt(name = "-lstags")]
LsTags,
#[structopt(name = "-tags")]
Tags {
#[structopt(name = "key:value", raw(min_values = "1"))]
tags: Vec<FindTag>,
},
#[structopt(name = "-public")]
Public,
}
#[derive(Fail, Debug)]
pub enum FindError {
#[fail(display = "Invalid s3 path")]
S3Parse,
#[fail(display = "Invalid size parameter")]
SizeParse,
#[fail(display = "Invalid mtime parameter")]
TimeParse,
#[fail(display = "Cannot parse tag")]
TagParseError,
#[fail(display = "Cannot parse tag key")]
TagKeyParseError,
#[fail(display = "Cannot parse tag value")]
TagValueParseError,
}
#[derive(Debug, Clone, PartialEq)]
pub struct S3path {
pub bucket: String,
pub prefix: Option<String>,
}
impl FromStr for S3path {
type Err = Error;
fn from_str(s: &str) -> Result<S3path, Error> {
let regex = Regex::new(r#"s3://([\d\w _-]+)(/([\d\w/ _-]*))?"#)?;
let captures = regex.captures(s).ok_or(FindError::S3Parse)?;
let bucket = captures
.get(1)
.map(|x| x.as_str().to_owned())
.ok_or(FindError::S3Parse)?;
let prefix = captures.get(3).map(|x| x.as_str().to_owned());
Ok(S3path { bucket, prefix })
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FindSize {
Equal(i64),
Bigger(i64),
Lower(i64),
}
impl FromStr for FindSize {
type Err = Error;
fn from_str(s: &str) -> Result<FindSize, Error> {
let re = Regex::new(r"([+-]?)(\d*)([kMGTP]?)$")?;
let m = re.captures(s).ok_or(FindError::SizeParse)?;
let sign = m
.get(1)
.ok_or(FindError::SizeParse)?
.as_str()
.chars()
.next();
let number: i64 = m.get(2).ok_or(FindError::SizeParse)?.as_str().parse()?;
let metric = m
.get(3)
.ok_or(FindError::SizeParse)?
.as_str()
.chars()
.next();
let bytes = match metric {
None => number,
Some('k') => number * 1024,
Some('M') => number * 1024_i64.pow(2),
Some('G') => number * 1024_i64.pow(3),
Some('T') => number * 1024_i64.pow(4),
Some('P') => number * 1024_i64.pow(5),
Some(_) => return Err(FindError::SizeParse.into()),
};
match sign {
Some('+') => Ok(FindSize::Bigger(bytes)),
Some('-') => Ok(FindSize::Lower(bytes)),
None => Ok(FindSize::Equal(bytes)),
Some(_) => Err(FindError::SizeParse.into()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FindTime {
Upper(i64),
Lower(i64),
}
impl FromStr for FindTime {
type Err = Error;
fn from_str(s: &str) -> Result<FindTime, Error> {
let re = Regex::new(r"([+-]?)(\d*)([smhdw]?)$")?;
let m = re.captures(s).ok_or(FindError::TimeParse)?;
let sign = m
.get(1)
.ok_or(FindError::TimeParse)?
.as_str()
.chars()
.next();
let number: i64 = m.get(2).ok_or(FindError::TimeParse)?.as_str().parse()?;
let metric = m
.get(3)
.ok_or(FindError::TimeParse)?
.as_str()
.chars()
.next();
let seconds = match metric {
None => number,
Some('s') => number,
Some('m') => number * 60,
Some('h') => number * 3600,
Some('d') => number * 3600 * 24,
Some('w') => number * 3600 * 24 * 7,
Some(_) => return Err(FindError::TimeParse.into()),
};
match sign {
Some('+') => Ok(FindTime::Upper(seconds)),
Some('-') => Ok(FindTime::Lower(seconds)),
None => Ok(FindTime::Upper(seconds)),
Some(_) => Err(FindError::TimeParse.into()),
}
}
}
pub type NameGlob = Pattern;
#[derive(Debug, Clone, PartialEq)]
pub struct InameGlob(pub Pattern);
impl FromStr for InameGlob {
type Err = Error;
fn from_str(s: &str) -> Result<InameGlob, Error> {
let pattern = Pattern::from_str(s)?;
Ok(InameGlob(pattern))
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct FindTag {
pub key: String,
pub value: String,
}
impl FromStr for FindTag {
type Err = Error;
fn from_str(s: &str) -> Result<FindTag, Error> {
let re = Regex::new(r"(\w+):(\w+)$")?;
let m = re.captures(s).ok_or(FindError::TagParseError)?;
let key = m.get(1).ok_or(FindError::TagKeyParseError)?.as_str();
let value = m.get(2).ok_or(FindError::TagValueParseError)?.as_str();
Ok(FindTag {
key: key.to_string(),
value: value.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn s3path_correct() {
assert_eq!(
"s3://testbucket/".parse().ok(),
Some(S3path {
bucket: "testbucket".to_owned(),
prefix: Some("".to_owned()),
})
);
assert_eq!(
"s3://testbucket/path".parse().ok(),
Some(S3path {
bucket: "testbucket".to_owned(),
prefix: Some("path".to_owned())
})
);
assert_eq!(
"s3://testbucket/multi/path".parse().ok(),
Some(S3path {
bucket: "testbucket".to_owned(),
prefix: Some("multi/path".to_owned())
})
);
assert_eq!(
"s3://testbucket".parse().ok(),
Some(S3path {
bucket: "testbucket".to_owned(),
prefix: None
})
);
}
#[test]
fn s3path_incorrect() {
assert!("testbucket".parse::<S3path>().is_err());
assert!("s3://".parse::<S3path>().is_err());
assert!("s3:/testbucket".parse::<S3path>().is_err());
assert!("://testbucket".parse::<S3path>().is_err());
}
#[test]
fn size_corect() {
assert_eq!("11".parse().ok(), Some(FindSize::Equal(11)));
assert_eq!("11k".parse().ok(), Some(FindSize::Equal(11 * 1024)));
assert_eq!(
"11M".parse().ok(),
Some(FindSize::Equal(11 * 1024_i64.pow(2)))
);
assert_eq!(
"11G".parse().ok(),
Some(FindSize::Equal(11 * 1024_i64.pow(3)))
);
assert_eq!(
"11T".parse().ok(),
Some(FindSize::Equal(11 * 1024_i64.pow(4)))
);
assert_eq!(
"11P".parse().ok(),
Some(FindSize::Equal(11 * 1024_i64.pow(5)))
);
assert_eq!("+11".parse().ok(), Some(FindSize::Bigger(11)));
assert_eq!("+11k".parse().ok(), Some(FindSize::Bigger(11 * 1024)));
assert_eq!("-11".parse().ok(), Some(FindSize::Lower(11)));
assert_eq!("-11k".parse().ok(), Some(FindSize::Lower(11 * 1024)));
}
#[test]
fn size_incorect() {
assert!("-".parse::<FindSize>().is_err());
assert!("-123w".parse::<FindSize>().is_err());
}
#[test]
fn time_corect() {
assert_eq!("11".parse().ok(), Some(FindTime::Upper(11)));
assert_eq!("11s".parse().ok(), Some(FindTime::Upper(11)));
assert_eq!("11m".parse().ok(), Some(FindTime::Upper(11 * 60)));
assert_eq!("11h".parse().ok(), Some(FindTime::Upper(11 * 3600)));
assert_eq!("11d".parse().ok(), Some(FindTime::Upper(11 * 3600 * 24)));
assert_eq!(
"11w".parse().ok(),
Some(FindTime::Upper(11 * 3600 * 24 * 7))
);
assert_eq!("+11".parse().ok(), Some(FindTime::Upper(11)));
assert_eq!("+11m".parse().ok(), Some(FindTime::Upper(11 * 60)));
assert_eq!("-11m".parse().ok(), Some(FindTime::Lower(11 * 60)));
assert_eq!("-11".parse().ok(), Some(FindTime::Lower(11)));
}
#[test]
fn time_incorect() {
assert!("-".parse::<FindTime>().is_err());
assert!("-10t".parse::<FindTime>().is_err());
assert!("+".parse::<FindTime>().is_err());
assert!("+10t".parse::<FindTime>().is_err());
}
#[test]
fn tag_ok() {
assert_eq!(
"tag1:value2".parse().ok(),
Some(FindTag {
key: "tag1".to_owned(),
value: "value2".to_owned()
})
);
}
#[test]
fn tag_incorect() {
assert!("tag1value2".parse::<FindTag>().is_err());
assert!("tag1:value2:".parse::<FindTag>().is_err());
assert!(":".parse::<FindTag>().is_err());
}
}