use anyhow::{Ok, Result};
use console::style;
use log::{info, warn};
use std::{
fs::{copy, read_dir, remove_file, File},
io::Read,
path::Path,
};
const CACHEDIR_TAG_FILE: &str = "CACHEDIR.TAG";
const CACHEDIR_TAG_SIGNATURE: &[u8; 43] = b"Signature: 8a477f597d28d172789f06886806bc55";
pub fn is_cache_dir(dir: &Path) -> bool {
let Some(mut file) = File::open(dir.join(CACHEDIR_TAG_FILE)).ok() else {
return false;
};
let mut buf = [0u8; CACHEDIR_TAG_SIGNATURE.len()];
file.read_exact(&mut buf).is_ok() && &buf == CACHEDIR_TAG_SIGNATURE
}
pub fn copy_files_recursively(
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
overwrite: bool,
) -> Result<()> {
let dst_path = dst.as_ref();
for src_entry in read_dir(src.as_ref())? {
let src_entry = src_entry?;
let filename = src_entry.file_name().to_string_lossy().to_string();
let entry_type = src_entry.file_type()?;
if entry_type.is_dir() {
if filename == ".git" {
continue;
}
if is_cache_dir(&src_entry.path()) {
info!(
"Skipping cache directory (CACHEDIR.TAG): `{}`",
src_entry.path().display()
);
continue;
}
let dst_dir = dst_path.join(filename);
if !dst_dir.exists() {
std::fs::create_dir(&dst_dir)?;
}
copy_files_recursively(src_entry.path(), dst_dir, overwrite)?;
} else if entry_type.is_file() {
copy_file(&src_entry.path(), dst_path, overwrite)?;
} else {
warn!(
"{} {} `{}`",
crate::emoji::WARN,
style("[Skipping] Symbolic links not supported")
.bold()
.red(),
style(src_entry.path().display()).bold(),
)
}
}
Ok(())
}
fn copy_file(src_path: &Path, dst: &Path, overwrite: bool) -> Result<()> {
let filename = src_path.file_name().unwrap().to_string_lossy().to_string();
safe_copy_skip_existing(src_path, &dst.join(filename), overwrite)
}
fn safe_copy_skip_existing(src_path: &Path, dst_path: &Path, overwrite: bool) -> Result<()> {
if dst_path.exists() && !overwrite {
warn!(
"{} `{}` {}",
style("[Skipping] File already exists").bold().yellow(),
style(dst_path.display()).bold(),
style("and `--overwrite` was not passed")
);
return Ok(());
}
if dst_path.exists() && overwrite {
remove_file(dst_path)?;
copy(src_path, dst_path)?;
} else if !dst_path.exists() {
copy(src_path, dst_path)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_overwriting_behavior2() {
let tmp1 = tempdir().unwrap();
let tmp2 = tempdir().unwrap();
let f1 = tmp1.path().join("README.md");
std::fs::write(&f1, "FIRST README").unwrap();
let f2 = tmp2.path().join("README.md");
std::fs::write(&f2, "SECOND README").unwrap();
assert!(
safe_copy_skip_existing(f1.as_path(), f2.as_path(), false).is_ok(),
"we do not allow overwriting if file with same name already exists without the flag set"
);
assert_eq!(
std::fs::read_to_string(f2.as_path()).unwrap(),
"SECOND README",
"the file should not be copied"
);
assert!(
safe_copy_skip_existing(f1.as_path(), f2.as_path(), true).is_ok(),
"we do allow overwriting if file with same name already exists without the flag set"
);
assert_eq!(
std::fs::read_to_string(f2.as_path()).unwrap(),
"FIRST README"
);
}
}