blob: 9897e1708509a4b1c17932913ed15314b26cd09a [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! This module provides a way to copy files preferentially using hardlinks, and
//! will fall back to a slow copy if that fails.
use anyhow::{Context, Result};
use std::path::Path;
/// Copy a file from `src` to `dst`, using hardlinks if possible, and silently
/// falling back to a "slow" copy (using `std::fs::copy()`) if the hardlink
/// fails to be created.
pub fn fast_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
let src = src.as_ref();
let dst = dst.as_ref();
// Defer to a function that doesn't need to be monomorphized.
fast_copy_impl(src, dst)
}
/// An internal helper method that doesn't use generics
fn fast_copy_impl(src: &Path, dst: &Path) -> Result<()> {
// Get the actual src file, if it exists, as 'hard_link' and 'copy' won't
// follow symlinks, but copy the symlink itself (which isn't our goal)
let real_path = src
.canonicalize()
.with_context(|| format!("getting the real path for: {}", src.display()))?;
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating parent directories for {}: ", dst.display()))?;
}
// Perform a hardlink if possible
std::fs::hard_link(&real_path, dst).or_else(|_| {
// falling back to a slow copy if it can't be copied.
std::fs::copy(&real_path, dst)
.map(|_| ())
.with_context(|| format!("copying {} to {}", src.display(), dst.display()))
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_simple_copy() {
let temp_dir = TempDir::new().unwrap();
let src = temp_dir.path().join("src.txt");
let dst = temp_dir.path().join("dst.txt");
std::fs::write(&src, "some text").unwrap();
fast_copy(src, &dst).unwrap();
let dst_contents = std::fs::read(dst).unwrap();
let dst_string = String::from_utf8(dst_contents).unwrap();
assert_eq!(dst_string, "some text");
}
#[test]
fn test_missing_source_fails() {
let temp_dir = TempDir::new().unwrap();
let src = temp_dir.path().join("src.txt");
let dst = temp_dir.path().join("dst.txt");
let result = fast_copy(src, &dst);
assert!(result.is_err());
}
#[test]
fn test_creates_parents_of_dest() {
let temp_dir = TempDir::new().unwrap();
let src = temp_dir.path().join("src.txt");
let dst = temp_dir.path().join("some/nonexistent/parent/dst.txt");
std::fs::write(&src, "some text").unwrap();
fast_copy(src, &dst).unwrap();
let dst_contents = std::fs::read(dst).unwrap();
let dst_string = String::from_utf8(dst_contents).unwrap();
assert_eq!(dst_string, "some text");
}
}
#[cfg(test)]
#[cfg(target_family = "unix")]
mod tests_unix {
use super::*;
use std::os::unix::fs::MetadataExt;
use tempfile::TempDir;
#[test]
fn test_copy_is_hardlink() {
let temp_dir = TempDir::new().unwrap();
let src = temp_dir.path().join("src.txt");
let dst = temp_dir.path().join("dst.txt");
std::fs::write(&src, "some text").unwrap();
fast_copy(&src, &dst).unwrap();
let dst_contents = std::fs::read(&dst).unwrap();
let dst_string = String::from_utf8(dst_contents).unwrap();
assert_eq!(dst_string, "some text");
let src_metadata = std::fs::metadata(&src).unwrap();
let dst_metadata = std::fs::metadata(&dst).unwrap();
assert_eq!(
src_metadata.ino(),
dst_metadata.ino(),
"src and dst files have different inodes"
)
}
#[test]
fn test_copy_through_symlink_is_hardlink() {
let temp_dir = TempDir::new().unwrap();
let src = temp_dir.path().join("src.txt");
let symlink = temp_dir.path().join("symlink.txt");
let dst = temp_dir.path().join("dst.txt");
std::fs::write(&src, "some text").unwrap();
std::os::unix::fs::symlink(&src, &symlink).unwrap();
fast_copy(&symlink, &dst).unwrap();
let dst_contents = std::fs::read(&dst).unwrap();
let dst_string = String::from_utf8(dst_contents).unwrap();
assert_eq!(dst_string, "some text");
// Validate that it copied the source file, not the symlink. This is after
// the contents have been validated to ensure that the file actually exists,
// as this will return false if the file doesn't exist.
assert!(!dst.is_symlink());
// Validate that the dst file is a hardlink to the src file, as they should
// have the same inode if they are hardlinks to the same file.
let src_metadata = std::fs::metadata(&src).unwrap();
let dst_metadata = std::fs::metadata(&dst).unwrap();
assert_eq!(
src_metadata.ino(),
dst_metadata.ino(),
"src and dst files have different inodes"
)
}
#[test]
fn test_copy_through_broken_symlink_fails() {
let temp_dir = TempDir::new().unwrap();
let src = temp_dir.path().join("src.txt");
let symlink = temp_dir.path().join("symlink.txt");
let dst = temp_dir.path().join("dst.txt");
std::os::unix::fs::symlink(&src, &symlink).unwrap();
let result = fast_copy(&symlink, &dst);
assert!(result.is_err());
}
}