1
0
Fork 0
mirror of synced 2024-04-27 17:22:13 +12:00

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:
Thomas Andreas Jung 2021-02-11 19:04:23 +01:00 committed by GitHub
parent 10156ccfd3
commit 03d41e173f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 147 additions and 5 deletions

70
Cargo.lock generated
View file

@ -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"

View file

@ -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"]

View file

@ -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(())
}
}