From 556ea82a06b023225ba5e85a141335d607aef7e4 Mon Sep 17 00:00:00 2001 From: Lynnesbian Date: Mon, 4 Oct 2021 23:33:48 +1000 Subject: [PATCH] added --fix - fif can now rename files itself! this alone warrants a bump to 0.4.0 imo, and now that i think about it, there's not really much keeping me from calling it 1.0... i think i'd want to get more tests, and maybe upgrade to clap 3 stable when that happens, before calling it 1.0, though. maybe even get some sort of configuration file... --- .gitignore | 1 + CHANGELOG.md | 11 ++++++++ src/findings.rs | 20 +++++++++----- src/formats.rs | 4 +-- src/main.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++-- src/parameters.rs | 19 +++++++++++--- src/tests/mod.rs | 14 ++++++++++ 7 files changed, 123 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index eac6fd2..1caff9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /target /imgs +/imgs.tar.zst /fif_* /old /.mypy_cache diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd8e50..8b4e7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- `--fix` mode - instead of outputting a shell script or text file, fif will rename the misnamed files for you! + - By default, the user will be prompted for each rename. This behaviour can be changed with the new `p`/`--prompt` + flag: `-p always` to be prompted each time (the default), `-p error` to be prompted on errors and when a file + would be overwritten by renaming, and `-p never` to disable prompting altogether - this behaves the same as + answering "yes" to every prompt. + - The `--overwrite` flag must be specified along with `--fix` in order for fif to process renames that would cause an + existing file to be overwritten. Without it, fif will never overwrite existing files, even with `-p always`. + **Caution**: If this flag is set in combination with `--prompt never`, fif will overwrite files **without asking**! + - For a more thorough breakdown of how these flags work, see [the corresponding wiki + page](https://gitlab.com/Lynnesbian/fif/-/wikis/Fix). ### Changed - Capped help output (`-h`/`--help`) width at 120 characters max - Output is now sorted by filename - specifically, errors will appear first, followed by files that fif is unable to diff --git a/src/findings.rs b/src/findings.rs index ca06799..db4dbfe 100644 --- a/src/findings.rs +++ b/src/findings.rs @@ -23,6 +23,20 @@ pub struct Findings { pub mime: Mime, } +impl Findings { + pub fn recommended_extension(&self) -> Option { + mime_extension_lookup(self.mime.essence_str().into()).map(|extensions| extensions[0].clone()) + } + + pub fn recommended_path(&self) -> Option { + if let Some(ext) = self.recommended_extension() { + Some(self.file.with_extension(ext.as_str())) + } else { + None + } + } +} + impl PartialOrd for Findings { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } @@ -61,12 +75,6 @@ impl serde::Serialize for Findings { } } -impl Findings { - pub fn recommended_extension(&self) -> Option { - mime_extension_lookup(self.mime.essence_str().into()).map(|extensions| extensions[0].clone()) - } -} - #[derive(Debug, PartialEq, PartialOrd, Ord, Eq)] #[cfg_attr(feature = "json", derive(serde::Serialize))] #[cfg_attr(feature = "json", serde(tag = "type", content = "path"))] diff --git a/src/formats.rs b/src/formats.rs index a992ed0..1b71eeb 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -142,8 +142,8 @@ pub trait FormatSteps { } for finding in findings { - if let Some(ext) = finding.recommended_extension() { - self.rename(f, finding.file.as_path(), &finding.file.with_extension(ext.as_str()))?; + if let Some(name) = finding.recommended_path() { + self.rename(f, finding.file.as_path(), &name)?; } else { self.no_known_extension(f, finding.file.as_path())?; } diff --git a/src/main.rs b/src/main.rs index 8da26da..d243160 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,14 +17,14 @@ #![forbid(unsafe_code)] #![warn(trivial_casts, unused_lifetimes, unused_qualifications)] -use std::io::{stdout, BufWriter, Write}; +use std::io::{stdin, stdout, BufWriter, Write}; use std::process::exit; use cfg_if::cfg_if; use clap::Clap; use fif::files::{scan_directory, scan_from_walkdir}; use fif::formats::Format; -use fif::parameters::OutputFormat; +use fif::parameters::{OutputFormat, Prompt}; use fif::utils::{os_name, CLAP_LONG_VERSION}; use fif::{formats, parameters}; use itertools::Itertools; @@ -136,6 +136,69 @@ fn main() { .collect_vec(); if args.fix { + fn ask(message: String) -> bool { + let mut buf = String::with_capacity(1); + print!("{} [y/N] ", message); + + // flush stdout to ensure message is displayed + stdout().flush().expect("Failed to flush stdout"); + + if let Err(e) = stdin().read_line(&mut buf) { + // something went wrong while reading input - just exit + error!("{}", e); + exit(exitcode::IOERR) + } + buf.starts_with("y") || buf.starts_with("Y") + } + + let prompt = args.prompt.unwrap_or(Prompt::Always); + + for f in findings { + if let Some(rename_to) = f.recommended_path() { + let will_rename = { + if !args.overwrite && rename_to.exists() { + // handles: --prompt never, --prompt error, --prompt always + // user didn't specify --overwrite, and the destination exists + info!("Not renaming {:#?}: Target {:#?} exists", f.file, rename_to); + false + } else if prompt == Prompt::Never { + // handles: --prompt never --overwrite + // user specified --prompt never in conjunction with --overwrite, so always rename + true + } else if prompt == Prompt::Error || ask(format!("Rename {:#?} to {:#?}?", &f.file, &rename_to)) { + // handles: --prompt error --overwrite, --prompt always --overwrite [y] + // if the target exists, prompt before renaming; otherwise, just rename + !rename_to.exists() || ask(format!("Destination {:#?} already exists, overwrite?", rename_to)) + } else { + // handles: --prompt always --overwrite [n] + // user was prompted and replied "no" + false + } + }; + + if !will_rename { continue } + + loop { + match std::fs::rename(&f.file, &rename_to) { + Ok(_) => { + info!("Renamed {:#?} -> {:#?}", f.file, rename_to); + break; + } + Err(e) => { + warn!("Couldn't rename {:#?} to {:#?}: {:#?}", f.file, rename_to, e); + // if the user passed --prompt never, continue to the next file + // otherwise, prompt user to retry move, retrying until the rename succeeds or they respond "N" + if prompt == Prompt::Never || !ask(format!("Error while renaming file: {:#?}. Try again?", e)) { + break; + } + } + } + } + } else { + // no recommended name :c + info!("No known extension for file {:#?} of type {}", f.file, f.mime) + } + } } else { let mut buffered_stdout = BufWriter::new(stdout()); diff --git a/src/parameters.rs b/src/parameters.rs index 9413c74..5e50b8a 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -32,6 +32,13 @@ pub enum OutputFormat { Json, } +#[derive(Clap, PartialEq, Debug)] +pub enum Prompt { + Never, + Error, + Always +} + #[derive(Clap, Debug)] #[allow(clippy::struct_excessive_bools)] #[clap( @@ -48,12 +55,18 @@ pub enum OutputFormat { max_term_width = 120 )] pub struct Parameters { - /// Automatically rename files to use the correct extension. + /// Automatically rename files to use the correct extension, prompting the user for every rename. #[clap(long)] pub fix: bool, - #[clap(long)] - pub noconfirm: bool, + /// Requires --fix. Should fif prompt you `Never`, only on `Error`s and overwrites, or `Always`? + #[clap(short = 'p', long, arg_enum, requires = "fix")] + pub prompt: Option, + + /// Requires --fix. Allow overwriting files. Warning: When used in combination with `--prompt never`, fif will + /// overwrite files without prompting! + #[clap(short = 'n', long, requires = "fix")] + pub overwrite: bool, // NOTE: clap's comma-separated argument parser makes it impossible to specify extensions with commas in their name - // `-e sil\,ly` is treated as ["sil", "ly"] rather than as ["silly"], no matter how i escape the comma (in bash, diff --git a/src/tests/mod.rs b/src/tests/mod.rs index e8982d5..fe5407f 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -136,6 +136,11 @@ fn simple_directory() { assert_eq!(result.mime, IMAGE_PNG); // 3. ensure the recommended extension for "wrong.jpg" is "png" assert_eq!(&result.recommended_extension().unwrap(), &String::from("png")); + // 4. ensure the recommended filename for "wrong.jpg" is "wrong.png" + assert_eq!( + result.recommended_path().unwrap().file_name(), + Some(OsStr::new("wrong.png")) + ); continue; } @@ -146,6 +151,15 @@ fn simple_directory() { .unwrap() .contains(&result.recommended_extension().unwrap())); + // ensure that the recommended_name function outputs something beginning with "test" + assert!(result + .recommended_path() + .unwrap() + .file_name() + .unwrap() + .to_string_lossy() + .starts_with("test")); + // make sure the guessed mimetype is correct based on the extension of the scanned file // because we already know that the extensions match the mimetype (as we created these files ourselves earlier in // the test), all files with the "jpg" extension should be IMAGE_JPEGs, etc.