use crate::sandbox::Sandbox;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use tokio::fs;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug)]
pub struct WriteResult {
pub message: String,
pub success: bool,
}
pub async fn write_file(sandbox: &Sandbox, path: &str, content: String) -> Result<String> {
tracing::info!("write_file {}", path);
let pathname = Path::new(path);
let filename = pathname
.file_name()
.with_context(|| format!("Could not get filename from path: '{}'", path))?;
let parent = pathname
.parent()
.context("Invalid path: no parent directory")?;
let canon_parent = sandbox
.resolve_path(parent.to_str().context("Invalid parent path")?)
.await?;
let temp_name = canon_parent.join(format!("tempfile-{}", Uuid::new_v4()));
let canon_filepath = canon_parent.join(filename);
if let Err(e) = fs::write(&temp_name, &content).await {
tracing::error!("Failed to write temp file: {}", e);
let _ = fs::remove_file(&temp_name).await;
anyhow::bail!("Failed to write temp file: {}", e);
}
if let Err(e) = fs::rename(&temp_name, &canon_filepath).await {
tracing::error!("Failed to rename temp file: {}", e);
let _ = fs::remove_file(&temp_name).await;
anyhow::bail!("Failed to rename temp file: {}", e);
}
tracing::info!("Successfully wrote file: {}", &canon_filepath.display());
Ok(format!(
"Successfully wrote file to {}",
&canon_filepath.display()
))
}
pub async fn create_directory(sandbox: &Sandbox, path: &str) -> Result<String> {
tracing::info!("create directory '{}'", path);
match fs::metadata(path).await {
Ok(metadata) if metadata.is_file() => {
tracing::warn!("Path '{}' is a file", path);
anyhow::bail!("Path '{}' is a file", path);
}
_ => {}
}
if sandbox.resolve_path(path).await.is_ok() {
tracing::warn!("Path '{}' already exists", path);
anyhow::bail!("Path '{}' already exists", path);
}
if sandbox.check_new_folders(path).await? {
fs::create_dir_all(path)
.await
.with_context(|| format!("Could not create dir {}", path))?;
tracing::info!("Path '{}' created.", path);
} else {
tracing::warn!("Not authorized, path '{}' outside roots", path);
anyhow::bail!("Not authorized, path '{}' outside roots", path);
}
Ok(format!("Path '{}' created.", path))
}
pub async fn move_file(
sandbox: &Sandbox,
source: &str,
destination: &str,
) -> anyhow::Result<String> {
let source_canon_path = sandbox.resolve_path(source).await?;
let dest_path = Path::new(destination);
let dest_filename = dest_path
.file_name()
.with_context(|| format!("Could not get filename from path: '{:?}'", dest_path))?;
let dest_parent = dest_path
.parent()
.context("Invalid path: no parent directory")?;
let dest_canon_parent = sandbox
.resolve_path(
dest_parent
.to_str()
.context("Invalid destination parent path")?,
)
.await?;
tokio::fs::rename(&source_canon_path, dest_canon_parent.join(dest_filename))
.await
.with_context(|| {
format!(
"Could not move '{}' to '{}'",
source_canon_path.display(),
dest_filename.display()
)
})?;
tracing::info!(
"Moved file from {} to {}",
&source_canon_path.display(),
dest_canon_parent.join(dest_filename).display()
);
Ok(format!(
"Successfully moved file from {} to {}",
&source_canon_path.display(),
dest_canon_parent.join(dest_filename).display()
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sandbox::Sandbox;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::fs;
async fn setup_sandbox(temp_dir: &TempDir) -> Arc<Sandbox> {
let root = temp_dir.path().to_string_lossy().to_string();
let sandbox = Sandbox::new(vec![root]).await.expect("sandbox init failed");
Arc::new(sandbox)
}
#[tokio::test]
async fn test_write_file_success() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("test.txt");
write_file(
&sandbox,
path.to_str().unwrap(),
"Hello, World!".to_string(),
)
.await
.expect("write_file should succeed");
let content = fs::read_to_string(&path).await.unwrap();
assert_eq!(content, "Hello, World!");
}
#[tokio::test]
async fn test_write_file_empty_content() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("empty.txt");
write_file(&sandbox, path.to_str().unwrap(), "".to_string())
.await
.expect("write_file should succeed");
let content = fs::read_to_string(&path).await.unwrap();
assert_eq!(content, "");
}
#[tokio::test]
async fn test_write_file_large_content() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("large.txt");
let content = "x".repeat(1_000_000);
write_file(&sandbox, path.to_str().unwrap(), content.clone())
.await
.expect("write_file should succeed");
let read_content = fs::read_to_string(&path).await.unwrap();
assert_eq!(read_content.len(), content.len());
}
#[tokio::test]
async fn test_write_file_overwrite_existing() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("overwrite.txt");
fs::write(&path, "initial").await.unwrap();
write_file(&sandbox, path.to_str().unwrap(), "updated".to_string())
.await
.expect("write_file should succeed");
let content = fs::read_to_string(&path).await.unwrap();
assert_eq!(content, "updated");
}
#[tokio::test]
async fn test_write_file_nested_subdir() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let subdir = temp_dir.path().join("sub");
fs::create_dir_all(&subdir).await.unwrap();
let path = subdir.join("nested.txt");
write_file(&sandbox, path.to_str().unwrap(), "nested".to_string())
.await
.expect("write_file should succeed");
let content = fs::read_to_string(&path).await.unwrap();
assert_eq!(content, "nested");
}
#[tokio::test]
async fn test_write_file_multiple_dots_in_filename() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("file.backup.tar.gz");
write_file(&sandbox, path.to_str().unwrap(), "data".to_string())
.await
.expect("write_file should succeed");
let content = fs::read_to_string(&path).await.unwrap();
assert_eq!(content, "data");
}
#[tokio::test]
async fn test_write_file_no_extension() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("README");
write_file(&sandbox, path.to_str().unwrap(), "content".to_string())
.await
.expect("write_file should succeed");
let content = fs::read_to_string(&path).await.unwrap();
assert_eq!(content, "content");
}
// Create directory tests
#[tokio::test]
async fn test_create_directory_success() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("newdir");
let result = create_directory(&sandbox, path.to_str().unwrap()).await;
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
format!("Path '{}' created.", path.display())
);
}
#[tokio::test]
async fn test_create_directory_nested() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("a/b/c");
let result = create_directory(&sandbox, &path.to_str().unwrap()).await;
assert_eq!(
result.unwrap().to_string(),
format!("Path '{}' created.", path.display())
);
}
#[tokio::test]
async fn test_create_directory_already_exists() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("existing");
fs::create_dir(&path).await.unwrap();
let result = create_directory(&sandbox, path.to_str().unwrap()).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
assert!(path.exists());
}
#[tokio::test]
async fn test_create_directory_already_exists_as_file() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = temp_dir.path().join("file.txt");
fs::write(&path, "data").await.unwrap();
let result = create_directory(&sandbox, path.to_str().unwrap()).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("is a file"));
}
#[tokio::test]
async fn test_create_directory_with_trailing_slash() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let path = format!("{}/trailing/", temp_dir.path().to_string_lossy());
let result = create_directory(&sandbox, &path).await.unwrap();
let check_path = temp_dir.path().join("trailing");
assert!(check_path.exists());
assert_eq!(result, format!("Path '{}' created.", path));
}
#[tokio::test]
async fn test_move_file_success() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let src_path = temp_dir.path().join("src.txt");
fs::write(&src_path, "hello").await.unwrap();
let dst_path = temp_dir.path().join("dst.txt");
move_file(
&sandbox,
src_path.to_str().unwrap(),
dst_path.to_str().unwrap(),
)
.await
.expect("move_file should succeed");
assert!(!src_path.exists());
assert!(dst_path.exists());
assert_eq!(fs::read_to_string(&dst_path).await.unwrap(), "hello");
}
#[tokio::test]
async fn test_move_file_overwrite() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let src = temp_dir.path().join("src.txt");
let dst = temp_dir.path().join("dst.txt");
fs::write(&src, "new").await.unwrap();
fs::write(&dst, "old").await.unwrap();
move_file(&sandbox, src.to_str().unwrap(), dst.to_str().unwrap())
.await
.expect("move_file should overwrite");
assert_eq!(fs::read_to_string(&dst).await.unwrap(), "new");
assert!(!src.exists());
}
#[tokio::test]
async fn test_move_file_nested_destination() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let nested_dir = temp_dir.path().join("subdir");
fs::create_dir_all(&nested_dir).await.unwrap();
let src = temp_dir.path().join("file.txt");
let dst = nested_dir.join("file.txt");
fs::write(&src, "data").await.unwrap();
move_file(&sandbox, src.to_str().unwrap(), dst.to_str().unwrap())
.await
.unwrap();
assert!(dst.exists());
assert!(!src.exists());
}
#[tokio::test]
async fn test_move_file_invalid_source() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let result = move_file(
&sandbox,
temp_dir.path().join("missing.txt").to_str().unwrap(),
temp_dir.path().join("dst.txt").to_str().unwrap(),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_move_file_invalid_destination() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let src = temp_dir.path().join("src.txt");
fs::write(&src, "data").await.unwrap();
// Use root as destination filename (likely invalid)
let dst = Path::new("/");
let result = move_file(&sandbox, src.to_str().unwrap(), dst.to_str().unwrap()).await;
assert!(result.is_err());
assert!(src.exists()); // original file still exists
}
#[tokio::test]
async fn test_move_file_destination_parent_missing() {
let temp_dir = TempDir::new().unwrap();
let sandbox = setup_sandbox(&temp_dir).await;
let src = temp_dir.path().join("src.txt");
fs::write(&src, "data").await.unwrap();
let dst = temp_dir.path().join("missing_dir/dst.txt");
let result = move_file(&sandbox, src.to_str().unwrap(), dst.to_str().unwrap()).await;
assert!(result.is_err());
assert!(src.exists());
}
}