diff --git a/Cargo.lock b/Cargo.lock index 2062316..4424a80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,7 +115,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "dano" -version = "0.8.0" +version = "0.8.1" dependencies = [ "clap", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index f28a9f4..6949ad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dano" -version = "0.8.0" +version = "0.8.1" edition = "2021" keywords = ["checksum", "verify", "media", "cli-utility", "storage"] description = "A CLI tool for generating checksums of media bitstreams" diff --git a/dano.1 b/dano.1 index 398619e..95e04dc 100644 --- a/dano.1 +++ b/dano.1 @@ -1,9 +1,9 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH DANO "1" "May 2023" "dano 0.8.0" "User Commands" +.TH DANO "1" "May 2023" "dano 0.8.1" "User Commands" .SH NAME -dano \- manual page for dano 0.8.0 +dano \- manual page for dano 0.8.1 .SH DESCRIPTION -dano 0.8.0 +dano 0.8.1 dano is a wrapper for ffmpeg that checksums the internal file streams of certain media files, and stores them in a format which can be used to verify such checksums later. This is handy, because, should you choose to change metadata tags, or change file names, the media checksums should remain diff --git a/src/config.rs b/src/config.rs index 99d11f3..b3d972c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,7 +16,6 @@ // that was distributed with this source code. use std::{ - borrow::Cow, collections::HashSet, ffi::OsStr, path::{Path, PathBuf}, @@ -75,6 +74,7 @@ fn parse_args() -> ArgMatches { .help("write the new input files' hash information. If no other flags are specified, dano will ignore files which already have file hashes.") .short('w') .long("write") + .conflicts_with_all(&["PRINT", "DUMP", "DUPLICATES", "CLEAN", "TEST"]) .display_order(4)) .arg( Arg::new("TEST") @@ -83,30 +83,39 @@ fn parse_args() -> ArgMatches { .long("test") .alias("compare") .short_alias('c') + .conflicts_with_all(&["PRINT", "DUMP", "DUPLICATES", "CLEAN", "WRITE"]) .display_order(5)) .arg( Arg::new("PRINT") .help("pretty print all recorded file information (discovered within both the hash file and any xattrs).") .short('p') .long("print") + .conflicts_with_all(&["DUMP", "DUPLICATES", "CLEAN", "WRITE", "TEST"]) .display_order(6)) .arg( Arg::new("DUMP") .help("dump the recorded file information (in hash file and xattrs) to the output file (don't test/compare).") .long("dump") + .conflicts_with_all(&["DUPLICATES", "CLEAN", "WRITE", "PRINT", "TEST"]) .display_order(7)) .arg( Arg::new("DUPLICATES") .help("show any hash value duplicates discovered when reading back recorded file information (in hash file and xattrs).") .long("duplicates") .aliases(&["dupes"]) + .conflicts_with_all(&["DUMP", "CLEAN", "WRITE", "PRINT", "TEST"]) .display_order(8)) + .arg( + Arg::new("CLEAN") + .help("remove any hash files, given as input files, and remove any extended attributes, given as input files.") + .long("clean") + .display_order(9)) .arg( Arg::new("IMPORT_FLAC") .help("import flac checksums and write such information as dano recorded file information.") .long("import-flac") .conflicts_with_all(&["TEST", "PRINT", "DUMP", "DUPLICATES"]) - .display_order(9)) + .display_order(10)) .arg( Arg::new("NUM_THREADS") .help("requested number of threads to use for file processing. Default is the number of logical cores.") @@ -116,13 +125,13 @@ fn parse_args() -> ArgMatches { .min_values(1) .require_equals(true) .value_parser(clap::builder::ValueParser::os_string()) - .display_order(10)) + .display_order(11)) .arg( Arg::new("SILENT") .help("quiet many informational messages (such as \"OK\").") .short('s') .long("silent") - .display_order(11), + .display_order(12), ) .arg( Arg::new("WRITE_NEW") @@ -130,7 +139,7 @@ fn parse_args() -> ArgMatches { .long("write-new") .requires("TEST") .conflicts_with_all(&["PRINT", "DUMP", "DUPLICATES", "WRITE"]) - .display_order(12), + .display_order(13), ) .arg( Arg::new("OVERWRITE_OLD") @@ -139,19 +148,19 @@ fn parse_args() -> ArgMatches { .long("overwrite") .requires("TEST") .conflicts_with_all(&["PRINT", "DUMP", "DUPLICATES", "WRITE"]) - .display_order(13), + .display_order(14), ) .arg( Arg::new("DISABLE_FILTER") .help("disable the default filtering of file extensions which ffmpeg lists as \"common\" extensions for supported file formats.") .long("disable-filter") - .display_order(14), + .display_order(15), ) .arg( Arg::new("CANONICAL_PATHS") .help("use canonical paths (paths from the root directory) instead of potentially relative paths.") .long("canonical-paths") - .display_order(15), + .display_order(16), ) .arg( Arg::new("XATTR") @@ -160,7 +169,7 @@ fn parse_args() -> ArgMatches { When XATTR is enabled, if a write is requested, dano will always overwrite extended attributes previously written.") .short('x') .long("xattr") - .display_order(16), + .display_order(17), ) .arg( Arg::new("HASH_ALGO") @@ -171,13 +180,13 @@ fn parse_args() -> ArgMatches { .require_equals(true) .possible_values(["murmur3", "md5", "crc32", "adler32", "sha1", "sha160", "sha256", "sha384", "sha512"]) .value_parser(clap::builder::ValueParser::os_string()) - .display_order(17)) + .display_order(18)) .arg( Arg::new("DECODE") .help("decode internal bitstream before hashing. This option makes testing and writes much slower, but this option is potentially useful for lossless formats.") .long("decode") .conflicts_with_all(&["PRINT", "DUMP", "DUPLICATES"]) - .display_order(18)) + .display_order(19)) .arg( Arg::new("REWRITE_ALL") .help("rewrite all recorded hashes to the latest and greatest format version. \ @@ -185,7 +194,7 @@ fn parse_args() -> ArgMatches { .long("rewrite") .requires("WRITE") .conflicts_with_all(&["PRINT", "DUMP", "DUPLICATES", "TEST"]) - .display_order(19)) + .display_order(20)) .arg( Arg::new("ONLY") .help("hash the an input file container's first audio or video stream only, if available. \ @@ -196,13 +205,13 @@ fn parse_args() -> ArgMatches { .possible_values(["audio", "video"]) .value_parser(clap::builder::ValueParser::os_string()) .requires("WRITE") - .display_order(20)) + .display_order(21)) .arg( Arg::new("DRY_RUN") .help("print the information to stdout that would be written to disk.") .long("dry-run") .conflicts_with_all(&["PRINT", "DUPLICATES"]) - .display_order(21)) + .display_order(22)) .get_matches() } @@ -225,6 +234,7 @@ pub enum ExecMode { Print, Dump, Duplicates, + Clean, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] @@ -275,8 +285,7 @@ impl Config { .into()); }; - let opt_xattr = - matches.is_present("XATTR") || std::env::var_os(XATTR_ENV_KEY).is_some(); + let opt_xattr = matches.is_present("XATTR") || std::env::var_os(XATTR_ENV_KEY).is_some(); let opt_dry_run = matches.is_present("DRY_RUN") || (matches.is_present("PRINT") && matches.is_present("WRITE")); let opt_num_threads = matches @@ -291,7 +300,9 @@ impl Config { let opt_overwrite_old = matches.is_present("OVERWRITE_OLD"); let opt_write_new = matches.is_present("WRITE_NEW"); - let exec_mode = if matches.is_present("TEST") { + let exec_mode = if matches.is_present("CLEAN") { + ExecMode::Clean + } else if matches.is_present("TEST") { let test_mode_config = TestModeConfig { opt_overwrite_old, opt_write_new, @@ -311,7 +322,7 @@ impl Config { ExecMode::Duplicates } else { return Err(DanoError::new( - "You must specify an execution mode: TEST, WRITE, PRINT or DUMP", + "You must specify an execution mode: TEST, WRITE, DUPLICATES, CLEAN, PRINT or DUMP", ) .into()); }; @@ -359,8 +370,10 @@ impl Config { _ => read_stdin()?, } }; + Self::parse_paths( &res, + &exec_mode, opt_disable_filter, opt_canonical_paths, opt_silent, @@ -391,6 +404,7 @@ impl Config { fn parse_paths( raw_paths: &[PathBuf], + exec_mode: &ExecMode, opt_disable_filter: bool, opt_canonical_paths: bool, opt_silent: bool, @@ -424,12 +438,47 @@ impl Config { eprintln!("ERROR: Path cannot be serialized to string: {:?}", path); false }) + .map(|path| { + if opt_canonical_paths { + if let Ok(canonical) = path.canonicalize() { + return canonical; + } + + eprintln!( + "WARN: Unable convert relative path to canonical path: {:?}", + path + ); + } + + path.to_owned() + }) .filter(|path| { - if path.file_name() == Some(OsStr::new(hash_file)) { + if let &ExecMode::Clean = exec_mode { + if path.file_name() == Some(OsStr::new(DANO_DEFAULT_HASH_FILE_NAME)) { + match std::fs::remove_file(path) { + Ok(_) => { + let msg = + format!("dano hash file successfully removed: {:?}", path); + println!("{}", &msg); + } + Err(err) => { + let msg = format!( + "ERROR: Removal of dano hash file failed: {:?}: {:?}", + path, err + ); + eprintln!("{}", &msg); + } + } + return false; + } + } + + if path.file_name() == Some(hash_file.as_os_str()) { eprintln!( "ERROR: File name is the name of a dano hash file: {:?}", path ); + return false; } @@ -437,17 +486,19 @@ impl Config { }) .filter_map(|path| { if !opt_disable_filter { - let opt_extension = path.extension(); + let path_ref = &path; + + let opt_extension = path_ref.extension(); if auto_extension_filter .lines() .any(|extension| opt_extension == Some(OsStr::new(extension))) { - return Some(Either::Right(path.as_path())); + return Some(Either::Right(path)); } if let Some(ext) = opt_extension { - return Some(Either::Left(ext.to_string_lossy())); + return Some(Either::Left(ext.to_string_lossy().to_string())); } // what are these None cases: hidden files (dot files), @@ -455,12 +506,12 @@ impl Config { return None; } - Some(Either::Right(path.as_path())) + Some(Either::Right(path)) }) .partition_map(|item| item); if !opt_silent && !bad_extensions.is_empty() { - let unique: HashSet> = bad_extensions.into_iter().collect(); + let unique: HashSet = bad_extensions.into_iter().collect(); let buffer: String = unique.iter().map(|ext| format!("{} ", ext)).collect(); @@ -468,21 +519,5 @@ impl Config { } valid_paths - .iter() - .map(|path| { - if opt_canonical_paths { - if let Ok(canonical) = path.canonicalize() { - return canonical; - } - - eprintln!( - "WARN: Unable convert relative path to canonical path: {:?}", - path - ); - } - - path.to_path_buf() - }) - .collect() } } diff --git a/src/main.rs b/src/main.rs index aa66cb1..1dae312 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ mod utility; mod versions; use std::collections::BTreeMap; +use std::path::PathBuf; use itertools::Itertools; @@ -36,7 +37,9 @@ use lookup::FileInfoLookup; use output::WriteableFileInfo; use process::{ProcessedFiles, RemainderBundle}; use requests::{FileInfoRequest, RequestBundle}; -use utility::{prepare_thread_pool, print_err_buf, print_file_info, DanoError, DanoResult}; +use utility::{ + prepare_thread_pool, print_err_buf, print_file_info, remove_dano_xattr, DanoError, DanoResult, +}; const DANO_FILE_INFO_VERSION: usize = 4; const HEXADECIMAL_RADIX: u32 = 16; @@ -65,6 +68,38 @@ fn exec() -> DanoResult { let recorded_file_info = RecordedFileInfo::new(&config)?; let exit_code = match &config.exec_mode { + ExecMode::Clean => { + // dano_hashes.txt is removed during recorded_file_info ingest + let errors: Vec<&PathBuf> = config + .paths + .iter() + .filter(|path| match remove_dano_xattr(path) { + Ok(_) => { + println!( + "dano successfully removed extended attribute from: {:?}", + path + ); + false + } + Err(err) if err.to_string().contains("No data available") => false, + Err(err) => { + eprintln!("ERROR: {}", err); + true + } + }) + .collect(); + + if errors.is_empty() { + println!("All dano extended attributes successfully cleaned."); + DANO_CLEAN_EXIT_CODE + } else { + println!( + "ERROR: Could not clean extended attributes form the following paths: {:?}", + errors + ); + DANO_ERROR_EXIT_CODE + } + } ExecMode::Write(write_config) if write_config.opt_rewrite || write_config.opt_import_flac => { diff --git a/src/utility.rs b/src/utility.rs index 64b31c4..1def91e 100644 --- a/src/utility.rs +++ b/src/utility.rs @@ -53,7 +53,7 @@ pub fn prepare_thread_pool(config: &Config) -> DanoResult { Ok(thread_pool) } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct DanoError { pub details: String, } @@ -109,8 +109,11 @@ pub fn write_non_file(file_info: &FileInfo) -> DanoResult<()> { write_out_xattr(&serialized, file_info) } +pub fn remove_dano_xattr(path: &Path) -> DanoResult<()> { + xattr::remove(path, DANO_XATTR_KEY_NAME).map_err(|err| err.into()) +} + fn write_out_xattr(out_string: &str, file_info: &FileInfo) -> DanoResult<()> { - // always remove and reset when writing xattrs let _ = xattr::remove(&file_info.path, DANO_XATTR_KEY_NAME); xattr::set(&file_info.path, DANO_XATTR_KEY_NAME, out_string.as_bytes()) .map_err(|err| err.into()) @@ -155,7 +158,7 @@ pub fn print_file_info(config: &Config, file_info: &FileInfo) -> DanoResult<()> // this fn used then is just to print info about the hash. we may wish to send to dev null match config.exec_mode { ExecMode::Print | ExecMode::Duplicates | ExecMode::Test(_) => print_out_buf(&buffer), - ExecMode::Write(_) | ExecMode::Dump => print_err_buf(&buffer), + ExecMode::Write(_) | ExecMode::Dump | ExecMode::Clean => print_err_buf(&buffer), } } diff --git a/third_party/LICENSES_THIRD_PARTY.html b/third_party/LICENSES_THIRD_PARTY.html index f1cbe36..504745b 100644 --- a/third_party/LICENSES_THIRD_PARTY.html +++ b/third_party/LICENSES_THIRD_PARTY.html @@ -1129,7 +1129,7 @@

Used by:

Apache License 2.0

Used by:

@@ -1412,7 +1412,7 @@

Used by:

Mozilla Public License 2.0

Used by:

Mozilla Public License Version 2.0
 ==================================