use anyhow::Result;
use async_trait::async_trait;
use tracing::{debug, warn};
use crate::config::FilterConfig;
use crate::filters::{ObjectFilter, ObjectFilterBase};
use crate::stage::Stage;
use crate::types::S3Object;
pub struct ExcludeRegexFilter<'a> {
base: ObjectFilterBase<'a>,
}
const FILTER_NAME: &str = "ExcludeRegexFilter";
impl ExcludeRegexFilter<'_> {
pub fn new(base: Stage) -> Self {
Self {
base: ObjectFilterBase {
base,
name: FILTER_NAME,
},
}
}
}
#[async_trait]
impl ObjectFilter for ExcludeRegexFilter<'_> {
async fn filter(&self) -> Result<()> {
self.base.filter(is_not_match).await
}
}
fn is_not_match(object: &S3Object, config: &FilterConfig) -> bool {
let Some(regex) = config.exclude_regex.as_ref() else {
warn!(
name = FILTER_NAME,
"exclude_regex config is None, skipping object to be safe."
);
return false;
};
let match_result = match regex.is_match(object.key()) {
Ok(matched) => matched,
Err(e) => {
warn!(
name = FILTER_NAME,
key = object.key(),
error = %e,
"regex match failed, skipping object to be safe."
);
return false;
}
};
if match_result {
let key = object.key();
let delete_marker = object.is_delete_marker();
let version_id = object.version_id();
let exclude_regex = regex.as_str();
debug!(
name = FILTER_NAME,
key = key,
delete_marker = delete_marker,
version_id = version_id,
exclude_regex = exclude_regex,
"object filtered."
);
}
!match_result
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::config::FilterConfig;
use crate::test_utils::init_dummy_tracing_subscriber;
use aws_sdk_s3::types::Object;
use fancy_regex::Regex;
pub(crate) fn test_is_not_match(object: &S3Object, config: &FilterConfig) -> bool {
is_not_match(object, config)
}
#[test]
fn is_not_match_true() {
init_dummy_tracing_subscriber();
let config = FilterConfig {
exclude_regex: Some(Regex::new(r".+\.(csv|pdf)$").unwrap()),
..Default::default()
};
let object = S3Object::NotVersioning(Object::builder().key("dir1/aaa.txt").build());
assert!(is_not_match(&object, &config));
let object = S3Object::NotVersioning(Object::builder().key("abcdefg.docx").build());
assert!(is_not_match(&object, &config));
}
#[test]
fn is_not_match_false() {
init_dummy_tracing_subscriber();
let config = FilterConfig {
exclude_regex: Some(Regex::new(r".+\.(csv|pdf)$").unwrap()),
..Default::default()
};
let object = S3Object::NotVersioning(Object::builder().key("aaa.csv").build());
assert!(!is_not_match(&object, &config));
let object = S3Object::NotVersioning(Object::builder().key("abcdefg.pdf").build());
assert!(!is_not_match(&object, &config));
}
#[test]
fn is_not_match_none_config_returns_false() {
init_dummy_tracing_subscriber();
let config = FilterConfig {
exclude_regex: None,
..Default::default()
};
let object = S3Object::NotVersioning(Object::builder().key("anything.csv").build());
assert!(!is_not_match(&object, &config));
}
#[test]
fn is_not_match_regex_error_returns_false() {
use fancy_regex::RegexBuilder;
init_dummy_tracing_subscriber();
let regex = RegexBuilder::new(r"(a+)\1b")
.backtrack_limit(1)
.build()
.unwrap();
assert!(regex.is_match("aaaaaaaaaaaaaaac").is_err());
let config = FilterConfig {
exclude_regex: Some(regex),
..Default::default()
};
let object = S3Object::NotVersioning(Object::builder().key("aaaaaaaaaaaaaaac").build());
assert!(!is_not_match(&object, &config));
}
}