feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
102
crates/infrastructure/src/fs/mod.rs
Normal file
102
crates/infrastructure/src/fs/mod.rs
Normal file
@ -0,0 +1,102 @@
|
||||
//! [`LocalFileSystem`] — minimal [`FileSystem`] adapter over [`tokio::fs`].
|
||||
//!
|
||||
//! Maps [`RemotePath`] (a location-neutral string path) onto the local OS
|
||||
//! filesystem. Only the operations needed to wire up the composition root and
|
||||
//! later lots are implemented; richer behaviour (and SSH/WSL siblings) arrives
|
||||
//! in L2/L9.
|
||||
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{DirEntry, FileSystem, FsError, RemotePath};
|
||||
use tokio::fs;
|
||||
|
||||
/// Filesystem adapter backed by the local OS via `tokio::fs`.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct LocalFileSystem;
|
||||
|
||||
impl LocalFileSystem {
|
||||
/// Creates a new [`LocalFileSystem`].
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a [`std::io::Error`] to the domain's [`FsError`], preserving the
|
||||
/// not-found / permission distinctions the application layer cares about.
|
||||
fn map_io(path: &RemotePath, err: &io::Error) -> FsError {
|
||||
match err.kind() {
|
||||
io::ErrorKind::NotFound => FsError::NotFound(path.as_str().to_owned()),
|
||||
io::ErrorKind::PermissionDenied => FsError::PermissionDenied(path.as_str().to_owned()),
|
||||
_ => FsError::Io(format!("{}: {err}", path.as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FileSystem for LocalFileSystem {
|
||||
async fn read(&self, path: &RemotePath) -> Result<Vec<u8>, FsError> {
|
||||
fs::read(path.as_str())
|
||||
.await
|
||||
.map_err(|e| map_io(path, &e))
|
||||
}
|
||||
|
||||
async fn write(&self, path: &RemotePath, data: &[u8]) -> Result<(), FsError> {
|
||||
fs::write(path.as_str(), data)
|
||||
.await
|
||||
.map_err(|e| map_io(path, &e))
|
||||
}
|
||||
|
||||
async fn exists(&self, path: &RemotePath) -> Result<bool, FsError> {
|
||||
match fs::metadata(path.as_str()).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
|
||||
Err(e) => Err(map_io(path, &e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_dir_all(&self, path: &RemotePath) -> Result<(), FsError> {
|
||||
fs::create_dir_all(path.as_str())
|
||||
.await
|
||||
.map_err(|e| map_io(path, &e))
|
||||
}
|
||||
|
||||
async fn list(&self, path: &RemotePath) -> Result<Vec<DirEntry>, FsError> {
|
||||
let mut entries = Vec::new();
|
||||
let mut read_dir = fs::read_dir(path.as_str())
|
||||
.await
|
||||
.map_err(|e| map_io(path, &e))?;
|
||||
|
||||
while let Some(entry) = read_dir.next_entry().await.map_err(|e| map_io(path, &e))? {
|
||||
let is_dir = entry
|
||||
.file_type()
|
||||
.await
|
||||
.map(|t| t.is_dir())
|
||||
.unwrap_or(false);
|
||||
entries.push(DirEntry {
|
||||
name: entry.file_name().to_string_lossy().into_owned(),
|
||||
is_dir,
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
async fn symlink(&self, src: &RemotePath, dst: &RemotePath) -> Result<(), FsError> {
|
||||
symlink_impl(Path::new(src.as_str()), Path::new(dst.as_str()))
|
||||
.await
|
||||
.map_err(|e| map_io(dst, &e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn symlink_impl(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
fs::symlink(src, dst).await
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn symlink_impl(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
// On Windows we default to a file symlink; directory symlinks require a
|
||||
// different call and are handled when the store layer (L2/L6) needs them.
|
||||
fs::symlink_file(src, dst).await
|
||||
}
|
||||
Reference in New Issue
Block a user