#![forbid(unsafe_code)]
#![warn(
trivial_casts,
unused_lifetimes,
unused_qualifications,
missing_copy_implementations,
unused_allocation
)]
use std::io::{stdin, stdout, BufWriter, Write};
use std::process::exit;
use cfg_if::cfg_if;
use clap::Parser;
use fif::files::{scan_directory, scan_from_walkdir};
use fif::formats::{self, Format};
use fif::parameters::{self, OutputFormat, Prompt};
use fif::utils::{os_name, CLAP_LONG_VERSION};
use itertools::Itertools;
use log::{debug, error, info, trace, warn, Level};
#[cfg(test)]
mod tests;
#[doc(hidden)]
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
fn main() {
let args: parameters::Parameters = parameters::Parameters::parse();
let mut builder = env_logger::Builder::new();
builder
.filter_level(args.get_verbosity()) .parse_default_env() .parse_env("FIF_LOG") .format(|buf, r| {
let mut style = buf.default_level_style(r.level());
style = if r.level() <= Level::Warn {
style.bold()
} else {
style
};
let abbreviation = r.level().to_string().chars().next().unwrap();
writeln!(buf, "[{}{}{}] {}", style, abbreviation, style.render_reset(), r.args())
})
.init();
trace!(
"fif {}, running on {} {}",
CLAP_LONG_VERSION.as_str(),
std::env::consts::ARCH,
os_name()
);
debug!("Iterating directory: {}", args.dir.display());
let extensions = args.extensions();
let excludes = args.excluded_extensions();
if let Some(extensions) = &extensions {
debug!("Checking files with extensions: {extensions:?}");
} else if let Some(excludes) = &excludes {
debug!("Skipping files with extensions: {excludes:?}");
} else {
debug!("Checking files regardless of extensions");
}
let Some(entries) = scan_directory(&args.dir, extensions.as_ref(), excludes.as_ref(), &args.get_scan_opts()) else { exit(exitcode::NOINPUT) };
if entries.is_empty() {
warn!("No files matching requested options found.");
exit(exitcode::OK);
}
trace!("Found {} items to check", entries.len());
cfg_if! {
if #[cfg(feature = "multi-threaded")] {
let use_threads = args.jobs != 1;
if use_threads {
let jobs = if args.jobs == 0 { num_cpus::get() } else { args.jobs };
rayon::ThreadPoolBuilder::new().num_threads(jobs).build_global().unwrap();
trace!("Multithreading enabled, using {jobs} threads");
} else {
trace!("Multithreading disabled at runtime");
}
} else { let use_threads = false;
trace!("Multithreading disabled at compile time");
}
}
let (findings, errors) = scan_from_walkdir(&entries, args.canonical_paths, use_threads);
trace!("Scanning complete");
if findings.is_empty() && errors.is_empty() {
info!("All files have valid extensions!");
exit(exitcode::OK);
}
let findings = findings
.into_iter()
.filter(|f| !f.valid)
.sorted_unstable()
.collect_vec();
let errors = errors
.into_iter()
.sorted_unstable()
.inspect(|e| {
warn!("{e}");
})
.collect_vec();
if args.fix {
fn ask(message: &str) -> bool {
let mut buf = String::with_capacity(1);
print!("{message} [y/N] ");
stdout().flush().expect("Failed to flush stdout");
if let Err(e) = stdin().read_line(&mut buf) {
error!("{e}");
exit(exitcode::IOERR)
}
buf.starts_with('y') || buf.starts_with('Y')
}
let prompt = args.prompt.unwrap_or(Prompt::Error);
let mut renamed = 0_u32; let mut skipped = 0_u32; let mut failed = 0_u32;
for f in findings {
if let Some(rename_to) = f.recommended_path() {
let will_rename = {
if !args.overwrite && rename_to.exists() {
info!("Not renaming {}: Target {} exists", f.file.display(), rename_to.display());
false
} else if prompt == Prompt::Never {
true
} else if prompt == Prompt::Error || ask(&format!("Rename {} to {}?", f.file.display(), rename_to.display())) {
!rename_to.exists() || ask(&format!("Destination {} already exists, overwrite?", rename_to.display()))
} else {
false
}
};
if !will_rename {
skipped += 1;
continue;
}
loop {
match std::fs::rename(&f.file, &rename_to) {
Ok(()) => {
info!("Renamed {} -> {}", f.file.display(), rename_to.display());
renamed += 1;
break;
}
Err(e) => {
warn!("Couldn't rename {} to {}: {:#?}", f.file.display(), rename_to.display(), e);
if prompt == Prompt::Never || !ask(&format!("Error while renaming file: {e:#?}. Try again?")) {
failed += 1;
break;
}
}
}
}
} else {
info!("No known extension for file {} of type {}", f.file.display(), f.mime);
skipped += 1;
}
}
info!(
"Processed {} files: Renamed {}, skipped {}, failed to rename {}",
renamed + skipped + failed,
renamed,
skipped,
failed
);
} else {
let mut buffered_stdout = BufWriter::new(stdout());
if match args.output_format {
OutputFormat::Sh => formats::Shell.write_all(&mut buffered_stdout, &findings, &errors),
OutputFormat::PowerShell => formats::PowerShell.write_all(&mut buffered_stdout, &findings, &errors),
#[cfg(feature = "json")]
OutputFormat::Json => formats::Json.write_all(&mut buffered_stdout, &findings, &errors),
OutputFormat::Text => formats::Text.write_all(&mut buffered_stdout, &findings, &errors),
}
.is_err()
{
error!("Failed to write to stdout.");
exit(exitcode::IOERR);
}
if buffered_stdout.flush().is_err() {
error!("Failed to flush stdout.");
exit(exitcode::IOERR);
}
}
debug!("Done");
}