diff --git a/Cargo.lock b/Cargo.lock index 7ea9d35..d0d1220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,6 +580,7 @@ dependencies = [ "img_hash", "rayon", "rodio", + "tempfile", "xxhash-rust", "zip", ] @@ -1824,6 +1825,12 @@ dependencies = [ "miniz_oxide 0.3.7", ] +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -1909,6 +1916,46 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.5.0" @@ -1974,6 +2021,15 @@ version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.19" @@ -2356,6 +2412,20 @@ dependencies = [ "xattr", ] +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand", + "redox_syscall 0.2.4", + "remove_dir_all", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/czkawka_core/Cargo.toml b/czkawka_core/Cargo.toml index 440d89a..abba9a1 100644 --- a/czkawka_core/Cargo.toml +++ b/czkawka_core/Cargo.toml @@ -39,7 +39,9 @@ blake3 = "0.3" crc32fast = "1.2.1" xxhash-rust = { version = "0.8.1", features = ["xxh3"] } +tempfile = "3.1" + [features] default = [] -broken_audio = ["rodio"] \ No newline at end of file +broken_audio = ["rodio"] diff --git a/czkawka_core/src/duplicate.rs b/czkawka_core/src/duplicate.rs index b53951d..ffb3f6b 100644 --- a/czkawka_core/src/duplicate.rs +++ b/czkawka_core/src/duplicate.rs @@ -3,6 +3,7 @@ use humansize::{file_size_opts as options, FileSize}; use std::collections::{BTreeMap, HashMap}; use std::fs::{File, Metadata, OpenOptions}; use std::io::prelude::*; +use std::io::{Error, ErrorKind, Result}; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{fs, thread}; @@ -1280,12 +1281,12 @@ fn delete_files(vector: &[FileEntry], delete_method: &DeleteMethod, warnings: &m let src = vector[q_index].path.clone(); for (index, file) in vector.iter().enumerate() { if q_index != index { - if fs::remove_file(file.path.clone()).and_then(|_| fs::hard_link(&src, &file.path)).is_ok() { + if let Err(e) = make_hard_link(&src, &file.path) { + failed_to_remove_files += 1; + warnings.push(format!("Failed to link {} -> {} ({})", file.path.display(), src.display(), e)); + } else { removed_files += 1; gained_space += file.size; - } else { - failed_to_remove_files += 1; - warnings.push(format!("Failed to link {} -> {}", file.path.display(), src.display())); } } } @@ -1297,6 +1298,17 @@ fn delete_files(vector: &[FileEntry], delete_method: &DeleteMethod, warnings: &m (gained_space, removed_files, failed_to_remove_files) } +fn make_hard_link(src: &PathBuf, dst: &PathBuf) -> Result<()> { + let dst_dir = dst.parent().ok_or_else(|| Error::new(ErrorKind::Other, "No parent"))?; + let temp = tempfile::Builder::new().tempfile_in(dst_dir)?; + fs::rename(dst, temp.path())?; + let result = fs::hard_link(src, dst); + if result.is_err() { + fs::rename(temp.path(), dst)?; + } + result +} + fn save_hashes_to_file(hashmap: &HashMap, text_messages: &mut Messages, type_of_hash: &HashType) { if let Some(proj_dirs) = ProjectDirs::from("pl", "Qarmin", "Czkawka") { let cache_dir = PathBuf::from(proj_dirs.cache_dir()); @@ -1600,3 +1612,61 @@ fn load_hashes_from_file(text_messages: &mut Messages, type_of_hash: &HashType) text_messages.messages.push("Cannot find or open system config dir to save cache file".to_string()); None } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{read_dir, File}; + use std::io::Result; + #[cfg(target_family = "windows")] + use std::os::fs::MetadataExt; + #[cfg(target_family = "unix")] + use std::os::unix::fs::MetadataExt; + + #[cfg(target_family = "unix")] + fn assert_inode(before: &Metadata, after: &Metadata) { + assert_eq!(before.ino(), after.ino()); + } + #[cfg(target_family = "windows")] + fn assert_inode(_: &Metadata, _: &Metadata) {} + + #[test] + fn test_make_hard_link() -> Result<()> { + let dir = tempfile::Builder::new().tempdir()?; + let (src, dst) = (dir.path().join("a"), dir.path().join("b")); + File::create(&src)?; + let metadata = fs::metadata(&src)?; + File::create(&dst)?; + + make_hard_link(&src, &dst)?; + + assert_inode(&metadata, &fs::metadata(&dst)?); + assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions()); + assert_eq!(metadata.modified()?, fs::metadata(&dst)?.modified()?); + assert_inode(&metadata, &fs::metadata(&src)?); + assert_eq!(metadata.permissions(), fs::metadata(&src)?.permissions()); + assert_eq!(metadata.modified()?, fs::metadata(&src)?.modified()?); + + let mut actual = read_dir(&dir)?.map(|e| e.unwrap().path()).collect::>(); + actual.sort(); + assert_eq!(vec![src, dst], actual); + Ok(()) + } + + #[test] + fn test_make_hard_link_fails() -> Result<()> { + let dir = tempfile::Builder::new().tempdir()?; + let (src, dst) = (dir.path().join("a"), dir.path().join("b")); + File::create(&dst)?; + let metadata = fs::metadata(&dst)?; + + assert!(make_hard_link(&src, &dst).is_err()); + + assert_inode(&metadata, &fs::metadata(&dst)?); + assert_eq!(metadata.permissions(), fs::metadata(&dst)?.permissions()); + assert_eq!(metadata.modified()?, fs::metadata(&dst)?.modified()?); + + assert_eq!(vec![dst], read_dir(&dir)?.map(|e| e.unwrap().path()).collect::>()); + Ok(()) + } +}