Keep original file if replacing duplicate with hardlink fails (#253)
If the user could not create a hard link (due to permissions or different block devices) then czkawka lost the duplicate file. ``` $ mkdir hardlink $ cd hardlink $ echo a > a $ cp a b $ chown thetestuser:thetestuser a $ ls -il . 25169246 -rw-rw-r-- 1 thetestuser thetestuser 2 Feb 7 11:18 a 25169641 -rw-rw-r-- 1 thomas thomas 2 Feb 7 11:18 b $ cargo run --bin czkawka_cli dup --directories $(pwd) -m 1 -f test101.result --delete-method HARD [...] -------------------------------WARNINGS-------------------------------- Failed to link /home/thomas/Development/czkawka/hardlink/b -> /home/thomas/Development/czkawka/hardlink/a ---------------------------END OF WARNINGS----------------------------- $ ls -il . 25169246 -rw-rw-r-- 1 thetestuser thetestuser 2 Feb 7 11:18 a ``` Now czkawka keeps all files and the warning provides more information why czkawka can't replace the duplicate with a hard link. ``` $ cargo run --bin czkawka_cli dup --directories $(pwd) -m 1 -f test101.result --delete-method HARD -------------------------------WARNINGS-------------------------------- Failed to link /home/thomas/Development/czkawka/hardlink/b -> /home/thomas/Development/czkawka/hardlink/a (Operation not permitted (os error 1)) ---------------------------END OF WARNINGS----------------------------- [...] 25169246 -rw-rw-r-- 1 thetestuser thetestuser 2 Feb 7 11:18 a 25169641 -rw-rw-r-- 1 thomas thomas 2 Feb 7 11:18 b ```
This commit is contained in:
parent
10156ccfd3
commit
03d41e173f
70
Cargo.lock
generated
70
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
broken_audio = ["rodio"]
|
||||
|
|
|
@ -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<String, FileEntry>, 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::<Vec<PathBuf>>();
|
||||
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::<Vec<PathBuf>>());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue