diff --git a/src/filesystem.rs b/src/filesystem.rs index 0d986b8..73d1742 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -1,7 +1,7 @@ //! The filesystem trait definitions needed to implement new virtual filesystems use crate::error::VfsErrorKind; -use crate::{SeekAndRead, VfsMetadata, VfsPath, VfsResult}; +use crate::{SeekAndRead, SeekAndReadAndWrite, VfsMetadata, VfsPath, VfsResult}; use std::fmt::Debug; use std::io::Write; @@ -46,6 +46,28 @@ pub trait FileSystem: Debug + Sync + Send + 'static { fn move_dir(&self, _src: &str, _dest: &str) -> VfsResult<()> { Err(VfsErrorKind::NotSupported.into()) } + + /// This obtains the potential size of the current path in the filesystem. + /// + /// This, by default, queries the current size. + fn size_hint(&self, path: &str) -> VfsResult { + self.metadata(path).map(|f| f.len) + } + + /// Informs the filesystem to 'flush' its potentially cached information. + fn sync(&self, path: &str) -> VfsResult<()>; + + /// Set a size hint for the associated path. + /// + /// This is, by default, a no-op. + fn set_size_hint(&self, _hint: usize, _path: &str) -> VfsResult<()> { + Ok(()) + } + + /// Opens the file at this path for reading and writing + fn update_file(&self, _path: &str) -> VfsResult> { + Err(VfsErrorKind::NotSupported.into()) + } } impl From for VfsPath { diff --git a/src/impls/altroot.rs b/src/impls/altroot.rs index a966272..b036f34 100644 --- a/src/impls/altroot.rs +++ b/src/impls/altroot.rs @@ -82,6 +82,14 @@ impl FileSystem for AltrootFS { } self.path(src)?.copy_file(&self.path(dest)?) } + + fn update_file(&self, path: &str) -> VfsResult> { + self.path(path)?.update_file() + } + + fn sync(&self, path: &str) -> VfsResult<()> { + self.path(path)?.sync() + } } #[cfg(test)] diff --git a/src/impls/embedded.rs b/src/impls/embedded.rs index fadedd4..7c23c4b 100644 --- a/src/impls/embedded.rs +++ b/src/impls/embedded.rs @@ -7,7 +7,7 @@ use std::marker::PhantomData; use rust_embed::RustEmbed; use crate::error::VfsErrorKind; -use crate::{FileSystem, SeekAndRead, VfsFileType, VfsMetadata, VfsResult}; +use crate::{FileSystem, SeekAndRead, VfsFileType, VfsMetadata, VfsResult, VfsAccess}; type EmbeddedPath = Cow<'static, str>; @@ -120,12 +120,14 @@ where return Ok(VfsMetadata { file_type: VfsFileType::File, len: *len, + access: HashSet::from([VfsAccess::Read]) }); } if self.directory_map.contains_key(normalized_path) { return Ok(VfsMetadata { file_type: VfsFileType::Directory, len: 0, + access: HashSet::from([VfsAccess::Read]) }); } Err(VfsErrorKind::FileNotFound.into()) @@ -153,6 +155,9 @@ where fn remove_dir(&self, _path: &str) -> VfsResult<()> { Err(VfsErrorKind::NotSupported.into()) } + fn sync(&self, _path: &str) -> VfsResult<()> { + Ok(()) + } } fn normalize_path(path: &str) -> VfsResult<&str> { diff --git a/src/impls/memory.rs b/src/impls/memory.rs index 5e8481e..4bb02a2 100644 --- a/src/impls/memory.rs +++ b/src/impls/memory.rs @@ -1,17 +1,16 @@ //! An ephemeral in-memory file system, intended mainly for unit tests use crate::error::VfsErrorKind; -use crate::VfsResult; -use crate::{FileSystem, VfsFileType}; -use crate::{SeekAndRead, VfsMetadata}; +use crate::{VfsResult, FileSystem, SeekAndRead, VfsFileType, VfsMetadata, VfsAccess}; use core::cmp; -use std::collections::HashMap; +use std::collections::{HashSet, HashMap}; use std::fmt; use std::fmt::{Debug, Formatter}; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::mem::swap; use std::sync::{Arc, RwLock}; + type MemoryFsHandle = Arc>; /// An ephemeral in-memory file system, intended mainly for unit tests @@ -119,6 +118,56 @@ impl Seek for ReadableFile { } } +struct RandomAccessFile { + content: Cursor>, + destination: String, + fs: MemoryFsHandle, +} + +impl RandomAccessFile { + fn from_file(value: Arc>, destination: String, fs: MemoryFsHandle) -> Self { + Self { + content: Cursor::new(value.to_vec()), + destination, + fs, + } + } +} + +impl Write for RandomAccessFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.content.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.content.flush() + } +} + +impl Read for RandomAccessFile { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.content.read(buf) + } +} +impl Seek for RandomAccessFile { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + self.content.seek(pos) + } +} +impl Drop for RandomAccessFile { + fn drop(&mut self) { + let mut content = vec![]; + swap(&mut content, self.content.get_mut()); + self.fs.write().unwrap().files.insert( + self.destination.clone(), + MemoryFile { + file_type: VfsFileType::File, + content: Arc::new(content), + }, + ); + } +} + impl FileSystem for MemoryFS { fn read_dir(&self, path: &str) -> VfsResult>> { let prefix = format!("{}/", path); @@ -195,18 +244,35 @@ impl FileSystem for MemoryFS { let writer = WritableFile { content, destination: path.to_string(), - fs: self.handle.clone(), + fs: Arc::clone(&self.handle), }; Ok(Box::new(writer)) } + fn update_file(&self, path: &str) -> VfsResult> { + let handle = self.handle.read().unwrap(); + let file = handle + .files + .get(path) + .ok_or_else(|| VfsErrorKind::FileNotFound)?; + ensure_file(file)?; + + Ok(Box::new(RandomAccessFile::from_file( + Arc::clone(&file.content), + path.to_string(), + Arc::clone(&self.handle), + ))) + } + fn metadata(&self, path: &str) -> VfsResult { let guard = self.handle.read().unwrap(); let files = &guard.files; let file = files.get(path).ok_or(VfsErrorKind::FileNotFound)?; + Ok(VfsMetadata { file_type: file.file_type, len: file.content.len() as u64, + access: HashSet::from([VfsAccess::Read, VfsAccess::Write]), }) } @@ -234,6 +300,10 @@ impl FileSystem for MemoryFS { .ok_or(VfsErrorKind::FileNotFound)?; Ok(()) } + + fn sync(&self, _path: &str) -> VfsResult<()> { + Ok(()) + } } struct MemoryFsImpl { diff --git a/src/impls/overlay.rs b/src/impls/overlay.rs index a758796..353f3aa 100644 --- a/src/impls/overlay.rs +++ b/src/impls/overlay.rs @@ -31,6 +31,10 @@ impl OverlayFS { &self.layers[0] } + fn sync_path(&self,path:&str) -> VfsResult<()>{ + self.read_path(path).and_then(|p| p.sync()) + } + fn read_path(&self, path: &str) -> VfsResult { if path.is_empty() { return Ok(self.layers[0].clone()); @@ -121,6 +125,10 @@ impl FileSystem for OverlayFS { self.read_path(path)?.open_file() } + fn update_file(&self, path: &str) -> VfsResult> { + self.read_path(path)?.update_file() + } + fn create_file(&self, path: &str) -> VfsResult> { self.ensure_has_parent(path)?; let result = self.write_path(path)?.create_file()?; @@ -182,6 +190,10 @@ impl FileSystem for OverlayFS { whiteout_path.create_file()?; Ok(()) } + + fn sync(&self, path: &str) -> VfsResult<()> { + self.sync_path(path) + } } #[cfg(test)] diff --git a/src/impls/physical.rs b/src/impls/physical.rs index 3d8d2f5..ea130e2 100644 --- a/src/impls/physical.rs +++ b/src/impls/physical.rs @@ -1,13 +1,13 @@ //! A "physical" file system implementation using the underlying OS file system use crate::error::VfsErrorKind; -use crate::VfsResult; -use crate::{FileSystem, VfsMetadata}; -use crate::{SeekAndRead, VfsFileType}; +use crate::{SeekAndRead, VfsFileType, VfsResult, VfsAccess, FileSystem, VfsMetadata}; +use std::collections::HashSet; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::{Path, PathBuf}; + /// A physical filesystem implementation using the underlying OS file system #[derive(Debug)] pub struct PhysicalFS { @@ -63,16 +63,26 @@ impl FileSystem for PhysicalFS { } fn metadata(&self, path: &str) -> VfsResult { - let metadata = self.get_path(path).metadata()?; + let pb = self.get_path(path); + let metadata = pb.metadata()?; + let mut access = HashSet::new(); + access.insert(VfsAccess::Read); + + if !metadata.permissions().readonly() { + access.insert(VfsAccess::Write); + } + Ok(if metadata.is_dir() { VfsMetadata { file_type: VfsFileType::Directory, len: 0, + access, } } else { VfsMetadata { file_type: VfsFileType::File, len: metadata.len(), + access, } }) } @@ -110,6 +120,19 @@ impl FileSystem for PhysicalFS { } Ok(()) } + + fn update_file(&self, path: &str) -> VfsResult> { + Ok(Box::new( + OpenOptions::new() + .write(true) + .read(true) + .open(self.get_path(path))?, + )) + } + + fn sync(&self, _path: &str) -> VfsResult<()> { + Ok(()) + } } #[cfg(test)] diff --git a/src/path.rs b/src/path.rs index a1ac33b..f77625d 100644 --- a/src/path.rs +++ b/src/path.rs @@ -3,6 +3,7 @@ //! The virtual file system abstraction generalizes over file systems and allow using //! different VirtualFileSystem implementations (i.e. an in memory implementation for unit tests) +use std::collections::HashSet; use std::io::{Read, Seek, Write}; use std::sync::Arc; @@ -14,6 +15,11 @@ pub trait SeekAndRead: Seek + Read {} impl SeekAndRead for T where T: Seek + Read {} +/// Trait combining Seek, Read and Write, return value for opening files with random access. +pub trait SeekAndReadAndWrite: Seek + Read + Write {} + +impl SeekAndReadAndWrite for T where T: Seek + Read + Write {} + /// Type of file #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum VfsFileType { @@ -23,6 +29,15 @@ pub enum VfsFileType { Directory, } +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum VfsAccess { + /// The resource at this path can be read from. + Read, + + /// The resource at this path can be written to. + Write, +} + /// File metadata information #[derive(Debug)] pub struct VfsMetadata { @@ -30,6 +45,8 @@ pub struct VfsMetadata { pub file_type: VfsFileType, /// Length of the file in bytes, 0 for directories pub len: u64, + /// Access levels available to this path. + pub access: HashSet, } #[derive(Debug)] @@ -296,6 +313,37 @@ impl VfsPath { }) } + /// Opens the file at this path for reading and writing + /// + /// ``` + /// # use std::io::{SeekFrom, Read}; + /// use vfs::{MemoryFS, VfsError, VfsPath}; + /// let path = VfsPath::new(MemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// write!(file.create_file()?, "Hello, world!")?; + /// let mut result = String::new(); + /// + /// let mut handle = file.update_file()?; + /// handle.read_to_string(&mut result)?; + /// + /// assert_eq!(&result, "Hello, world!"); + /// + /// handle.seek(SeekFrom::Start(0))?; + /// handle.write(b"Goodnight, world.")?; + /// handle.seek(SeekFrom::Start(0))?; + /// + /// let mut result2 = String::new(); + /// handle.read_to_string(&mut result2)?; + /// + /// assert_eq!(&result2, "Goodnight, world."); + /// # Ok::<(), VfsError>(()) + /// ``` + pub fn update_file(&self) -> VfsResult> { + self.fs + .fs + .update_file(&self.path) + } + /// Checks whether parent is a directory fn get_parent(&self, action: &str) -> VfsResult<()> { let parent = self.parent(); @@ -584,7 +632,7 @@ impl VfsPath { /// Directories are visited before their children /// /// Note that the iterator items can contain errors, usually when directories are removed during the iteration. - /// The returned paths may also point to non-existant files if there is concurrent removal. + /// The returned paths may also point to non-existent files if there is concurrent removal. /// /// Also note that loops in the file system hierarchy may cause this iterator to never terminate. /// @@ -880,6 +928,14 @@ impl VfsPath { })?; Ok(()) } + + pub fn sync(&self) -> VfsResult<()> { + self.fs.fs.sync(&self.path) + } + + pub fn set_size_hint(&mut self, size_hint: usize) -> VfsResult<()> { + self.fs.fs.set_size_hint(size_hint, &self.path) + } } /// An iterator for recursively walking a file hierarchy