use anyhow::{bail, Ok, Result};
use console::style;
use log::{debug, warn};
use std::{
fs::{copy, read_dir, remove_file},
path::Path,
};
pub const LIQUID_SUFFIX: &str = ".liquid";
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;
}
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();
let dst_path = dst.join(&filename);
let mut overwrite = overwrite;
if let Some(new_filename) = filename.strip_suffix(LIQUID_SUFFIX) {
if src_path.with_file_name(new_filename).exists() {
debug!("A non-liquid file exists for {filename}, overwriting it with the liquid file");
overwrite = true;
}
let dst_path = dst.join(new_filename);
safe_copy(src_path, &dst_path, overwrite)?;
} else if src_path
.with_file_name(format!("{filename}{LIQUID_SUFFIX}"))
.exists()
{
debug!("A liquid file exists for {filename}, skipping the non-liquid file");
return Ok(());
} else {
safe_copy_skip_existing(src_path, &dst_path, overwrite)?;
}
Ok(())
}
fn safe_copy(src_path: &Path, dst_path: &Path, overwrite: bool) -> Result<()> {
if dst_path.exists() && !overwrite {
bail!(
"{} {} `{}` {}",
crate::emoji::ERROR,
style("File already exists").bold().red(),
style(dst_path.display()).bold(),
style("and `--overwrite` was not passed")
)
}
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(())
}
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_behavior() {
let tmp = tempdir().unwrap();
let f1 = tmp.path().join("README.md");
std::fs::write(&f1, "A README").unwrap();
let f2 = tmp.path().join("README.md.liquid");
std::fs::write(&f2, "A README liquid file").unwrap();
assert!(
safe_copy(f1.as_path(), f2.as_path(), false).is_err(),
"we do not allow overwriting without the flag set"
);
assert!(
safe_copy(f1.as_path(), f2.as_path(), true).is_ok(),
"we do allow overwriting with the flag set"
);
assert_eq!(std::fs::read_to_string(f2.as_path()).unwrap(), "A README");
}
#[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"
);
}
#[test]
fn test_special_liquid_file_handling() {
let tmp = tempdir().unwrap();
let f1 = tmp.path().join("README.md");
std::fs::write(&f1, "A README").unwrap();
let f2 = tmp.path().join("README.md.liquid");
std::fs::write(&f2, "A README liquid file").unwrap();
let tmp2 = tempdir().unwrap();
copy_file(f1.as_path(), tmp2.path(), false).unwrap();
assert!(
!tmp2.path().join("README.md").exists(),
"the file should not be copied"
);
copy_file(f2.as_path(), tmp2.path(), false).unwrap();
assert!(
tmp2.path().join("README.md").exists(),
"the file should be copied and the .liquid suffix removed"
);
assert_eq!(
std::fs::read_to_string(tmp2.path().join("README.md")).unwrap(),
"A README liquid file"
);
let f4 = tmp2.path().join("README.md");
std::fs::write(&f4, "Existing file, should be overwritten").unwrap();
assert!(
copy_file(f2.as_path(), tmp2.path(), false).is_ok(),
"the file should be copied"
);
assert_eq!(
std::fs::read_to_string(tmp2.path().join("README.md")).unwrap(),
"A README liquid file"
);
}
}