Skip to content

Commit

Permalink
feat: add virtualfs crate with filesystem abstraction (#136)
Browse files Browse the repository at this point in the history
* feat: add virtualfs crate with filesystem abstraction

Add a new virtualfs crate that provides a generic virtual filesystem interface
and an in-memory implementation. This abstraction will allow for different
filesystem implementations while maintaining a consistent interface.

Key changes:
- Add virtualfs crate with VirtualFileSystem trait
- Implement MemoryFileSystem as reference implementation
- Add comprehensive test coverage for MemoryFileSystem
- Fix error handling in monofs FsError::Custom
- Add cfg-if as workspace dependency

The virtualfs crate provides:
- Async filesystem operations (read, write, create, delete, etc)
- Path-based access with proper validation
- Support for files, directories and symlinks
- Metadata handling
- Comprehensive error types

* refactor: format code and improve documentation examples

- Add working examples for Dir::find() and Dir::find_mut()
- Add examples for Metadata methods
- Remove redundant VirtualFileSystem trait documentation
- Fix and expand documentation examples in virtualfs
- Reorganize imports to be alphabetically sorted
- Group related imports using nested paths
  • Loading branch information
appcypher authored Feb 14, 2025
1 parent 40c17d5 commit 9249440
Show file tree
Hide file tree
Showing 18 changed files with 3,586 additions and 31 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"ipldstore",
"cryptdag",
"did-wk",
"virtualfs",
]
resolver = "2"

Expand Down Expand Up @@ -66,3 +67,4 @@ serde_ipld_dagcbor = "0.6"
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls"] }
regex = "1.10"
async-recursion = "1.1"
cfg-if = "1.0"
2 changes: 1 addition & 1 deletion monocore/lib/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use ipldstore::{ipld, StoreError};
use monofs::FsError;
use monoutils::MonoutilsError;
use ipldstore::{ipld, StoreError};
use nix::errno::Errno;
use sqlx::migrate::MigrateError;
use std::{
Expand Down
17 changes: 10 additions & 7 deletions monocore/lib/management/rootfs.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use async_recursion::async_recursion;
use chrono::{TimeZone, Utc};
use ipldstore::ipld::ipld::Ipld;
use ipldstore::{ipld::cid::Cid, IpldStore};
use ipldstore::{
ipld::{cid::Cid, ipld::Ipld},
IpldStore,
};
use monofs::filesystem::{
Dir, Entity, File, Metadata, SymPathLink, UNIX_ATIME_KEY, UNIX_GID_KEY, UNIX_MODE_KEY,
UNIX_MTIME_KEY, UNIX_UID_KEY,
};
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path;
use tokio::fs::File as TokioFile;
use tokio::io::BufReader;
use std::{
fs,
os::unix::fs::{MetadataExt, PermissionsExt},
path::Path,
};
use tokio::{fs::File as TokioFile, io::BufReader};

use crate::MonocoreResult;

Expand Down
3 changes: 1 addition & 2 deletions monofs/examples/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
//! cargo run --example file
//! ```
use monofs::filesystem::File;
use ipldstore::{MemoryStore, Storable};
use monofs::filesystem::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};

//--------------------------------------------------------------------------------------------------
Expand All @@ -49,7 +49,6 @@ async fn main() -> anyhow::Result<()> {
drop(output_stream);
println!("Wrote content to file");


// Read content from the file
let input_stream = file.get_input_stream().await?;
let mut buffer = Vec::new();
Expand Down
2 changes: 1 addition & 1 deletion monofs/lib/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub enum FsError {
PathNotFound(String),

/// Custom error.
#[error("Custom error: {0}")]
#[error(transparent)]
Custom(#[from] AnyError),

// /// DID related error.
Expand Down
5 changes: 5 additions & 0 deletions monofs/lib/filesystem/dir/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ impl TryFrom<&str> for Utf8UnixPathSegment {
return Err(FsError::InvalidPathComponent(value.to_string()));
}

// Reject input string if it contains the '/' separator
if value.contains('/') {
return Err(FsError::InvalidPathComponent(value.to_string()));
}

let component = Utf8UnixComponent::try_from(value)
.map_err(|_| FsError::InvalidPathComponent(value.to_string()))?;

Expand Down
28 changes: 8 additions & 20 deletions monofs/lib/server/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use crate::store::FlatFsStore;

use super::MonofsNFS;

//--------------------------------------------------------------------------------------------------
// Types
//--------------------------------------------------------------------------------------------------

/// A server that provides NFS access to a content-addressed store.
/// This server uses a flat filesystem store as its backing store.
#[derive(Debug, Getters)]
Expand All @@ -21,6 +25,10 @@ pub struct MonofsServer {
port: u32,
}

//--------------------------------------------------------------------------------------------------
// Methods
//--------------------------------------------------------------------------------------------------

impl MonofsServer {
/// Creates a new MonofsServer with the given store path and host:port.
pub fn new(store_dir: impl Into<PathBuf>, host: impl Into<String>, port: u32) -> Self {
Expand All @@ -45,23 +53,3 @@ impl MonofsServer {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;

#[tokio::test]
async fn test_monofsserver_creation() {
let temp_dir = TempDir::new().unwrap();
let server = MonofsServer::new(
temp_dir.path().to_path_buf(),
"127.0.0.1",
0, // Use port 0 for testing
);

assert_eq!(server.store_dir, temp_dir.path());
assert_eq!(server.host, "127.0.0.1");
assert_eq!(server.port, 0);
}
}
23 changes: 23 additions & 0 deletions virtualfs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "virtualfs"
version = "0.1.0"
description = "`virtualfs` is a library for virtual file systems."
authors.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true

[lib]
name = "virtualfs"
path = "lib/lib.rs"

[dependencies]
async-trait.workspace = true
pretty-error-debug.workspace = true
thiserror.workspace = true
anyhow.workspace = true
tokio.workspace = true
futures.workspace = true
chrono.workspace = true
getset.workspace = true
cfg-if.workspace = true
138 changes: 138 additions & 0 deletions virtualfs/lib/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use std::{
error::Error,
fmt::{self, Display},
io,
path::PathBuf,
};

use thiserror::Error;

//--------------------------------------------------------------------------------------------------
// Types
//--------------------------------------------------------------------------------------------------

/// The result of a file system operation.
pub type VfsResult<T> = Result<T, VfsError>;

/// An error that occurred during a file system operation.
#[derive(pretty_error_debug::Debug, Error)]
pub enum VfsError {
/// The parent directory does not exist
#[error("parent directory does not exist: {0}")]
ParentDirectoryNotFound(PathBuf),

/// The path already exists
#[error("path already exists: {0}")]
AlreadyExists(PathBuf),

/// The path does not exist
#[error("path does not exist: {0}")]
NotFound(PathBuf),

/// The path is not a directory
#[error("path is not a directory: {0}")]
NotADirectory(PathBuf),

/// The path is not a file
#[error("path is not a file: {0}")]
NotAFile(PathBuf),

/// The path is not a symlink
#[error("path is not a symlink: {0}")]
NotASymlink(PathBuf),

/// Invalid offset for read/write operation
#[error("invalid offset {offset} for path: {path}")]
InvalidOffset {
/// The path of the file
path: PathBuf,

/// The offset that is invalid
offset: u64,
},

/// Insufficient permissions to perform the operation
#[error("insufficient permissions for operation on: {0}")]
PermissionDenied(PathBuf),

/// The filesystem is read-only
#[error("filesystem is read-only")]
ReadOnlyFilesystem,

/// Invalid symlink target
#[error("invalid symlink target: {0}")]
InvalidSymlinkTarget(PathBuf),

/// Empty path segment
#[error("empty path segment")]
EmptyPathSegment,

/// Invalid path component (e.g. ".", "..", "/")
#[error("invalid path component: {0}")]
InvalidPathComponent(String),

/// IO error during filesystem operation
#[error("io error: {0}")]
Io(#[from] io::Error),

/// Custom error.
#[error(transparent)]
Custom(#[from] AnyError),
}

/// An error that can represent any error.
#[derive(Debug)]
pub struct AnyError {
error: anyhow::Error,
}

//--------------------------------------------------------------------------------------------------
// Methods
//--------------------------------------------------------------------------------------------------

impl VfsError {
/// Creates a new `Err` result.
pub fn custom(error: impl Into<anyhow::Error>) -> VfsError {
VfsError::Custom(AnyError {
error: error.into(),
})
}
}

impl AnyError {
/// Downcasts the error to a `T`.
pub fn downcast<T>(&self) -> Option<&T>
where
T: Display + fmt::Debug + Send + Sync + 'static,
{
self.error.downcast_ref::<T>()
}
}

//--------------------------------------------------------------------------------------------------
// Functions
//--------------------------------------------------------------------------------------------------

/// Creates an `Ok` `VfsResult`.
#[allow(non_snake_case)]
pub fn Ok<T>(value: T) -> VfsResult<T> {
Result::Ok(value)
}

//--------------------------------------------------------------------------------------------------
// Trait Implementations
//--------------------------------------------------------------------------------------------------

impl PartialEq for AnyError {
fn eq(&self, other: &Self) -> bool {
self.error.to_string() == other.error.to_string()
}
}

impl Display for AnyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}

impl Error for AnyError {}
Loading

0 comments on commit 9249440

Please sign in to comment.