use anyhow::Result;
use std::path::Path;
pub async fn create_dir(path: &Path) -> Result<()> {
tokio::fs::create_dir_all(path).await?;
Ok(())
}
pub fn copy_dir<'a>(
from: &'a Path,
to: &'a Path,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
create_dir(to).await?;
let mut entries = tokio::fs::read_dir(from).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let relative_path = path.strip_prefix(from)?;
let dst_path = to.join(relative_path);
let metadata = tokio::fs::symlink_metadata(&path).await?;
if metadata.is_symlink() {
let target = tokio::fs::read_link(&path).await?;
if let Some(parent) = dst_path.parent() {
create_dir(parent).await?;
}
#[cfg(unix)]
tokio::fs::symlink(&target, &dst_path).await?;
#[cfg(windows)]
{
if let Ok(target_metadata) = tokio::fs::metadata(&path).await {
if target_metadata.is_dir() {
tokio::fs::symlink_dir(&target, &dst_path).await?;
} else {
tokio::fs::symlink_file(&target, &dst_path).await?;
}
} else {
tokio::fs::symlink_file(&target, &dst_path).await?;
}
}
} else if metadata.is_dir() {
copy_dir(&path, &dst_path).await?;
} else {
if let Some(parent) = dst_path.parent() {
create_dir(parent).await?;
}
copy_file(&path, &dst_path).await?;
}
}
Ok(())
})
}
pub async fn write_file(path: &Path, content: &str) -> Result<()> {
if let Some(parent) = path.parent() {
create_dir(parent).await?;
}
let temp_path = {
let mut temp = path.to_path_buf();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = std::process::id();
temp.set_file_name(format!(
".{}.{}.{}.tmp",
path.file_name().unwrap_or_default().to_string_lossy(),
pid,
timestamp
));
temp
};
tokio::fs::write(&temp_path, content).await?;
match tokio::fs::rename(&temp_path, path).await {
Ok(()) => Ok(()),
Err(e) => {
let _ = tokio::fs::remove_file(&temp_path).await;
Err(e.into())
}
}
}
pub async fn read_file(path: &Path) -> Result<String> {
let content = tokio::fs::read_to_string(path).await?;
Ok(content)
}
pub async fn exists(path: &Path) -> Result<bool> {
Ok(tokio::fs::try_exists(path).await.unwrap_or(false))
}
pub async fn remove_dir(path: &Path) -> Result<()> {
tokio::fs::remove_dir_all(path).await?;
Ok(())
}
pub async fn remove_file(path: &Path) -> Result<()> {
tokio::fs::remove_file(path).await?;
Ok(())
}
pub async fn copy_file(from: &Path, to: &Path) -> Result<()> {
tokio::fs::copy(from, to).await?;
let metadata = tokio::fs::metadata(from).await?;
tokio::fs::set_permissions(to, metadata.permissions()).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use tempfile::TempDir;
#[tokio::test]
async fn test_create_and_read_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
let content = "Hello, world!";
write_file(&file_path, content).await.unwrap();
let read_content = read_file(&file_path).await.unwrap();
assert_eq!(read_content, content);
assert!(exists(&file_path).await.unwrap());
}
#[tokio::test]
async fn test_create_dir() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().join("test_dir");
create_dir(&dir_path).await.unwrap();
assert!(exists(&dir_path).await.unwrap());
}
#[tokio::test]
async fn test_copy_file() {
let temp_dir = TempDir::new().unwrap();
let source_path = temp_dir.path().join("source.txt");
let dest_path = temp_dir.path().join("dest.txt");
let content = "Test content";
write_file(&source_path, content).await.unwrap();
copy_file(&source_path, &dest_path).await.unwrap();
assert!(exists(&source_path).await.unwrap());
assert!(exists(&dest_path).await.unwrap());
let dest_content = read_file(&dest_path).await.unwrap();
assert_eq!(dest_content, content);
}
#[tokio::test]
async fn test_copy_dir() {
let temp_dir = TempDir::new().unwrap();
let source_dir = temp_dir.path().join("source_dir");
let dest_dir = temp_dir.path().join("dest_dir");
create_dir(&source_dir).await.unwrap();
write_file(&source_dir.join("file1.txt"), "content1")
.await
.unwrap();
create_dir(&source_dir.join("subdir")).await.unwrap();
write_file(&source_dir.join("subdir").join("file2.txt"), "content2")
.await
.unwrap();
copy_dir(&source_dir, &dest_dir).await.unwrap();
assert!(exists(&dest_dir).await.unwrap());
assert!(exists(&dest_dir.join("file1.txt")).await.unwrap());
assert!(exists(&dest_dir.join("subdir")).await.unwrap());
assert!(
exists(&dest_dir.join("subdir").join("file2.txt"))
.await
.unwrap()
);
let content1 = read_file(&dest_dir.join("file1.txt")).await.unwrap();
assert_eq!(content1, "content1");
let content2 = read_file(&dest_dir.join("subdir").join("file2.txt"))
.await
.unwrap();
assert_eq!(content2, "content2");
}
#[tokio::test]
async fn test_copy_dir_with_symlinks() {
let temp_dir = TempDir::new().unwrap();
let source_dir = temp_dir.path().join("source_dir");
let dest_dir = temp_dir.path().join("dest_dir");
let target_dir = temp_dir.path().join("target_dir");
create_dir(&source_dir).await.unwrap();
write_file(&source_dir.join("regular_file.txt"), "regular content")
.await
.unwrap();
create_dir(&target_dir).await.unwrap();
write_file(&target_dir.join("target_file.txt"), "target content")
.await
.unwrap();
create_dir(&target_dir.join("target_subdir")).await.unwrap();
write_file(
&target_dir.join("target_subdir").join("nested.txt"),
"nested content",
)
.await
.unwrap();
#[cfg(unix)]
{
tokio::fs::symlink(
&target_dir.join("target_file.txt"),
&source_dir.join("symlink_to_file"),
)
.await
.unwrap();
tokio::fs::symlink(
&target_dir.join("target_subdir"),
&source_dir.join("symlink_to_dir"),
)
.await
.unwrap();
tokio::fs::symlink(
"../target_dir/target_file.txt",
&source_dir.join("relative_symlink"),
)
.await
.unwrap();
}
#[cfg(windows)]
{
tokio::fs::symlink_file(
&target_dir.join("target_file.txt"),
&source_dir.join("symlink_to_file"),
)
.await
.unwrap();
tokio::fs::symlink_dir(
&target_dir.join("target_subdir"),
&source_dir.join("symlink_to_dir"),
)
.await
.unwrap();
tokio::fs::symlink_file(
"../target_dir/target_file.txt",
&source_dir.join("relative_symlink"),
)
.await
.unwrap();
}
copy_dir(&source_dir, &dest_dir).await.unwrap();
assert!(exists(&dest_dir.join("regular_file.txt")).await.unwrap());
let content = read_file(&dest_dir.join("regular_file.txt")).await.unwrap();
assert_eq!(content, "regular content");
#[cfg(unix)]
{
let symlink_metadata = tokio::fs::symlink_metadata(&dest_dir.join("symlink_to_file"))
.await
.unwrap();
assert!(symlink_metadata.is_symlink());
let dir_symlink_metadata =
tokio::fs::symlink_metadata(&dest_dir.join("symlink_to_dir"))
.await
.unwrap();
assert!(dir_symlink_metadata.is_symlink());
let rel_symlink_metadata =
tokio::fs::symlink_metadata(&dest_dir.join("relative_symlink"))
.await
.unwrap();
assert!(rel_symlink_metadata.is_symlink());
let link_target = tokio::fs::read_link(&dest_dir.join("symlink_to_file"))
.await
.unwrap();
assert_eq!(link_target, target_dir.join("target_file.txt"));
let dir_link_target = tokio::fs::read_link(&dest_dir.join("symlink_to_dir"))
.await
.unwrap();
assert_eq!(dir_link_target, target_dir.join("target_subdir"));
let rel_link_target = tokio::fs::read_link(&dest_dir.join("relative_symlink"))
.await
.unwrap();
assert_eq!(
rel_link_target.to_string_lossy(),
"../target_dir/target_file.txt"
);
}
#[cfg(windows)]
{
let symlink_metadata = tokio::fs::symlink_metadata(&dest_dir.join("symlink_to_file"))
.await
.unwrap();
assert!(symlink_metadata.is_symlink());
let dir_symlink_metadata =
tokio::fs::symlink_metadata(&dest_dir.join("symlink_to_dir"))
.await
.unwrap();
assert!(dir_symlink_metadata.is_symlink());
}
}
#[tokio::test]
async fn test_remove_dir() {
let temp_dir = TempDir::new().unwrap();
let test_dir = temp_dir.path().join("test_dir");
create_dir(&test_dir).await.unwrap();
write_file(&test_dir.join("file.txt"), "content")
.await
.unwrap();
assert!(exists(&test_dir).await.unwrap());
remove_dir(&test_dir).await.unwrap();
assert!(!exists(&test_dir).await.unwrap());
}
#[tokio::test]
async fn test_remove_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
let content = "Test content";
write_file(&file_path, content).await.unwrap();
assert!(exists(&file_path).await.unwrap());
remove_file(&file_path).await.unwrap();
assert!(!exists(&file_path).await.unwrap());
let result = remove_file(&file_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_concurrent_write_and_read_safety() {
use tokio::task;
let temp_dir = TempDir::new().unwrap();
let file_path = Arc::new(temp_dir.path().join("concurrent_test.json"));
let initial_content = r#"{"tasks": []}"#;
write_file(&file_path, initial_content).await.unwrap();
let mut handles = vec![];
for i in 0..5 {
let path_clone = file_path.clone();
handles.push(task::spawn(async move {
for j in 0..20 {
let content = format!(r#"{{"task_id": {}, "iteration": {}}}"#, i, j);
write_file(&path_clone, &content).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
}));
}
for _ in 0..5 {
let path_clone = file_path.clone();
handles.push(task::spawn(async move {
for _ in 0..50 {
let content = read_file(&path_clone).await.unwrap();
assert!(!content.is_empty(), "File should never be empty");
let _: serde_json::Value = serde_json::from_str(&content)
.expect("File should always contain valid JSON");
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
}));
}
for handle in handles {
handle.await.unwrap();
}
}
#[cfg(unix)]
#[tokio::test]
async fn test_copy_file_preserves_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().unwrap();
let source_path = temp_dir.path().join("executable.sh");
let dest_path = temp_dir.path().join("executable_copy.sh");
write_file(&source_path, "#!/bin/bash\necho hello")
.await
.unwrap();
let perms = std::fs::Permissions::from_mode(0o755);
tokio::fs::set_permissions(&source_path, perms)
.await
.unwrap();
copy_file(&source_path, &dest_path).await.unwrap();
let dest_metadata = tokio::fs::metadata(&dest_path).await.unwrap();
let dest_mode = dest_metadata.permissions().mode();
assert_eq!(
dest_mode & 0o777,
0o755,
"Destination file should preserve executable permissions"
);
}
}