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...
This commit is contained in:
parent
c4fabbc0f4
commit
556ea82a06
7 changed files with 123 additions and 13 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
/target
|
||||
/imgs
|
||||
/imgs.tar.zst
|
||||
/fif_*
|
||||
/old
|
||||
/.mypy_cache
|
||||
|
|
11
CHANGELOG.md
11
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
|
||||
|
|
|
@ -23,6 +23,20 @@ pub struct Findings {
|
|||
pub mime: Mime,
|
||||
}
|
||||
|
||||
impl Findings {
|
||||
pub fn recommended_extension(&self) -> Option<String> {
|
||||
mime_extension_lookup(self.mime.essence_str().into()).map(|extensions| extensions[0].clone())
|
||||
}
|
||||
|
||||
pub fn recommended_path(&self) -> Option<PathBuf> {
|
||||
if let Some(ext) = self.recommended_extension() {
|
||||
Some(self.file.with_extension(ext.as_str()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<Self> for Findings {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
|
||||
}
|
||||
|
@ -61,12 +75,6 @@ impl serde::Serialize for Findings {
|
|||
}
|
||||
}
|
||||
|
||||
impl Findings {
|
||||
pub fn recommended_extension(&self) -> Option<String> {
|
||||
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"))]
|
||||
|
|
|
@ -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())?;
|
||||
}
|
||||
|
|
67
src/main.rs
67
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());
|
||||
|
||||
|
|
|
@ -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<Prompt>,
|
||||
|
||||
/// 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,
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue