From 9f41a2c63dcbe69a342b0f1d84c6e9c01502b0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ko=C5=82aczkowski?= Date: Sun, 4 Sep 2022 07:36:30 +0200 Subject: [PATCH] Restore extended file attributes after dedupe Fixes #152 --- Cargo.lock | 12 +++++++- Cargo.toml | 5 ++-- src/reflink.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 82 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9dc0f5f..2cda7bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,7 +324,7 @@ dependencies = [ [[package]] name = "fclones" -version = "0.27.1" +version = "0.27.2" dependencies = [ "atomic-counter", "bincode", @@ -374,6 +374,7 @@ dependencies = [ "uuid", "winapi", "winapi-util", + "xattr", ] [[package]] @@ -1405,3 +1406,12 @@ name = "windows_x86_64_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] diff --git a/Cargo.toml b/Cargo.toml index 3fbb3cd..110b045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fclones" -version = "0.27.1" +version = "0.27.2" description = "Finds duplicate, unique, under- or over-replicated files" authors = ["Piotr Kołaczkowski "] homepage = "https://github.com/pkolaczk/fclones" @@ -63,8 +63,9 @@ uuid = { version = "0.8", features = ["v4"] } fiemap = "0.1" [target.'cfg(unix)'.dependencies] -nix = "0.21" libc = "0.2" +nix = "0.21" +xattr = "0.2" [target.'cfg(windows)'.dependencies] winapi = "0.3" diff --git a/src/reflink.rs b/src/reflink.rs index b204c3c..8787612 100644 --- a/src/reflink.rs +++ b/src/reflink.rs @@ -7,11 +7,18 @@ use filetime::FileTime; use crate::dedupe::{FsCommand, PathAndMetadata}; use crate::log::Log; +#[cfg(unix)] +#[cfg(any(not(any(target_os = "linux", target_os = "android")), test))] +struct XAttr { + name: std::ffi::OsString, + value: Option>, +} + /// Calls OS-specific reflink implementations with an option to call the more generic /// one during testing one on Linux ("crosstesting"). /// The destination file is allowed to exist. pub fn reflink(src: &PathAndMetadata, dest: &PathAndMetadata, log: &Log) -> io::Result<()> { - // Remember the original metadata of the parent directory of the destination file: + // Remember original metadata of the parent directory: let dest_parent = dest.path.parent(); let dest_parent_metadata = dest_parent.map(|p| p.to_path_buf().metadata()); @@ -29,16 +36,11 @@ pub fn reflink(src: &PathAndMetadata, dest: &PathAndMetadata, log: &Log) -> io:: } }; - // Restore the original metadata of the deduplicated file: - if let Err(e) = restore_some_metadata(&dest.path.to_path_buf(), &dest.metadata) { - log.warn(format!("Failed keep metadata for {}: {}", dest, e)) - } - // Restore the original metadata of the deduplicated files's parent directory: if let Some(parent) = dest_parent { if let Some(metadata) = dest_parent_metadata { - let result = metadata - .and_then(|metadata| restore_some_metadata(&parent.to_path_buf(), &metadata)); + let result = + metadata.and_then(|metadata| restore_metadata(&parent.to_path_buf(), &metadata)); if let Err(e) = result { log.warn(format!( "Failed keep metadata for {}: {}", @@ -148,7 +150,7 @@ fn reflink_overwrite(target: &std::path::Path, link: &std::path::Path) -> io::Re } // Not kept: owner, xattrs, ACLs, etc. -fn restore_some_metadata(path: &std::path::Path, metadata: &Metadata) -> io::Result<()> { +fn restore_metadata(path: &std::path::Path, metadata: &Metadata) -> io::Result<()> { let atime = FileTime::from_last_access_time(metadata); let mtime = FileTime::from_last_modification_time(metadata); @@ -160,6 +162,40 @@ fn restore_some_metadata(path: &std::path::Path, metadata: &Metadata) -> io::Res } } +#[cfg(unix)] +#[cfg(any(not(any(target_os = "linux", target_os = "android")), test))] +fn get_xattrs(path: &std::path::Path) -> io::Result> { + use itertools::Itertools; + use xattr::FileExt; + + let file = fs::File::open(path)?; + file.list_xattr()? + .into_iter() + .map(|name| { + Ok(XAttr { + value: file.get_xattr(name.as_os_str())?, + name, + }) + }) + .try_collect() +} + +#[cfg(unix)] +#[cfg(any(not(any(target_os = "linux", target_os = "android")), test))] +fn restore_xattrs(path: &std::path::Path, xattrs: io::Result>) -> io::Result<()> { + use xattr::FileExt; + let file = fs::File::open(path)?; + for name in file.list_xattr()? { + file.remove_xattr(name)?; + } + for attr in xattrs? { + if let Some(value) = attr.value { + file.set_xattr(attr.name, &value)?; + } + } + Ok(()) +} + // Reflink which expects the destination to not exist. #[cfg(any(not(any(target_os = "linux", target_os = "android")), test))] fn copy_by_reflink(src: &crate::path::Path, dest: &crate::path::Path) -> io::Result<()> { @@ -177,9 +213,32 @@ fn copy_by_reflink(src: &crate::path::Path, dest: &crate::path::Path) -> io::Res } // Create a reflink by removing the file and making a reflink copy of the original. +// After successful copy, attempts to restore the metadata of the file. #[cfg(any(not(any(target_os = "linux", target_os = "android")), test))] fn safe_reflink(src: &PathAndMetadata, dest: &PathAndMetadata, log: &Log) -> io::Result<()> { + let dest_path_buf = dest.path.to_path_buf(); + + // Save extended attributes of the file we're going to replace: + #[cfg(unix)] + let dest_xattrs = get_xattrs(&dest_path_buf); + FsCommand::safe_remove(&dest.path, |link| copy_by_reflink(&src.path, link), log)?; + + // Restore the original metadata of the deduplicated file: + if let Err(e) = restore_metadata(&dest_path_buf, &dest.metadata) { + log.warn(format!("Failed to keep metadata for {}: {}", dest, e)) + } + // Restore the original extended attributes of the deduplicated file: + #[cfg(unix)] + if xattr::SUPPORTED_PLATFORM { + if let Err(e) = restore_xattrs(&dest_path_buf, dest_xattrs) { + log.warn(format!( + "Failed to keep extended attributes for {}: {}", + dest, e + )) + } + } + Ok(()) }