P1: AsyncLocalFs implementation (Phase 2)
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- AsyncLocalFile: tokio::fs::File wrapper
- AsyncLocalFs: AsyncVfsBackend impl using tokio::fs
- Key methods: read_dir, open_file, stat, create_dir, remove_file, rename
- 4 async tests passing

Phase 2 complete: AsyncLocalFs working
Phase 3 pending: AsyncS3Vfs
Phase 4 pending: AsyncSmbVfs
Phase 5 pending: WebDAV integration

Tests: 293 passed, 0 failed
This commit is contained in:
Warren
2026-06-21 20:59:41 +08:00
parent 6242a5eaab
commit 790efe13f4
2 changed files with 252 additions and 1 deletions

View File

@@ -0,0 +1,249 @@
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use std::pin::Pin;
use std::future::Future;
use std::io::{self, SeekFrom};
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncSeekExt};
use super::{VfsError, VfsStat, VfsDirEntry, open_flags::OpenFlags};
/// Async VFS 文件實現(使用 tokio::fs
pub struct AsyncLocalFile {
file: fs::File,
path: PathBuf,
is_write: bool,
}
impl AsyncLocalFile {
pub fn new(file: fs::File, path: PathBuf, is_write: bool) -> Self {
Self { file, path, is_write }
}
}
impl super::AsyncVfsFile for AsyncLocalFile {
fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> Pin<Box<dyn Future<Output = Result<usize, VfsError>> + Send + 'a>> {
Box::pin(async move {
self.file.read(buf).await
.map_err(|e| VfsError::Io(e.to_string()))
})
}
fn write<'a>(&'a mut self, buf: &'a [u8]) -> Pin<Box<dyn Future<Output = Result<usize, VfsError>> + Send + 'a>> {
Box::pin(async move {
self.file.write(buf).await
.map_err(|e| VfsError::Io(e.to_string()))
})
}
fn seek<'a>(&'a mut self, pos: SeekFrom) -> Pin<Box<dyn Future<Output = Result<u64, VfsError>> + Send + 'a>> {
Box::pin(async move {
self.file.seek(pos).await
.map_err(|e| VfsError::Io(e.to_string()))
})
}
fn flush<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
Box::pin(async move {
self.file.flush().await
.map_err(|e| VfsError::Io(e.to_string()))
})
}
}
/// Async VFS 后端實現(使用 tokio::fs
pub struct AsyncLocalFs {
root: PathBuf,
}
impl AsyncLocalFs {
pub fn new() -> Self {
Self { root: PathBuf::new() }
}
pub fn with_root(root: PathBuf) -> Self {
Self { root }
}
fn map_io_error(path: &Path, e: io::Error) -> VfsError {
match e.kind() {
io::ErrorKind::NotFound => VfsError::NotFound(path.to_string_lossy().to_string()),
io::ErrorKind::PermissionDenied => VfsError::PermissionDenied(path.to_string_lossy().to_string()),
io::ErrorKind::AlreadyExists => VfsError::AlreadyExists(path.to_string_lossy().to_string()),
_ => VfsError::Io(e.to_string()),
}
}
fn stat_from_metadata(meta: &std::fs::Metadata) -> VfsStat {
VfsStat {
size: meta.len(),
mode: 0o644,
uid: 0,
gid: 0,
atime: meta.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
mtime: meta.modified().unwrap_or(SystemTime::UNIX_EPOCH),
is_dir: meta.is_dir(),
is_symlink: meta.file_type().is_symlink(),
}
}
}
impl Clone for AsyncLocalFs {
fn clone(&self) -> Self {
Self { root: self.root.clone() }
}
}
impl super::AsyncVfsBackend for AsyncLocalFs {
fn clone_boxed(&self) -> Box<dyn super::AsyncVfsBackend> {
Box::new(self.clone())
}
fn read_dir<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = Result<Vec<VfsDirEntry>, VfsError>> + Send + 'a>> {
Box::pin(async move {
let mut entries = Vec::new();
let mut dir = fs::read_dir(path).await
.map_err(|e| Self::map_io_error(path, e))?;
while let Some(entry) = dir.next_entry().await.map_err(|e| Self::map_io_error(path, e))? {
let name = entry.file_name().to_string_lossy().to_string();
let long_name = name.clone();
let meta = entry.metadata().await.map_err(|e| Self::map_io_error(path, e))?;
let stat = Self::stat_from_metadata(&meta);
entries.push(VfsDirEntry { name, long_name, stat });
}
Ok(entries)
})
}
fn open_file<'a>(&'a self, path: &'a Path, flags: &'a OpenFlags) -> Pin<Box<dyn Future<Output = Result<Box<dyn super::AsyncVfsFile>, VfsError>> + Send + 'a>> {
Box::pin(async move {
let mut options = fs::OpenOptions::new();
if flags.read {
options.read(true);
}
if flags.write {
options.write(true);
}
if flags.create {
options.create(true);
}
if flags.truncate {
options.truncate(true);
}
if flags.append {
options.append(true);
}
let file = options.open(path).await
.map_err(|e| Self::map_io_error(path, e))?;
Ok(Box::new(AsyncLocalFile::new(file, path.to_path_buf(), flags.write)) as Box<dyn super::AsyncVfsFile>)
})
}
fn stat<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = Result<VfsStat, VfsError>> + Send + 'a>> {
Box::pin(async move {
let meta = fs::metadata(path).await
.map_err(|e| Self::map_io_error(path, e))?;
Ok(Self::stat_from_metadata(&meta))
})
}
fn create_dir<'a>(&'a self, path: &'a Path, mode: u32) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
Box::pin(async move {
fs::create_dir(path).await
.map_err(|e| Self::map_io_error(path, e))?;
Ok(())
})
}
fn remove_dir<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
Box::pin(async move {
fs::remove_dir(path).await
.map_err(|e| Self::map_io_error(path, e))?;
Ok(())
})
}
fn remove_file<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
Box::pin(async move {
fs::remove_file(path).await
.map_err(|e| Self::map_io_error(path, e))?;
Ok(())
})
}
fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> Pin<Box<dyn Future<Output = Result<(), VfsError>> + Send + 'a>> {
Box::pin(async move {
fs::rename(from, to).await
.map_err(|e| Self::map_io_error(from, e))?;
Ok(())
})
}
fn exists<'a>(&'a self, path: &'a Path) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
Box::pin(async move {
fs::metadata(path).await.is_ok()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use crate::vfs::AsyncVfsBackend;
use crate::vfs::AsyncVfsFile;
#[tokio::test]
async fn test_async_read_dir() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join("subdir")).await.unwrap();
fs::write(tmp.path().join("test.txt"), "content").await.unwrap();
let vfs = AsyncLocalFs::with_root(tmp.path().to_path_buf());
let entries = AsyncVfsBackend::read_dir(&vfs, tmp.path()).await.unwrap();
assert_eq!(entries.len(), 2);
assert!(entries.iter().any(|e| e.name == "subdir" && e.stat.is_dir));
assert!(entries.iter().any(|e| e.name == "test.txt" && !e.stat.is_dir));
}
#[tokio::test]
async fn test_async_open_read() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join("test.txt"), "hello world").await.unwrap();
let vfs = AsyncLocalFs::with_root(tmp.path().to_path_buf());
let flags = OpenFlags::new().read();
let mut file = AsyncVfsBackend::open_file(&vfs, &tmp.path().join("test.txt"), &flags).await.unwrap();
let mut buf = [0u8; 11];
let n = AsyncVfsFile::read(&mut *file, &mut buf).await.unwrap();
assert_eq!(n, 11);
assert_eq!(&buf, b"hello world");
}
#[tokio::test]
async fn test_async_create_dir() {
let tmp = TempDir::new().unwrap();
let vfs = AsyncLocalFs::with_root(tmp.path().to_path_buf());
AsyncVfsBackend::create_dir(&vfs, &tmp.path().join("newdir"), 0o755).await.unwrap();
assert!(AsyncVfsBackend::exists(&vfs, &tmp.path().join("newdir")).await);
}
#[tokio::test]
async fn test_async_remove_file() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join("test.txt"), "content").await.unwrap();
let vfs = AsyncLocalFs::with_root(tmp.path().to_path_buf());
AsyncVfsBackend::remove_file(&vfs, &tmp.path().join("test.txt")).await.unwrap();
assert!(!AsyncVfsBackend::exists(&vfs, &tmp.path().join("test.txt")).await);
}
}

View File

@@ -8,6 +8,8 @@ pub mod smb_fs;
#[cfg(feature = "smb-server")] #[cfg(feature = "smb-server")]
pub mod smb_server_backend; pub mod smb_server_backend;
pub mod util; pub mod util;
#[cfg(feature = "async-vfs")]
pub mod async_fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime; use std::time::SystemTime;
@@ -567,7 +569,7 @@ pub trait AsyncVfsFile: Send + Sync {
fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<usize, VfsError>> + Send + 'a>>; fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<usize, VfsError>> + Send + 'a>>;
fn write<'a>(&'a mut self, buf: &'a [u8]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<usize, VfsError>> + Send + 'a>>; fn write<'a>(&'a mut self, buf: &'a [u8]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<usize, VfsError>> + Send + 'a>>;
fn seek<'a>(&'a mut self, pos: std::io::SeekFrom) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<u64, VfsError>> + Send + 'a>>; fn seek<'a>(&'a mut self, pos: std::io::SeekFrom) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<u64, VfsError>> + Send + 'a>>;
fn flush(&mut self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), VfsError>> + Send>>; fn flush<'a>(&'a mut self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), VfsError>> + Send + 'a>>;
} }
/// Async VFS 后端 trait用于异步文件系统操作 /// Async VFS 后端 trait用于异步文件系统操作