upstream-rs 1.4.2

Fetch package updates directly from the source.
use anyhow::{Context, Result};
use std::{
    fs, io,
    path::{Path, PathBuf},
};

pub fn move_file_or_dir(src: &Path, dst: &Path) -> Result<()> {
    move_file_or_dir_with_rename(src, dst, |from, to| fs::rename(from, to))
}

fn move_file_or_dir_with_rename<F>(src: &Path, dst: &Path, mut rename_fn: F) -> Result<()>
where
    F: FnMut(&Path, &Path) -> io::Result<()>,
{
    match rename_fn(src, dst) {
        Ok(()) => Ok(()),
        Err(err) if is_cross_device(&err) => fallback_move(src, dst),
        Err(err) => Err(err).context(format!(
            "Failed to move '{}' to '{}'",
            src.display(),
            dst.display()
        )),
    }
}

pub fn is_cross_device(err: &io::Error) -> bool {
    err.kind() == io::ErrorKind::CrossesDevices
}

fn fallback_move(src: &Path, dst: &Path) -> Result<()> {
    let metadata = fs::metadata(src)
        .with_context(|| format!("Failed to read metadata for '{}'", src.display()))?;

    if metadata.is_dir() {
        copy_dir_recursive(src, dst)
            .with_context(|| format!("Failed to copy directory to '{}'", dst.display()))?;
        fs::remove_dir_all(src)
            .with_context(|| format!("Failed to remove source directory '{}'", src.display()))?;
        return Ok(());
    }

    fs::copy(src, dst).with_context(|| {
        format!(
            "Failed to copy file from '{}' to '{}'",
            src.display(),
            dst.display()
        )
    })?;
    fs::set_permissions(dst, metadata.permissions())
        .with_context(|| format!("Failed to preserve file permissions on '{}'", dst.display()))?;
    fs::remove_file(src)
        .with_context(|| format!("Failed to remove source file '{}'", src.display()))?;
    Ok(())
}

fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> {
    if dst.exists() {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            format!("Destination already exists: '{}'", dst.display()),
        ));
    }

    fs::create_dir_all(dst)?;
    let src_metadata = fs::metadata(src)?;
    fs::set_permissions(dst, src_metadata.permissions())?;

    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let entry_path = entry.path();
        let target_path: PathBuf = dst.join(entry.file_name());
        let file_type = entry.file_type()?;

        if file_type.is_dir() {
            copy_dir_recursive(&entry_path, &target_path)?;
        } else if file_type.is_file() {
            fs::copy(&entry_path, &target_path)?;
            let source_permissions = fs::metadata(&entry_path)?.permissions();
            fs::set_permissions(&target_path, source_permissions)?;
        } else if file_type.is_symlink() {
            copy_symlink(&entry_path, &target_path)?;
        } else {
            return Err(io::Error::other(format!(
                "Unsupported entry type while moving directory: '{}'",
                entry_path.display()
            )));
        }
    }

    Ok(())
}

fn copy_symlink(src: &Path, dst: &Path) -> io::Result<()> {
    let link_target = fs::read_link(src)?;

    #[cfg(unix)]
    {
        return std::os::unix::fs::symlink(link_target, dst);
    }

    #[cfg(windows)]
    {
        if src.metadata()?.is_dir() {
            return std::os::windows::fs::symlink_dir(link_target, dst);
        }
        return std::os::windows::fs::symlink_file(link_target, dst);
    }
}

#[cfg(test)]
#[path = "../../tests/utils/fs_move.rs"]
mod tests;