use std::{ collections::{HashMap, HashSet}, io, path::{Path, PathBuf}, }; use memofs::Vfs; /// A simple representation of a subsection of a file system. #[derive(Default)] pub struct FsSnapshot { /// Paths representing new files mapped to their contents. added_files: HashMap>, /// Paths representing new directories. added_dirs: HashSet, /// Paths representing removed files. removed_files: HashSet, /// Paths representing removed directories. removed_dirs: HashSet, } impl FsSnapshot { /// Creates a new `FsSnapshot`. pub fn new() -> Self { Self { added_files: HashMap::new(), added_dirs: HashSet::new(), removed_files: HashSet::new(), removed_dirs: HashSet::new(), } } /// Adds the given path to the `FsSnapshot` as a file with the given /// contents, then returns it. pub fn with_added_file>(mut self, path: P, data: Vec) -> Self { self.added_files.insert(path.as_ref().to_path_buf(), data); self } /// Adds the given path to the `FsSnapshot` as a file with the given /// then returns it. pub fn with_added_dir>(mut self, path: P) -> Self { self.added_dirs.insert(path.as_ref().to_path_buf()); self } /// Merges two `FsSnapshot`s together. #[inline] pub fn merge(&mut self, other: Self) { self.added_files.extend(other.added_files); self.added_dirs.extend(other.added_dirs); self.removed_files.extend(other.removed_files); self.removed_dirs.extend(other.removed_dirs); } /// Merges two `FsSnapshot`s together, with a filter applied to the paths. #[inline] pub fn merge_with_filter(&mut self, other: Self, mut predicate: F) where F: FnMut(&Path) -> bool, { self.added_files .extend(other.added_files.into_iter().filter(|(k, _)| predicate(k))); self.added_dirs .extend(other.added_dirs.into_iter().filter(|p| predicate(p))); self.removed_files .extend(other.removed_files.into_iter().filter(|p| predicate(p))); self.removed_dirs .extend(other.removed_dirs.into_iter().filter(|p| predicate(p))); } /// Adds the provided path as a file with the given contents. pub fn add_file>(&mut self, path: P, data: Vec) { self.added_files.insert(path.as_ref().to_path_buf(), data); } /// Adds the provided path as a directory. pub fn add_dir>(&mut self, path: P) { self.added_dirs.insert(path.as_ref().to_path_buf()); } /// Removes the provided path, as a file. pub fn remove_file>(&mut self, path: P) { self.removed_files.insert(path.as_ref().to_path_buf()); } /// Removes the provided path, as a directory. pub fn remove_dir>(&mut self, path: P) { self.removed_dirs.insert(path.as_ref().to_path_buf()); } /// Writes the `FsSnapshot` to the provided VFS, using the provided `base` /// as a root for the other paths in the `FsSnapshot`. /// /// This includes removals, but makes no effort to minimize work done. pub fn write_to_vfs>(&self, base: P, vfs: &Vfs) -> io::Result<()> { let mut lock = vfs.lock(); let base_path = base.as_ref(); for dir_path in &self.added_dirs { match lock.create_dir_all(base_path.join(dir_path)) { Ok(_) => (), Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (), Err(err) => return Err(err), }; } for (path, contents) in &self.added_files { lock.write(base_path.join(path), contents)?; } for dir_path in &self.removed_dirs { let full_path = base_path.join(dir_path); match lock.remove_dir_all(&full_path) { Ok(()) => (), // Directory might have already been removed (e.g., added twice via different code paths) Err(err) if err.kind() == io::ErrorKind::NotFound => { log::debug!( "Directory already removed or doesn't exist: {}", full_path.display() ); } Err(err) => return Err(err), } } // Only remove files that aren't already inside a directory we're removing. // remove_dir_all already deleted those files recursively. // Also handle the case where the same file might be listed twice (absolute vs relative path) // by gracefully handling "file not found" errors. for path in &self.removed_files { let is_inside_removed_dir = self .removed_dirs .iter() .any(|dir| path.starts_with(dir)); if is_inside_removed_dir { continue; } let full_path = base_path.join(path); match lock.remove_file(&full_path) { Ok(()) => (), // File might have already been removed (e.g., by remove_dir_all, or listed twice // with different path formats like absolute vs relative) Err(err) if err.kind() == io::ErrorKind::NotFound => { log::debug!( "File already removed or doesn't exist: {}", full_path.display() ); } Err(err) => return Err(err), } } drop(lock); log::debug!( "Wrote {} directories and {} files to the file system", self.added_dirs.len(), self.added_files.len() ); // Count how many files were skipped because they're inside removed directories let files_inside_dirs = self .removed_files .iter() .filter(|path| { self.removed_dirs .iter() .any(|dir| path.starts_with(dir)) }) .count(); log::debug!( "Removed {} directories and {} files from the file system ({} files skipped, inside removed dirs)", self.removed_dirs.len(), self.removed_files.len() - files_inside_dirs, files_inside_dirs ); Ok(()) } /// Returns whether this `FsSnapshot` is empty or not. #[inline] pub fn is_empty(&self) -> bool { self.added_files.is_empty() && self.added_dirs.is_empty() && self.removed_files.is_empty() && self.removed_dirs.is_empty() } /// Returns a list of paths that would be added by this `FsSnapshot`. #[inline] pub fn added_paths(&self) -> Vec<&Path> { let mut list = Vec::with_capacity(self.added_files.len() + self.added_dirs.len()); list.extend(self.added_files()); list.extend(self.added_dirs()); list } /// Returns a list of paths that would be removed by this `FsSnapshot`. #[inline] pub fn removed_paths(&self) -> Vec<&Path> { let mut list = Vec::with_capacity(self.removed_files.len() + self.removed_dirs.len()); list.extend(self.removed_files()); list.extend(self.removed_dirs()); list } /// Returns a list of file paths that would be added by this `FsSnapshot` #[inline] pub fn added_files(&self) -> Vec<&Path> { let mut added_files: Vec<_> = self.added_files.keys().map(PathBuf::as_path).collect(); added_files.sort_unstable(); added_files } /// Returns a list of directory paths that would be added by this `FsSnapshot` #[inline] pub fn added_dirs(&self) -> Vec<&Path> { let mut added_dirs: Vec<_> = self.added_dirs.iter().map(PathBuf::as_path).collect(); added_dirs.sort_unstable(); added_dirs } /// Returns a list of file paths that would be removed by this `FsSnapshot` #[inline] pub fn removed_files(&self) -> Vec<&Path> { let mut removed_files: Vec<_> = self.removed_files.iter().map(PathBuf::as_path).collect(); removed_files.sort_unstable(); removed_files } /// Returns a list of directory paths that would be removed by this `FsSnapshot` #[inline] pub fn removed_dirs(&self) -> Vec<&Path> { let mut removed_dirs: Vec<_> = self.removed_dirs.iter().map(PathBuf::as_path).collect(); removed_dirs.sort_unstable(); removed_dirs } }