use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub fn is_cross_device_error(e: &std::io::Error) -> bool {
#[cfg(unix)]
{
e.raw_os_error() == Some(libc::EXDEV)
}
#[cfg(windows)]
{
e.raw_os_error() == Some(17) }
}
pub fn symlink(original: &Path, link: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(original, link)
}
#[cfg(windows)]
{
if original.is_dir() {
std::os::windows::fs::symlink_dir(original, link)
} else {
std::os::windows::fs::symlink_file(original, link)
}
}
}
fn walk_dir(dir: &Path, visitor: &mut dyn FnMut(&Path) -> Result<()>) -> Result<()> {
if !dir.exists() {
return Ok(());
}
for entry in std::fs::read_dir(dir)
.with_context(|| format!("failed to read directory: {}", dir.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_dir() {
walk_dir(&path, visitor)?;
} else {
visitor(&path)?;
}
}
Ok(())
}
pub fn walk_files_relative(base: &Path) -> Result<Vec<(String, PathBuf)>> {
let mut files = Vec::new();
walk_dir(base, &mut |path| {
let rel = path
.strip_prefix(base)
.with_context(|| "failed to compute relative path")?;
files.push((rel.to_string_lossy().to_string(), path.to_path_buf()));
Ok(())
})?;
Ok(files)
}
pub fn walk_files(dir: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
walk_dir(dir, &mut |path| {
files.push(path.to_path_buf());
Ok(())
})?;
Ok(files)
}
pub fn count_files(dir: &Path) -> Result<u64> {
let mut count = 0u64;
walk_dir(dir, &mut |_| {
count += 1;
Ok(())
})?;
Ok(count)
}
pub async fn symlink_async(original: &Path, link: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
}
#[cfg(windows)]
{
if original.is_dir() {
tokio::fs::symlink_dir(original, link).await
} else {
tokio::fs::symlink_file(original, link).await
}
}
}
pub fn deploy_symlinks(src: &Path, dst: &Path) -> Result<()> {
if !dst.exists() {
std::fs::create_dir_all(dst)?;
}
for entry in std::fs::read_dir(src)
.with_context(|| format!("failed to read staging dir: {}", src.display()))?
{
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
std::fs::create_dir_all(&dst_path)?;
deploy_symlinks(&src_path, &dst_path)?;
} else {
if dst_path.exists() || dst_path.symlink_metadata().is_ok() {
std::fs::remove_file(&dst_path)?;
}
symlink(&src_path, &dst_path)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn walk_files_relative_empty() {
let tmp = TempDir::new().unwrap();
let files = walk_files_relative(tmp.path()).unwrap();
assert!(files.is_empty());
}
#[test]
fn walk_files_relative_flat() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("a.txt"), "a").unwrap();
std::fs::write(tmp.path().join("b.esp"), "b").unwrap();
let mut files = walk_files_relative(tmp.path()).unwrap();
files.sort_by(|a, b| a.0.cmp(&b.0));
assert_eq!(files.len(), 2);
assert_eq!(files[0].0, "a.txt");
assert_eq!(files[1].0, "b.esp");
}
#[test]
fn walk_files_relative_nested() {
let tmp = TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join("sub/deep")).unwrap();
std::fs::write(tmp.path().join("sub/deep/file.txt"), "x").unwrap();
std::fs::write(tmp.path().join("top.txt"), "y").unwrap();
let files = walk_files_relative(tmp.path()).unwrap();
assert_eq!(files.len(), 2);
let rels: Vec<&str> = files.iter().map(|(r, _)| r.as_str()).collect();
assert!(rels.contains(&"top.txt"));
assert!(rels.contains(&"sub/deep/file.txt"));
}
#[test]
fn walk_files_relative_nonexistent() {
let tmp = TempDir::new().unwrap();
let files = walk_files_relative(&tmp.path().join("nope")).unwrap();
assert!(files.is_empty());
}
#[test]
fn walk_files_flat_test() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("a"), "a").unwrap();
std::fs::write(tmp.path().join("b"), "b").unwrap();
let files = walk_files(tmp.path()).unwrap();
assert_eq!(files.len(), 2);
assert!(files.iter().all(|p| p.is_absolute()));
}
#[test]
fn count_files_test() {
let tmp = TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join("sub")).unwrap();
std::fs::write(tmp.path().join("a"), "a").unwrap();
std::fs::write(tmp.path().join("sub/b"), "b").unwrap();
assert_eq!(count_files(tmp.path()).unwrap(), 2);
}
#[test]
fn count_files_nonexistent() {
let tmp = TempDir::new().unwrap();
assert_eq!(count_files(&tmp.path().join("nope")).unwrap(), 0);
}
#[test]
fn deploy_symlinks_test() {
let tmp = TempDir::new().unwrap();
let src = tmp.path().join("src");
let dst = tmp.path().join("dst");
std::fs::create_dir_all(src.join("sub")).unwrap();
std::fs::write(src.join("a.txt"), "a").unwrap();
std::fs::write(src.join("sub/b.txt"), "b").unwrap();
deploy_symlinks(&src, &dst).unwrap();
assert!(dst.join("a.txt").symlink_metadata().unwrap().file_type().is_symlink());
assert!(dst.join("sub/b.txt").symlink_metadata().unwrap().file_type().is_symlink());
assert_eq!(std::fs::read_to_string(dst.join("a.txt")).unwrap(), "a");
assert_eq!(std::fs::read_to_string(dst.join("sub/b.txt")).unwrap(), "b");
}
}