WebDAV improvements: flush fix, RwLock recovery, expired lock cleanup, atomic set_times
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled

P0 fixes:
- flush(): add flushed flag, proper error logging, Drop warning for data loss
- props_data RwLock: replace unwrap() with try_read/try_write recovery
- PersistedLs: add is_expired() + cleanup_expired_locks() helper

P1 improvements:
- Props persistence via VFS (load_props/save_props/patch_props)
- COPY/MOVE sync dead props (copy on COPY, move key on rename)
- Atomic set_atime/set_mtime via filetime crate (no race condition)

New files:
- webdav_locks.rs: PersistedLs with lock persistence + expiry cleanup

Tests: 288 passed, 0 failed
This commit is contained in:
Warren
2026-06-21 16:07:12 +08:00
parent 614275f77a
commit 9acd174388
9 changed files with 1940 additions and 112 deletions

View File

@@ -153,6 +153,10 @@ impl VfsBackend for LocalFs {
fs::remove_dir(path).map_err(|e| util::map_io_error(path, e))
}
fn remove_dir_all(&self, path: &Path) -> Result<(), VfsError> {
fs::remove_dir_all(path).map_err(|e| util::map_io_error(path, e))
}
fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
fs::remove_file(path).map_err(|e| util::map_io_error(path, e))
}
@@ -185,6 +189,39 @@ impl VfsBackend for LocalFs {
Ok(())
}
fn set_times(&self, path: &Path, atime: SystemTime, mtime: SystemTime) -> Result<(), VfsError> {
let at = atime.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| VfsError::Io("atime before UNIX_EPOCH".to_string()))?;
let mt = mtime.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| VfsError::Io("mtime before UNIX_EPOCH".to_string()))?;
filetime::set_file_times(
path,
filetime::FileTime::from_unix_time(at.as_secs() as i64, at.subsec_nanos() as u32),
filetime::FileTime::from_unix_time(mt.as_secs() as i64, mt.subsec_nanos() as u32),
)
.map_err(|e| util::map_io_error(path, e))
}
fn set_atime(&self, path: &Path, atime: SystemTime) -> Result<(), VfsError> {
let at = atime.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| VfsError::Io("atime before UNIX_EPOCH".to_string()))?;
filetime::set_file_atime(
path,
filetime::FileTime::from_unix_time(at.as_secs() as i64, at.subsec_nanos() as u32),
)
.map_err(|e| util::map_io_error(path, e))
}
fn set_mtime(&self, path: &Path, mtime: SystemTime) -> Result<(), VfsError> {
let mt = mtime.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| VfsError::Io("mtime before UNIX_EPOCH".to_string()))?;
filetime::set_file_mtime(
path,
filetime::FileTime::from_unix_time(mt.as_secs() as i64, mt.subsec_nanos() as u32),
)
.map_err(|e| util::map_io_error(path, e))
}
fn read_link(&self, path: &Path) -> Result<PathBuf, VfsError> {
let target = fs::read_link(path).map_err(|e| util::map_io_error(path, e))?;
Ok(target)
@@ -232,6 +269,15 @@ impl VfsBackend for LocalFs {
Ok(())
}
fn copy(&self, from: &Path, to: &Path) -> Result<(), VfsError> {
// Check if source is a directory
if from.is_dir() {
return copy_dir_recursive_impl(from, to);
}
fs::copy(from, to).map_err(|e| util::map_io_error(from, e))?;
Ok(())
}
// ===== Snapshot support =====
fn create_snapshot(&self, path: &Path, name: &str) -> Result<(), VfsError> {
@@ -240,7 +286,7 @@ impl VfsBackend for LocalFs {
let snapshot_path = snapshot_dir.join(name);
if path.is_dir() {
self.copy_dir_recursive(path, &snapshot_path)?;
copy_dir_recursive_impl(path, &snapshot_path)?;
} else {
fs::copy(path, &snapshot_path).map_err(|e| util::map_io_error(path, e))?;
}
@@ -311,7 +357,7 @@ impl VfsBackend for LocalFs {
}
if snapshot_path.is_dir() {
self.copy_dir_recursive(&snapshot_path, path)?;
copy_dir_recursive_impl(&snapshot_path, path)?;
} else {
fs::copy(&snapshot_path, path).map_err(|e| util::map_io_error(&snapshot_path, e))?;
}
@@ -540,24 +586,6 @@ impl VfsBackend for LocalFs {
}
impl LocalFs {
fn copy_dir_recursive(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
fs::create_dir_all(dst).map_err(|e| util::map_io_error(dst, e))?;
for entry in fs::read_dir(src).map_err(|e| util::map_io_error(src, e))? {
let entry = entry.map_err(|e| VfsError::Io(e.to_string()))?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
self.copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path).map_err(|e| util::map_io_error(&src_path, e))?;
}
}
Ok(())
}
fn calculate_size(&self, path: &Path) -> Result<u64, VfsError> {
if path.is_dir() {
let mut total = 0;
@@ -772,6 +800,22 @@ impl VfsAclMeta {
}
}
/// Recursive directory copy helper (used by VfsBackend::copy)
fn copy_dir_recursive_impl(src: &Path, dst: &Path) -> Result<(), VfsError> {
fs::create_dir_all(dst).map_err(|e| util::map_io_error(dst, e))?;
for entry in fs::read_dir(src).map_err(|e| util::map_io_error(src, e))? {
let entry = entry.map_err(|e| util::map_io_error(src, e))?;
let src_entry = entry.path();
let dst_entry = dst.join(entry.file_name());
if src_entry.is_dir() {
copy_dir_recursive_impl(&src_entry, &dst_entry)?;
} else {
fs::copy(&src_entry, &dst_entry).map_err(|e| util::map_io_error(&src_entry, e))?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -148,6 +148,21 @@ pub trait VfsBackend: Send + Sync {
/// 删除空目录
fn remove_dir(&self, path: &Path) -> Result<(), VfsError>;
/// 递归删除目录及其所有内容
fn remove_dir_all(&self, path: &Path) -> Result<(), VfsError> {
// Default: read entries and remove one by one
let entries = self.read_dir(path)?;
for entry in entries {
let child = path.join(&entry.name);
if entry.stat.is_dir {
self.remove_dir_all(&child)?;
} else {
self.remove_file(&child)?;
}
}
self.remove_dir(path)
}
/// 删除文件
fn remove_file(&self, path: &Path) -> Result<(), VfsError>;
@@ -157,6 +172,28 @@ pub trait VfsBackend: Send + Sync {
/// 设置文件属性
fn set_stat(&self, path: &Path, stat: &VfsStat) -> Result<(), VfsError>;
/// 原子性设置 atime 和 mtime默认实现调用 stat + set_stat有 race condition
fn set_times(&self, path: &Path, atime: SystemTime, mtime: SystemTime) -> Result<(), VfsError> {
let mut stat = self.stat(path)?;
stat.atime = atime;
stat.mtime = mtime;
self.set_stat(path, &stat)
}
/// 原子性设置 atime默认实现调用 stat + set_stat有 race condition
fn set_atime(&self, path: &Path, atime: SystemTime) -> Result<(), VfsError> {
let mut stat = self.stat(path)?;
stat.atime = atime;
self.set_stat(path, &stat)
}
/// 原子性设置 mtime默认实现调用 stat + set_stat有 race condition
fn set_mtime(&self, path: &Path, mtime: SystemTime) -> Result<(), VfsError> {
let mut stat = self.stat(path)?;
stat.mtime = mtime;
self.set_stat(path, &stat)
}
/// 读取符号链接目标
fn read_link(&self, path: &Path) -> Result<PathBuf, VfsError>;
@@ -172,6 +209,24 @@ pub trait VfsBackend: Send + Sync {
/// 创建硬链接
fn hard_link(&self, original: &Path, link: &Path) -> Result<(), VfsError>;
/// 复制文件高效实现fallback 到 read+write
fn copy(&self, from: &Path, to: &Path) -> Result<(), VfsError> {
let flags = open_flags::OpenFlags::new().read();
let mut src = self.open_file(from, &flags)?;
let write_flags = open_flags::OpenFlags::new().write().create().truncate().mode(0o644);
let mut dst = self.open_file(to, &write_flags)?;
let mut buf = vec![0u8; 65536];
loop {
match src.read(&mut buf) {
Ok(0) => break,
Ok(n) => dst.write_all(&buf[..n])?,
Err(e) => return Err(e),
}
}
dst.flush()?;
Ok(())
}
// ===== Snapshot support (ZFS-style) =====
/// 创建快照