recursive_copy 2.1.0

A minimal, safe, and portable recursive copy library for Unix systems.
Documentation
use super::*;
use std::fs::{self, File};
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;

const SYMLINKS: bool = false;
const COPY_ONLY: bool = false;
const NO_DEST: bool = false;

fn create_file(path: &PathBuf, content: &str) {
    if let Some(p) = path.parent() {
        fs::create_dir_all(p).unwrap();
    }
    let mut file = File::create(path).unwrap();
    writeln!(file, "{}", content).unwrap();
}

fn cleanup(path: &PathBuf) {
    if path.exists() {
        fs::remove_dir_all(path).ok();
    }
}

#[test]
fn test_copy_recursive_with_symlinks() {
    let base = PathBuf::from("/tmp/recursive_copy_test_symlinks");
    let src = base.join("src");
    let dst = base.join("dst");

    cleanup(&base);

    let create = NO_DEST;
    if !create {
        fs::create_dir_all(&dst).unwrap();
    }

    fs::create_dir_all(src.join("subdir/nested")).unwrap();
    create_file(&src.join("root.txt"), "Root file");
    create_file(&src.join("subdir/file1.txt"), "File in subdir");
    create_file(&src.join("subdir/nested/deep.txt"), "Nested file");

    let symlink_file = src.join("link_to_root");
    let symlink_dir = src.join("link_to_nested");
    std::os::unix::fs::symlink("root.txt", &symlink_file).unwrap_or_default();
    std::os::unix::fs::symlink("subdir/nested", &symlink_dir).unwrap_or_default();

    let mut opts = CopyOptions::default();
    opts.follow_symlinks = SYMLINKS;
    opts.content_only = COPY_ONLY;

    let mut final_dst = PathBuf::from(&dst);
    if !opts.content_only && !create {
        final_dst = dst.join("src");
    }

    println!("--- Running Test: Recursive Copy ---");

    copy_all(&src, &dst, &opts).expect("Copy failed");

    let files = [
        "root.txt",
        "subdir/file1.txt",
        "subdir/nested/deep.txt",
        "link_to_root",
        "link_to_nested",
    ];

    for f in files {
        let path = final_dst.join(f);
        assert!(path.exists());
        let symlink_info = if path.is_symlink() { " (symlink)" } else { "" };
        println!(
            "  [OK] Copied file exists{}: {}",
            symlink_info,
            path.display()
        );
    }
}

#[test]
fn test_copy_ignore_permission_denied() {
    let base = PathBuf::from("/tmp/recursive_copy_test_permission_denied");
    let src = base.join("src");
    let dst = base.join("dst");

    cleanup(&base);
    fs::create_dir_all(&dst).unwrap();

    // Layout:
    //   src/
    //   ├── visible.txt      <- must be copied
    //   └── locked/          <- mode 000; inaccessible
    //       └── secret.txt   <- must never appear in dst
    fs::create_dir_all(src.join("locked")).unwrap();
    create_file(&src.join("visible.txt"), "visible");
    create_file(&src.join("locked/secret.txt"), "secret");

    let locked = src.join("locked");
    let original_mode = fs::metadata(&locked).unwrap().permissions().mode();
    fs::set_permissions(&locked, fs::Permissions::from_mode(0o000)).unwrap();

    let restore = || {
        let _ = fs::set_permissions(&locked, fs::Permissions::from_mode(original_mode));
    };

    println!("--- Running Test: ignore_permission_denied ---");

    {
        let opts = CopyOptions {
            content_only: true,
            ..CopyOptions::default()
        };
        let result = copy_all(&src, &dst, &opts);
        assert!(
            result.is_err(),
            "expected Err when ignore_permission_denied is false"
        );
        println!("  [OK] Error returned when ignore_permission_denied = false");
    }

    {
        let opts = CopyOptions {
            content_only: true,
            ignore_permission_denied: true,
            ..CopyOptions::default()
        };
        copy_all(&src, &dst, &opts).expect("copy_all failed with ignore_permission_denied = true");

        let visible_dst = dst.join("visible.txt");
        let secret_dst = dst.join("locked/secret.txt");

        assert!(visible_dst.exists(), "visible.txt should have been copied");
        assert!(!secret_dst.exists(), "secret.txt must not be reachable");
        println!("  [OK] visible.txt copied, secret.txt unreachable");
    }

    restore();
    cleanup(&base);
}

#[test]
fn test_copy_single_file_to_existing_dir() {
    let base = PathBuf::from("/tmp/test_single_file");
    let src_file = base.join("source_file.txt");
    let dst_dir = base.join("dest_dir");
    let expected_dst_file = dst_dir.join("source_file.txt");

    cleanup(&base);

    fs::create_dir_all(&dst_dir).unwrap();
    create_file(&src_file, "This is the content of the file.");

    let opts = CopyOptions::default();

    println!("--- Running Test: Single File Copy ---");
    copy_all(&src_file, &dst_dir, &opts).expect("File copy failed");

    assert!(dst_dir.is_dir(), "The destination must be a directory.");
    assert!(
        expected_dst_file.exists(),
        "The copied file does not exist in the expected destination."
    );
    println!(
        "  [OK] File copied successfully to directory: {}",
        expected_dst_file.display()
    );

    let new_file_name = base.join("new_file_name.txt");
    copy_all(&src_file, &new_file_name, &opts).expect("File copy failed to new name");

    assert!(
        new_file_name.exists(),
        "The file should have been copied and renamed."
    );
    println!(
        "  [OK] File copied successfully with rename: {}",
        new_file_name.display()
    );
}