use std::path::PathBuf;
use anyhow::Context as _;
use async_trait::async_trait;
use crate::path::AbsolutePath;
#[async_trait]
pub trait Filesystem: Send + Sync + std::fmt::Debug {
async fn read_to_string(&self, path: &AbsolutePath) -> anyhow::Result<String>;
async fn read(&self, path: &AbsolutePath) -> anyhow::Result<Vec<u8>>;
async fn write(&self, path: &AbsolutePath, contents: &[u8]) -> anyhow::Result<()>;
async fn create_dir_all(&self, path: &AbsolutePath) -> anyhow::Result<()>;
async fn remove_file(&self, path: &AbsolutePath) -> anyhow::Result<()>;
async fn exists(&self, path: &AbsolutePath) -> anyhow::Result<bool>;
async fn is_dir(&self, path: &AbsolutePath) -> anyhow::Result<bool>;
async fn canonicalize(&self, path: &AbsolutePath) -> anyhow::Result<PathBuf>;
async fn glob(&self, pattern: &str) -> anyhow::Result<Vec<PathBuf>>;
async fn file_size(&self, path: &AbsolutePath) -> anyhow::Result<u64>;
}
#[derive(Debug)]
pub struct LocalFilesystem;
#[async_trait]
impl Filesystem for LocalFilesystem {
async fn read_to_string(&self, path: &AbsolutePath) -> anyhow::Result<String> {
tokio::fs::read_to_string(path.as_path())
.await
.with_context(|| format!("Failed to read {}", path.display()))
}
async fn read(&self, path: &AbsolutePath) -> anyhow::Result<Vec<u8>> {
tokio::fs::read(path.as_path())
.await
.with_context(|| format!("Failed to read {}", path.display()))
}
async fn write(&self, path: &AbsolutePath, contents: &[u8]) -> anyhow::Result<()> {
tokio::fs::write(path.as_path(), contents)
.await
.with_context(|| format!("Failed to write {}", path.display()))
}
async fn create_dir_all(&self, path: &AbsolutePath) -> anyhow::Result<()> {
tokio::fs::create_dir_all(path.as_path())
.await
.with_context(|| format!("Failed to create directory {}", path.display()))
}
async fn remove_file(&self, path: &AbsolutePath) -> anyhow::Result<()> {
tokio::fs::remove_file(path.as_path())
.await
.with_context(|| format!("Failed to remove {}", path.display()))
}
async fn exists(&self, path: &AbsolutePath) -> anyhow::Result<bool> {
tokio::fs::try_exists(path.as_path())
.await
.with_context(|| format!("Failed to check if {} exists", path.display()))
}
async fn is_dir(&self, path: &AbsolutePath) -> anyhow::Result<bool> {
match tokio::fs::metadata(path.as_path()).await {
Ok(m) => Ok(m.is_dir()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(anyhow::anyhow!(e))
.with_context(|| format!("Failed to check if {} is a directory", path.display())),
}
}
async fn canonicalize(&self, path: &AbsolutePath) -> anyhow::Result<PathBuf> {
tokio::fs::canonicalize(path.as_path())
.await
.with_context(|| format!("Failed to canonicalize {}", path.display()))
}
async fn glob(&self, pattern: &str) -> anyhow::Result<Vec<PathBuf>> {
let pattern = pattern.to_string();
tokio::task::spawn_blocking(move || {
glob::glob(&pattern)
.with_context(|| format!("Invalid glob pattern: {pattern}"))?
.collect::<Result<Vec<_>, _>>()
.context("Failed to read glob entry")
})
.await
.context("spawn_blocking panicked")?
}
async fn file_size(&self, path: &AbsolutePath) -> anyhow::Result<u64> {
tokio::fs::metadata(path.as_path())
.await
.map(|m| m.len())
.with_context(|| format!("Failed to stat {}", path.display()))
}
}