Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
346 changes: 346 additions & 0 deletions src/uu/mv/src/hardlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore hardlinked

//! Hardlink preservation utilities for mv operations
//!
//! This module provides functionality to preserve hardlink relationships
//! when moving files across different filesystems/partitions.

use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};

/// Tracks hardlinks during cross-partition moves to preserve them
#[derive(Debug, Default)]
pub struct HardlinkTracker {
/// Maps (device, inode) -> destination path for the first occurrence
inode_map: HashMap<(u64, u64), PathBuf>,
}

/// Pre-scans files to identify hardlink groups with optimized memory usage
#[derive(Debug, Default)]
pub struct HardlinkGroupScanner {
/// Maps (device, inode) -> list of source paths that are hardlinked together
hardlink_groups: HashMap<(u64, u64), Vec<PathBuf>>,
/// List of source files/directories being moved (for destination mapping)
source_files: Vec<PathBuf>,
/// Whether scanning has been performed
scanned: bool,
}

/// Configuration options for hardlink preservation
#[derive(Debug, Clone, Default)]
pub struct HardlinkOptions {
/// Whether to show verbose output about hardlink operations
pub verbose: bool,
}

/// Result type for hardlink operations
pub type HardlinkResult<T> = Result<T, HardlinkError>;

/// Errors that can occur during hardlink operations
#[derive(Debug)]
pub enum HardlinkError {
Io(io::Error),
Scan(String),
Preservation { source: PathBuf, target: PathBuf },
Metadata { path: PathBuf, error: io::Error },
}

impl std::fmt::Display for HardlinkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HardlinkError::Io(e) => write!(f, "I/O error during hardlink operation: {}", e),
HardlinkError::Scan(msg) => {
write!(f, "Failed to scan files for hardlinks: {}", msg)

Check warning on line 58 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L54-L58

Added lines #L54 - L58 were not covered by tests
}
HardlinkError::Preservation { source, target } => {
write!(
f,
"Failed to preserve hardlink: {} -> {}",
source.display(),
target.display()

Check warning on line 65 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L60-L65

Added lines #L60 - L65 were not covered by tests
)
}
HardlinkError::Metadata { path, error } => {
write!(f, "Metadata access error for {}: {}", path.display(), error)

Check warning on line 69 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L68-L69

Added lines #L68 - L69 were not covered by tests
}
}
}

Check warning on line 72 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L72

Added line #L72 was not covered by tests
}

impl std::error::Error for HardlinkError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
HardlinkError::Io(e) => Some(e),
HardlinkError::Metadata { error, .. } => Some(error),
_ => None,

Check warning on line 80 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L76-L80

Added lines #L76 - L80 were not covered by tests
}
}

Check warning on line 82 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L82

Added line #L82 was not covered by tests
}

impl From<io::Error> for HardlinkError {
fn from(error: io::Error) -> Self {
HardlinkError::Io(error)
}

Check warning on line 88 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L86-L88

Added lines #L86 - L88 were not covered by tests
}

impl From<HardlinkError> for io::Error {
fn from(error: HardlinkError) -> Self {
match error {
HardlinkError::Io(e) => e,
HardlinkError::Scan(msg) => io::Error::other(msg),
HardlinkError::Preservation { source, target } => io::Error::other(format!(
"Failed to preserve hardlink: {} -> {}",
source.display(),
target.display()

Check warning on line 99 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L92-L99

Added lines #L92 - L99 were not covered by tests
)),

HardlinkError::Metadata { path, error } => io::Error::other(format!(
"Metadata access error for {}: {}",
path.display(),

Check warning on line 104 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L102-L104

Added lines #L102 - L104 were not covered by tests
error
)),
}
}

Check warning on line 108 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L108

Added line #L108 was not covered by tests
}

impl HardlinkTracker {
pub fn new() -> Self {
Self::default()
}

/// Check if a file is a hardlink we've seen before, and return the target path if so
pub fn check_hardlink(
&mut self,
source: &Path,
dest: &Path,
scanner: &HardlinkGroupScanner,
options: &HardlinkOptions,
) -> HardlinkResult<Option<PathBuf>> {
use std::os::unix::fs::MetadataExt;

let metadata = match source.metadata() {
Ok(meta) => meta,
Err(e) => {

Check warning on line 128 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L128

Added line #L128 was not covered by tests
// Gracefully handle metadata errors by logging and continuing without hardlink tracking
if options.verbose {
eprintln!(
"warning: cannot get metadata for {}: {}",
source.display(),
e
);
}
return Ok(None);

Check warning on line 137 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L130-L137

Added lines #L130 - L137 were not covered by tests
}
};

let key = (metadata.dev(), metadata.ino());

// Check if we've already processed a file with this inode
if let Some(existing_path) = self.inode_map.get(&key) {
// Check if this file is part of a hardlink group from the scanner
let has_hardlinks = scanner
.hardlink_groups
.get(&key)
.map(|group| group.len() > 1)
.unwrap_or(false);

if has_hardlinks {
if options.verbose {
eprintln!(
"preserving hardlink {} -> {} (hardlinked)",
source.display(),
existing_path.display()
);

Check warning on line 158 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L154-L158

Added lines #L154 - L158 were not covered by tests
}
return Ok(Some(existing_path.clone()));
}

Check warning on line 161 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L161

Added line #L161 was not covered by tests
}

// This is the first time we see this file, record its destination
self.inode_map.insert(key, dest.to_path_buf());

Ok(None)
}
}

impl HardlinkGroupScanner {
pub fn new() -> Self {
Self::default()
}

/// Scan files and group them by hardlinks, including recursive directory scanning
pub fn scan_files(
&mut self,
files: &[PathBuf],
options: &HardlinkOptions,
) -> HardlinkResult<()> {
if self.scanned {
return Ok(());

Check warning on line 183 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L183

Added line #L183 was not covered by tests
}

// Store the source files for destination mapping
self.source_files = files.to_vec();

for file in files {
if let Err(e) = self.scan_single_path(file) {
if options.verbose {
// Only show warnings for verbose mode
eprintln!("warning: failed to scan {}: {}", file.display(), e);

Check warning on line 193 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L192-L193

Added lines #L192 - L193 were not covered by tests
}
// For non-verbose mode, silently continue for missing files
// This provides graceful degradation - we'll lose hardlink info for this file
// but can still preserve hardlinks for other files
continue;
}
}

self.scanned = true;

if options.verbose {
let stats = self.stats();
if stats.total_groups > 0 {
eprintln!(
"found {} hardlink groups with {} total files",
stats.total_groups, stats.total_files
);
}
}

Ok(())
}

/// Scan a single path (file or directory)
fn scan_single_path(&mut self, path: &Path) -> io::Result<()> {
use std::os::unix::fs::MetadataExt;

if path.is_dir() {
// Recursively scan directory contents
self.scan_directory_recursive(path)?;
} else {
let metadata = path.metadata()?;
if metadata.nlink() > 1 {
let key = (metadata.dev(), metadata.ino());
self.hardlink_groups
.entry(key)
.or_default()
.push(path.to_path_buf());
}
}
Ok(())
}

/// Recursively scan a directory for hardlinked files
fn scan_directory_recursive(&mut self, dir: &Path) -> io::Result<()> {
use std::os::unix::fs::MetadataExt;

let entries = std::fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();

if path.is_dir() {
self.scan_directory_recursive(&path)?;
} else {
let metadata = path.metadata()?;
if metadata.nlink() > 1 {
let key = (metadata.dev(), metadata.ino());
self.hardlink_groups.entry(key).or_default().push(path);
}
}
}
Ok(())
}

#[cfg(not(unix))]
pub fn scan_files(
&mut self,
files: &[PathBuf],
_options: &HardlinkOptions,
) -> HardlinkResult<()> {
self.source_files = files.to_vec();
self.scanned = true;
Ok(())
}

#[cfg(not(unix))]
pub fn stats(&self) -> ScannerStats {
ScannerStats {
total_groups: 0,
total_files: 0,
}
}

/// Get statistics about scanned hardlinks
#[cfg(unix)]
pub fn stats(&self) -> ScannerStats {
let total_groups = self.hardlink_groups.len();
let total_files = self.hardlink_groups.values().map(|group| group.len()).sum();

ScannerStats {
total_groups,
total_files,
}
}
}

/// Statistics about hardlink scanning
#[derive(Debug, Clone)]
pub struct ScannerStats {
pub total_groups: usize,
pub total_files: usize,
}

/// Create a new hardlink tracker and scanner pair
pub fn create_hardlink_context() -> (HardlinkTracker, HardlinkGroupScanner) {
(HardlinkTracker::new(), HardlinkGroupScanner::new())
}

/// Convenient function to execute operations with proper hardlink context handling
pub fn with_optional_hardlink_context<F, R>(
tracker: Option<&mut HardlinkTracker>,
scanner: Option<&HardlinkGroupScanner>,
operation: F,
) -> R
where
F: FnOnce(&mut HardlinkTracker, &HardlinkGroupScanner) -> R,
{
match (tracker, scanner) {
(Some(tracker), Some(scanner)) => operation(tracker, scanner),
_ => {
let (mut dummy_tracker, dummy_scanner) = create_hardlink_context();
operation(&mut dummy_tracker, &dummy_scanner)

Check warning on line 316 in src/uu/mv/src/hardlink.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/hardlink.rs#L315-L316

Added lines #L315 - L316 were not covered by tests
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_hardlink_tracker_creation() {
let _tracker = HardlinkTracker::new();
// Just test that creation works
}

#[test]
fn test_scanner_creation() {
let scanner = HardlinkGroupScanner::new();
let stats = scanner.stats();
assert_eq!(stats.total_groups, 0);
assert_eq!(stats.total_files, 0);
}

#[test]
fn test_create_hardlink_context() {
let (_tracker, scanner) = create_hardlink_context();
let stats = scanner.stats();
assert_eq!(stats.total_groups, 0);
assert_eq!(stats.total_files, 0);
}
}
Loading
Loading