Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow vfs implementations to specify m/b/a-times in file metadata #63

Merged
merged 14 commits into from
Mar 9, 2024
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ async-trait = { version = "0.1.73", optional = true}
tokio = { version = "1.29.0", features = ["macros", "rt"], optional = true}
futures = {version = "0.3.28", optional = true}
async-recursion = {version = "1.0.5", optional = true}
filetime = "0.2.23"
camino = { version = "1.0.5", optional = true }

[dev-dependencies]
uuid = { version = "=0.8.1", features = ["v4"] }
Expand All @@ -29,7 +31,7 @@ tokio-test = "0.4.3"
[features]
embedded-fs = ["rust-embed"]
async-vfs = ["tokio", "async-std", "async-trait", "futures", "async-recursion"]
export-test-macros = []
export-test-macros = [ "camino" ]

[package.metadata.docs.rs]
features = ["embedded-fs", "async-vfs"]
15 changes: 14 additions & 1 deletion src/async_vfs/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

use crate::async_vfs::{AsyncVfsPath, SeekAndRead};
use crate::error::VfsErrorKind;
use crate::{VfsMetadata, VfsResult};
use crate::{VfsError, VfsMetadata, VfsResult};

use async_std::io::Write;
use async_std::stream::Stream;
use async_trait::async_trait;
use std::fmt::Debug;
use std::time::SystemTime;

/// File system implementations must implement this trait
/// All path parameters are absolute, starting with '/', except for the root directory
Expand Down Expand Up @@ -36,6 +37,18 @@ pub trait AsyncFileSystem: Debug + Sync + Send + 'static {
async fn append_file(&self, path: &str) -> VfsResult<Box<dyn Write + Send + Unpin>>;
/// Returns the file metadata for the file at this path
async fn metadata(&self, path: &str) -> VfsResult<VfsMetadata>;
/// Sets the files creation timestamp, if the implementation supports it
async fn set_creation_time(&self, _path: &str, _time: SystemTime) -> VfsResult<()> {
Err(VfsError::from(VfsErrorKind::NotSupported))
}
/// Sets the files modification timestamp, if the implementation supports it
async fn set_modification_time(&self, _path: &str, _time: SystemTime) -> VfsResult<()> {
Err(VfsError::from(VfsErrorKind::NotSupported))
}
/// Sets the files access timestamp, if the implementation supports it
async fn set_access_time(&self, _path: &str, _time: SystemTime) -> VfsResult<()> {
Err(VfsError::from(VfsErrorKind::NotSupported))
}
/// Returns true if a file or directory at path exists, false otherwise
async fn exists(&self, path: &str) -> VfsResult<bool>;
/// Removes the file at this path
Expand Down
13 changes: 13 additions & 0 deletions src/async_vfs/impls/altroot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::async_vfs::{AsyncFileSystem, AsyncVfsPath, SeekAndRead};
use crate::{error::VfsErrorKind, VfsMetadata, VfsResult};
use std::time::SystemTime;

use async_std::io::Write;
use async_trait::async_trait;
Expand Down Expand Up @@ -71,6 +72,18 @@ impl AsyncFileSystem for AsyncAltrootFS {
self.path(path)?.metadata().await
}

async fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.path(path)?.set_creation_time(time).await
}

async fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.path(path)?.set_modification_time(time).await
}

async fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.path(path)?.set_access_time(time).await
}

async fn exists(&self, path: &str) -> VfsResult<bool> {
match self.path(path) {
Ok(p) => p.exists().await,
Expand Down
3 changes: 3 additions & 0 deletions src/async_vfs/impls/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ impl AsyncFileSystem for AsyncMemoryFS {
Ok(VfsMetadata {
file_type: file.file_type,
len: file.content.len() as u64,
modified: None,
created: None,
accessed: None,
})
}

Expand Down
13 changes: 13 additions & 0 deletions src/async_vfs/impls/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use async_std::io::Write;
use async_trait::async_trait;
use futures::stream::{Stream, StreamExt};
use std::collections::HashSet;
use std::time::SystemTime;

/// An overlay file system combining several filesystems into one, an upper layer with read/write access and lower layers with only read access
///
Expand Down Expand Up @@ -154,6 +155,18 @@ impl AsyncFileSystem for AsyncOverlayFS {
self.read_path(path).await?.metadata().await
}

async fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.write_path(path)?.set_creation_time(time).await
}

async fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.write_path(path)?.set_modification_time(time).await
}

async fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
self.write_path(path)?.set_access_time(time).await
}

async fn exists(&self, path: &str) -> VfsResult<bool> {
if self
.whiteout_path(path)
Expand Down
50 changes: 50 additions & 0 deletions src/async_vfs/impls/physical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ use async_std::fs::{File, OpenOptions};
use async_std::io::{ErrorKind, Write};
use async_std::path::{Path, PathBuf};
use async_trait::async_trait;
use filetime::FileTime;
use futures::stream::{Stream, StreamExt};
use std::pin::Pin;
use std::time::SystemTime;
use tokio::runtime::Handle;

/// A physical filesystem implementation using the underlying OS file system
#[derive(Debug)]
Expand All @@ -33,6 +36,31 @@ impl AsyncPhysicalFS {
}
}

/// Runs normal blocking io on a tokio thread.
/// Requires a tokio runtime.
async fn blocking_io<F>(f: F) -> Result<(), VfsError>
where
F: FnOnce() -> std::io::Result<()> + Send + 'static,
{
if Handle::try_current().is_ok() {
let result = tokio::task::spawn_blocking(f).await;

match result {
Ok(val) => val,
Err(err) => {
return Err(VfsError::from(VfsErrorKind::Other(format!(
"Tokio Concurrency Error: {}",
err
))));
}
}?;

Ok(())
} else {
Err(VfsError::from(VfsErrorKind::NotSupported))
}
}

#[async_trait]
impl AsyncFileSystem for AsyncPhysicalFS {
async fn read_dir(
Expand Down Expand Up @@ -89,15 +117,37 @@ impl AsyncFileSystem for AsyncPhysicalFS {
VfsMetadata {
file_type: VfsFileType::Directory,
len: 0,
modified: metadata.modified().ok(),
created: metadata.created().ok(),
accessed: metadata.accessed().ok(),
}
} else {
VfsMetadata {
file_type: VfsFileType::File,
len: metadata.len(),
modified: metadata.modified().ok(),
created: metadata.created().ok(),
accessed: metadata.accessed().ok(),
}
})
}

async fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
let path = self.get_path(path);

blocking_io(move || filetime::set_file_mtime(path, FileTime::from(time))).await?;

Ok(())
}

async fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> {
let path = self.get_path(path);

blocking_io(move || filetime::set_file_atime(path, FileTime::from(time))).await?;

Ok(())
}

async fn exists(&self, path: &str) -> VfsResult<bool> {
Ok(self.get_path(path).exists().await)
}
Expand Down
91 changes: 91 additions & 0 deletions src/async_vfs/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use async_std::sync::Arc;
use async_std::task::{Context, Poll};
use futures::{future::BoxFuture, FutureExt, Stream, StreamExt};
use std::pin::Pin;
use std::time::SystemTime;

/// Trait combining Seek and Read, return value for opening files
pub trait SeekAndRead: Seek + Read {}
Expand Down Expand Up @@ -467,6 +468,96 @@ impl AsyncVfsPath {
})
}

/// Sets the files creation timestamp at this path
///
/// ```
/// use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath};
/// use vfs::{VfsError, VfsFileType, VfsMetadata, VfsPath};
/// use async_std::io::WriteExt;
/// # tokio_test::block_on(async {
/// let path = AsyncVfsPath::new(AsyncMemoryFS::new());
/// let file = path.join("foo.txt")?;
/// file.create_file();
///
/// let time = std::time::SystemTime::now();
/// file.set_creation_time(time).await?;
///
/// assert_eq!(file.metadata().await?.len, 0);
/// assert_eq!(file.metadata().await?.created, Some(time));
///
/// # Ok::<(), VfsError>(())
/// # });
pub async fn set_creation_time(&self, time: SystemTime) -> VfsResult<()> {
self.fs
.fs
.set_creation_time(&self.path, time)
.await
.map_err(|err| {
err.with_path(&*self.path)
.with_context(|| "Could not set creation timestamp.")
})
}

/// Sets the files modification timestamp at this path
///
/// ```
/// use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath};
/// use vfs::{VfsError, VfsFileType, VfsMetadata, VfsPath};
/// use async_std::io::WriteExt;
/// # tokio_test::block_on(async {
/// let path = AsyncVfsPath::new(AsyncMemoryFS::new());
/// let file = path.join("foo.txt")?;
/// file.create_file();
///
/// let time = std::time::SystemTime::now();
/// file.set_modification_time(time).await?;
///
/// assert_eq!(file.metadata().await?.len, 0);
/// assert_eq!(file.metadata().await?.modified, Some(time));
///
/// # Ok::<(), VfsError>(())
/// # });
pub async fn set_modification_time(&self, time: SystemTime) -> VfsResult<()> {
self.fs
.fs
.set_modification_time(&self.path, time)
.await
.map_err(|err| {
err.with_path(&*self.path)
.with_context(|| "Could not set modification timestamp.")
})
}

/// Sets the files access timestamp at this path
///
/// ```
/// use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath};
/// use vfs::{VfsError, VfsFileType, VfsMetadata, VfsPath};
/// use async_std::io::WriteExt;
/// # tokio_test::block_on(async {
/// let path = AsyncVfsPath::new(AsyncMemoryFS::new());
/// let file = path.join("foo.txt")?;
/// file.create_file();
///
/// let time = std::time::SystemTime::now();
/// file.set_access_time(time).await?;
///
/// assert_eq!(file.metadata().await?.len, 0);
/// assert_eq!(file.metadata().await?.accessed, Some(time));
///
/// # Ok::<(), VfsError>(())
/// # });
pub async fn set_access_time(&self, time: SystemTime) -> VfsResult<()> {
self.fs
.fs
.set_access_time(&self.path, time)
.await
.map_err(|err| {
err.with_path(&*self.path)
.with_context(|| "Could not set access timestamp.")
})
}

/// Returns `true` if the path exists and is pointing at a regular file, otherwise returns `false`.
///
/// Note that this call may fail if the file's existence cannot be determined or the metadata can not be retrieved
Expand Down
74 changes: 74 additions & 0 deletions src/async_vfs/test_macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ macro_rules! test_async_vfs {
use $crate::VfsFileType;
use $crate::async_vfs::AsyncVfsPath;
use $crate::VfsResult;
use $crate::error::VfsErrorKind;
use futures::stream::StreamExt;
use async_std::io::{WriteExt, ReadExt};
use std::time::SystemTime;

fn create_root() -> AsyncVfsPath {
$root.into()
Expand All @@ -21,6 +23,78 @@ macro_rules! test_async_vfs {
create_root();
}

#[tokio::test]
async fn set_and_query_creation_timestamp() -> VfsResult<()> {
let root = create_root();
let path = root.join("foobar.txt").unwrap();
drop( path.create_file().await.unwrap() );

let time = SystemTime::now();
let result = path.set_creation_time(time).await;

match result {
Err(err) => {
if let VfsErrorKind::NotSupported = err.kind() {
println!("Skipping creation time test: set_creation_time unsupported!");
} else {
return Err(err);
}
},
_ => {
assert_eq!(path.metadata().await?.created, Some(time));
}
}
Ok(())
}

#[tokio::test]
async fn set_and_query_modification_timestamp() -> VfsResult<()> {
let root = create_root();
let path = root.join("foobar.txt").unwrap();
drop( path.create_file().await.unwrap() );

let time = SystemTime::now();
let result = path.set_modification_time(time).await;

match result {
Err(err) => {
if let VfsErrorKind::NotSupported = err.kind() {
println!("Skipping creation time test: set_modification_time unsupported!");
} else {
return Err(err);
}
},
_ => {
assert_eq!(path.metadata().await?.modified, Some(time));
}
}
Ok(())
}

#[tokio::test]
async fn set_and_query_access_timestamp() -> VfsResult<()> {
let root = create_root();
let path = root.join("foobar.txt").unwrap();
drop( path.create_file().await.unwrap() );

let time = SystemTime::now();
let result = path.set_access_time(time).await;

match result {
Err(err) => {
if let VfsErrorKind::NotSupported = err.kind() {
println!("Skipping access time test: set_access_time unsupported!");
} else {
return Err(err);
}
},
_ => {
assert_eq!(path.metadata().await?.accessed, Some(time));
}
}
Ok(())
}

#[tokio::test]
async fn write_and_read_file() -> VfsResult<()>{
let root = create_root();
Expand Down
Loading
Loading