#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unused_import_braces,
unused_qualifications
)]
#![cfg_attr(
feature = "cargo-clippy",
warn(
clippy,
clippy_correctness,
clippy_style,
clippy_complexity,
clippy_perf,
clippy_cargo,
float_cmp_const,
if_not_else,
similar_names,
shadow_unrelated,
stutter,
unicode_not_nfc,
unimplemented,
unnecessary_unwrap,
unseparated_literal_suffix,
use_self,
used_underscore_binding,
wrong_pub_self_convention,
decimal_literal_representation,
empty_enum,
enum_glob_use,
expl_impl_clone_on_copy,
fallible_impl_from,
invalid_upcast_comparisons,
mem_forget,
multiple_crate_versions,
multiple_inherent_impl,
mut_mut,
mutex_integer,
needless_borrow,
needless_continue,
option_unwrap_used,
print_stdout,
pub_enum_variant_names,
range_plus_one,
replace_consts
)
)]
#![cfg_attr(
feature = "cargo-clippy",
allow(redundant_field_names, multiple_crate_versions)
)]
#[macro_use]
extern crate log;
#[macro_use]
extern crate structopt;
extern crate chrono;
extern crate failure;
extern crate fern;
extern crate pretty_bytes;
extern crate rayon;
extern crate slack_api;
use pretty_bytes::converter::convert as prettify_bytes;
use rayon::prelude::*;
use std::collections::HashMap;
mod config;
static BATCH_SIZE: u32 = 1000;
fn calculate_rention_timestamp(days: u32) -> u32 {
let now = chrono::Local::now();
let retention_max = now - chrono::Duration::days(days as i64);
retention_max.timestamp() as u32
}
fn list_files(token: &str, retention_days: u32) -> Vec<slack_api::File> {
let mut done = false;
let mut page: u32 = 1;
let mut expected_pages: u32 = 1;
let mut files = vec![];
let client = slack_api::requests::default_client().unwrap();
debug!("++ requesting file listings for a user");
while !done {
let request = slack_api::files::ListRequest {
ts_to: Some(calculate_rention_timestamp(retention_days)),
count: Some(BATCH_SIZE),
page: Some(page),
..Default::default()
};
done = true;
let resp = slack_api::files::list(&client, &token, &request)
.expect("The slack client failed to yield a response object");
if let Some(new_files) = resp.files {
files.extend(new_files);
}
if let Some(paging) = resp.paging {
if let (Some(cur_page), Some(pages)) = (paging.page, paging.pages) {
info!(" * received page {}/{}", cur_page, expected_pages);
expected_pages = pages as u32;
if cur_page < pages {
done = false;
page = 1 + cur_page as u32;
}
}
}
}
debug!("-- finished pulling listings for a user");
files
}
fn delete_file(token: &str, file_id: &str) -> bool {
let client = slack_api::requests::default_client().unwrap();
let del_request = slack_api::files::DeleteRequest {
file: &file_id,
..Default::default()
};
match slack_api::files::delete(&client, &token, &del_request) {
Ok(_) => true,
Err(err) => {
error!("del({}) failed: {}", file_id, err);
false
},
}
}
fn main() -> Result<(), failure::Error> {
let options = config::parse_args();
let level = if options.verbose {
if options.quiet {
log::LevelFilter::Warn
} else {
log::LevelFilter::Debug
}
} else {
log::LevelFilter::Info
};
config::setup_logger(level)?;
info!("Starting up and querying Slack!");
info!(
"We're looking for files published more than {} day(s) ago.",
options.retention_days
);
info!(
"If we find any, we {}.",
if options.issue_deletions {
"will purge them"
} else {
"will tell you about them"
}
);
let file_sets: HashMap<_, _> = options
.token
.par_iter()
.map(|token| (token, list_files(token, options.retention_days)))
.collect();
let (num_files, total_bytes) = file_sets
.iter()
.map(|(_, files)| {
(
files.len(),
files
.iter()
.fold(0, |tally, file| tally + file.size.unwrap_or(0)),
)
})
.fold((0, 0), |(acc_num, acc_bytes), (num, bytes)| {
(acc_num + num, acc_bytes + bytes)
});
if num_files == 0 {
info!("No files need to be deleted for any users.");
return Ok(());
}
info!(
"Identified {} files for deletion, totalling {}.",
num_files,
prettify_bytes(total_bytes as f64)
);
if options.issue_deletions {
let delete_results: Vec<_> = file_sets
.par_iter()
.map(|(token, files)| -> Vec<&slack_api::File> {
if files.len() == 0 {
info!("No files need to be deleted for this user.");
return vec![];
}
files.clone().sort_by(|a, b| {
b.size.unwrap_or(0).cmp(&a.size.unwrap_or(0))
});
let files_deleted: Vec<_> = files
.par_iter()
.filter_map(|f| {
if let Some(ref id) = f.id {
Some((f, delete_file(token, &id)))
} else {
None
}
})
.filter_map(
|(f, success)| {
if success {
Some(f)
} else {
None
}
},
)
.collect();
files_deleted
})
.flatten()
.collect();
info!(
"Deleted {} files totalling {}",
delete_results.len(),
prettify_bytes(
delete_results
.iter()
.fold(0, |tally, file| tally + file.size.unwrap_or(0))
as f64
)
)
} else {
warn!(
"Not actually deleting anything (did you mean to pass --delete?)"
);
}
Ok(())
}