pub mod file_entry;
pub mod file_entry_kind;
pub mod file_entry_stub;
pub mod memory;
pub mod read_file_contents_result;
pub mod storage;
use std::path::Path;
use std::sync::Arc;
use anyhow::Result;
use anyhow::anyhow;
use async_trait::async_trait;
use self::file_entry::FileEntry;
use self::read_file_contents_result::ReadFileContentsResult;
#[async_trait]
pub trait Filesystem: Send + Sync {
async fn read_project_files(&self) -> Result<Vec<FileEntry>>;
async fn read_file_contents(&self, path: &Path) -> Result<ReadFileContentsResult>;
async fn set_file_contents(&self, path: &Path, contents: &str) -> Result<()>;
fn set_file_contents_sync(&self, path: &Path, contents: &str) -> Result<()>;
async fn copy_file_from<TFilesystem: Filesystem>(
&self,
other: Arc<TFilesystem>,
path: &Path,
) -> Result<()> {
let other_file_contents = other.read_file_contents_string(path).await?;
self.set_file_contents(path, &other_file_contents).await?;
Ok(())
}
async fn copy_project_files_from<TFilesystem: Filesystem>(
&self,
other: Arc<TFilesystem>,
) -> Result<()> {
for FileEntry {
contents,
relative_path,
..
} in other.read_project_files().await?
{
self.set_file_contents(&relative_path, &contents).await?;
}
Ok(())
}
async fn read_file_contents_string(&self, path: &Path) -> Result<String> {
match self.read_file_contents(path).await? {
ReadFileContentsResult::Directory => {
return Err(anyhow!("File is a directory: '{}'", path.display()));
}
ReadFileContentsResult::Found { contents } => Ok(contents),
ReadFileContentsResult::NotFound => {
return Err(anyhow!("File not found: '{}'", path.display()));
}
}
}
}
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use tempfile::tempdir;
use super::*;
async fn test_filesystem<TFilesystem>(log_prefix: &str, filesystem: TFilesystem) -> Result<()>
where
TFilesystem: Filesystem,
{
println!("{log_prefix} - Creating 'content/test.md'");
filesystem
.set_file_contents(Path::new("content/test.md"), "Hello, World! 1")
.await?;
println!("{log_prefix} - Creating 'content/test2.md'");
filesystem
.set_file_contents(Path::new("content/test2.md"), "Hello, World! 2")
.await?;
println!("{log_prefix} - Creating 'content/test/3.md'");
filesystem
.set_file_contents(Path::new("content/test/3.md"), "Hello, World! 3")
.await?;
println!("{log_prefix} - Creating 'content/test/4/5/6.md'");
filesystem
.set_file_contents(Path::new("content/test/4/5/6.md"), "Hello, World! 456")
.await?;
println!("{log_prefix} - Reading project files");
let mut files = filesystem.read_project_files().await?;
println!("{log_prefix} - Sorting project files");
files.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
assert_eq!(files.len(), 4);
assert_eq!(
files[0].relative_path.to_path_buf().display().to_string(),
"content/test/3.md"
);
assert_eq!(files[0].contents, "Hello, World! 3");
match filesystem
.read_file_contents(Path::new("content/test/3.md"))
.await?
{
ReadFileContentsResult::Directory => {
return Err(anyhow!("Expected file, got directory"));
}
ReadFileContentsResult::Found { contents } => assert_eq!(contents, "Hello, World! 3"),
ReadFileContentsResult::NotFound => return Err(anyhow!("File not found")),
}
assert_eq!(
files[1].relative_path.to_path_buf().display().to_string(),
"content/test/4/5/6.md"
);
assert_eq!(files[1].contents, "Hello, World! 456");
match filesystem
.read_file_contents(Path::new("content/test/4/5/6.md"))
.await?
{
ReadFileContentsResult::Directory => {
return Err(anyhow!("Expected file, got directory"));
}
ReadFileContentsResult::Found { contents } => assert_eq!(contents, "Hello, World! 456"),
ReadFileContentsResult::NotFound => return Err(anyhow!("File not found")),
}
assert_eq!(
files[2].relative_path.to_path_buf().display().to_string(),
"content/test.md"
);
assert_eq!(files[2].contents, "Hello, World! 1");
match filesystem
.read_file_contents(Path::new("content/test.md"))
.await?
{
ReadFileContentsResult::Directory => {
return Err(anyhow!("Expected file, got directory"));
}
ReadFileContentsResult::Found { contents } => assert_eq!(contents, "Hello, World! 1"),
ReadFileContentsResult::NotFound => return Err(anyhow!("File not found")),
}
assert_eq!(
files[3].relative_path.to_path_buf().display().to_string(),
"content/test2.md"
);
assert_eq!(files[3].contents, "Hello, World! 2");
match filesystem
.read_file_contents(Path::new("content/test2.md"))
.await?
{
ReadFileContentsResult::Directory => {
return Err(anyhow!("Expected file, got directory"));
}
ReadFileContentsResult::Found { contents } => assert_eq!(contents, "Hello, World! 2"),
ReadFileContentsResult::NotFound => return Err(anyhow!("File not found")),
}
match filesystem
.read_file_contents(Path::new("test_not_found.md"))
.await?
{
ReadFileContentsResult::Directory => {
return Err(anyhow!("Expected file, got directory"));
}
ReadFileContentsResult::Found { contents } => {
return Err(anyhow!("File should not be found: {contents}"));
}
ReadFileContentsResult::NotFound => {}
}
Ok(())
}
#[tokio::test]
async fn test_files_are_set_and_read() -> Result<()> {
test_filesystem("Memory", memory::Memory::default()).await?;
{
let base_directory = tempdir()?;
test_filesystem(
"Storage",
storage::Storage {
base_directory: base_directory.path().to_path_buf(),
},
)
.await?;
}
Ok(())
}
#[tokio::test]
async fn copies_single_file_between_filesystems() -> Result<()> {
let source = Arc::new(memory::Memory::default());
source
.set_file_contents(Path::new("content/a.md"), "alpha")
.await?;
let destination = memory::Memory::default();
destination
.copy_file_from(source, Path::new("content/a.md"))
.await?;
assert_eq!(
destination
.read_file_contents_string(Path::new("content/a.md"))
.await?,
"alpha"
);
Ok(())
}
#[tokio::test]
async fn copies_all_project_files_between_filesystems() -> Result<()> {
let source = Arc::new(memory::Memory::default());
source
.set_file_contents(Path::new("content/a.md"), "alpha")
.await?;
source
.set_file_contents(Path::new("content/b.md"), "beta")
.await?;
let destination = memory::Memory::default();
destination.copy_project_files_from(source).await?;
assert_eq!(
destination
.read_file_contents_string(Path::new("content/a.md"))
.await?,
"alpha"
);
assert_eq!(
destination
.read_file_contents_string(Path::new("content/b.md"))
.await?,
"beta"
);
Ok(())
}
#[tokio::test]
async fn read_file_contents_string_fails_for_missing_file() {
let filesystem = memory::Memory::default();
assert!(
filesystem
.read_file_contents_string(Path::new("missing.md"))
.await
.is_err()
);
}
#[tokio::test]
async fn read_file_contents_string_fails_for_directory() -> Result<()> {
let base_directory = tempdir()?;
let filesystem = storage::Storage {
base_directory: base_directory.path().to_path_buf(),
};
filesystem
.set_file_contents(Path::new("content/a.md"), "alpha")
.await?;
assert!(
filesystem
.read_file_contents_string(Path::new("content"))
.await
.is_err()
);
Ok(())
}
}