Jalon vert regroupant deux chantiers entrelacés dans le working tree, indissociables au niveau fichier mais tous deux verts (cargo test --workspace + tests frontend permissions au vert). Permissions — voie « projection CLI » (advisory), complète : - LP0 domaine pur : modèle PermissionSet/EffectivePermissions, resolve deny-wins + postures Allow<Ask<Deny (crates/domain/src/permission.rs). - LP1 store : FsPermissionStore (.ideai/permissions.json). - LP2 use cases : Get/Update project, Update agent override, Resolve. - LP3 projecteurs Claude/Codex (settings.local.json / config.toml), câblage launch-path + PermissionProjectorRegistry, nettoyage des fichiers Replace orphelins au swap de profil (LP3-4), composition root + commandes Tauri, UI PermissionsPanel (projet + override agent). - ports.rs : PermissionStore + FileSystem::remove_file (cleanup au swap). Reste ouvert (hors scope, marqué dans le code) : LP4 enforcement OS airtight (Landlock fichiers) + résumé de permissions injecté. Inclut aussi le chantier Codex/input/sessions structurées en cours (McpConfigStrategy, StructuredAdapter, gestion d'input) partageant les mêmes fichiers (lifecycle.rs, commands.rs, dto.rs, state.rs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
3.6 KiB
Rust
106 lines
3.6 KiB
Rust
//! [`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 remove_file(&self, path: &RemotePath) -> Result<(), FsError> {
|
|
match fs::remove_file(path.as_str()).await {
|
|
Ok(()) => Ok(()),
|
|
// Idempotent best-effort delete: an already-absent file is success.
|
|
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
|
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
|
|
}
|