Files
IdeaSDK/crates/infrastructure/src/fs/mod.rs
Blomios 27597eb64e feat(permissions): voie projection CLI (LP0→LP3) + checkpoint Codex/input
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>
2026-06-15 20:39:18 +02:00

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
}