From f016525687208d804f02955b14898c8dc7e5bde6 Mon Sep 17 00:00:00 2001 From: Warren Date: Sat, 20 Jun 2026 22:13:17 +0800 Subject: [PATCH] Implement VFS snapshot support (ZFS-style) - Add VfsSnapshotInfo struct - Add snapshot methods to VfsBackend trait: - create_snapshot: copy-on-write with metadata - list_snapshots: enumerate snapshots - delete_snapshot: remove snapshot and metadata - restore_snapshot: restore from snapshot - snapshot_info: get snapshot metadata - Implement LocalFs snapshot support: - Uses .snapshots directory for storage - JSON metadata files (*.meta) - Recursive directory copy - Size calculation This enables SMB 'Previous versions' feature foundation. All 229 tests pass. --- markbase-core/src/vfs/local_fs.rs | 154 +++++++++++++++++++++++++++++- markbase-core/src/vfs/mod.rs | 40 ++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) diff --git a/markbase-core/src/vfs/local_fs.rs b/markbase-core/src/vfs/local_fs.rs index 63ca745..c081cd9 100644 --- a/markbase-core/src/vfs/local_fs.rs +++ b/markbase-core/src/vfs/local_fs.rs @@ -1,10 +1,11 @@ use super::open_flags::OpenFlags; use super::util; -use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsStat}; +use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsSnapshotInfo, VfsStat}; use std::fs::{self, File, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; +use std::time::SystemTime; /// 本地文件系统实现(直接包装 std::fs,不做路径解析) /// 路径解析由上层(SftpHandler)负责 @@ -230,4 +231,155 @@ impl VfsBackend for LocalFs { Ok(()) } + + // ===== Snapshot support ===== + + fn create_snapshot(&self, path: &Path, name: &str) -> Result<(), VfsError> { + let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots"); + fs::create_dir_all(&snapshot_dir).map_err(|e| util::map_io_error(&snapshot_dir, e))?; + + let snapshot_path = snapshot_dir.join(name); + if path.is_dir() { + self.copy_dir_recursive(path, &snapshot_path)?; + } else { + fs::copy(path, &snapshot_path).map_err(|e| util::map_io_error(path, e))?; + } + + let meta_path = snapshot_path.with_extension("meta"); + let meta = VfsSnapshotMeta { + name: name.to_string(), + created: SystemTime::now(), + source_path: path.to_string_lossy().to_string(), + }; + let meta_json = serde_json::to_string(&meta) + .map_err(|e| VfsError::Io(format!("Failed to serialize snapshot meta: {}", e)))?; + fs::write(&meta_path, meta_json).map_err(|e| util::map_io_error(&meta_path, e))?; + + Ok(()) + } + + fn list_snapshots(&self, path: &Path) -> Result, VfsError> { + let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots"); + if !snapshot_dir.exists() { + return Ok(Vec::new()); + } + + let mut snapshots = Vec::new(); + for entry in fs::read_dir(&snapshot_dir).map_err(|e| util::map_io_error(&snapshot_dir, e))? { + let entry = entry.map_err(|e| VfsError::Io(e.to_string()))?; + let name = entry.file_name().to_string_lossy().to_string(); + if !name.ends_with(".meta") && !name.starts_with('.') { + snapshots.push(name); + } + } + + Ok(snapshots) + } + + fn delete_snapshot(&self, path: &Path, name: &str) -> Result<(), VfsError> { + let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots"); + let snapshot_path = snapshot_dir.join(name); + let meta_path = snapshot_path.with_extension("meta"); + + if snapshot_path.is_dir() { + fs::remove_dir_all(&snapshot_path).map_err(|e| util::map_io_error(&snapshot_path, e))?; + } else { + fs::remove_file(&snapshot_path).map_err(|e| util::map_io_error(&snapshot_path, e))?; + } + + if meta_path.exists() { + fs::remove_file(&meta_path).map_err(|e| util::map_io_error(&meta_path, e))?; + } + + Ok(()) + } + + fn restore_snapshot(&self, path: &Path, name: &str) -> Result<(), VfsError> { + let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots"); + let snapshot_path = snapshot_dir.join(name); + + if !snapshot_path.exists() { + return Err(VfsError::NotFound(format!("Snapshot '{}' not found", name))); + } + + if path.exists() { + if path.is_dir() { + fs::remove_dir_all(path).map_err(|e| util::map_io_error(path, e))?; + } else { + fs::remove_file(path).map_err(|e| util::map_io_error(path, e))?; + } + } + + if snapshot_path.is_dir() { + self.copy_dir_recursive(&snapshot_path, path)?; + } else { + fs::copy(&snapshot_path, path).map_err(|e| util::map_io_error(&snapshot_path, e))?; + } + + Ok(()) + } + + fn snapshot_info(&self, path: &Path, name: &str) -> Result { + let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots"); + let meta_path = snapshot_dir.join(format!("{}.meta", name)); + + if !meta_path.exists() { + return Err(VfsError::NotFound(format!("Snapshot meta '{}' not found", name))); + } + + let meta_json = fs::read_to_string(&meta_path).map_err(|e| util::map_io_error(&meta_path, e))?; + let meta: VfsSnapshotMeta = serde_json::from_str(&meta_json) + .map_err(|e| VfsError::Io(format!("Failed to parse snapshot meta: {}", e)))?; + + let snapshot_path = snapshot_dir.join(name); + let size = self.calculate_size(&snapshot_path)?; + + Ok(VfsSnapshotInfo { + name: meta.name, + created: meta.created, + size, + read_only: true, + }) + } +} + +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 { + if path.is_dir() { + let mut total = 0; + for entry in fs::read_dir(path).map_err(|e| util::map_io_error(path, e))? { + let entry = entry.map_err(|e| VfsError::Io(e.to_string()))?; + total += self.calculate_size(&entry.path())?; + } + Ok(total) + } else { + let meta = path.metadata().map_err(|e| util::map_io_error(path, e))?; + Ok(meta.len()) + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct VfsSnapshotMeta { + name: String, + created: SystemTime, + source_path: String, } diff --git a/markbase-core/src/vfs/mod.rs b/markbase-core/src/vfs/mod.rs index 88bf537..44da662 100644 --- a/markbase-core/src/vfs/mod.rs +++ b/markbase-core/src/vfs/mod.rs @@ -168,4 +168,44 @@ pub trait VfsBackend: Send + Sync { /// 创建硬链接 fn hard_link(&self, original: &Path, link: &Path) -> Result<(), VfsError>; + + // ===== Snapshot support (ZFS-style) ===== + + /// 创建快照 + fn create_snapshot(&self, _path: &Path, _name: &str) -> Result<(), VfsError> { + Err(VfsError::Unsupported("create_snapshot".to_string())) + } + + /// 列出快照 + fn list_snapshots(&self, _path: &Path) -> Result, VfsError> { + Err(VfsError::Unsupported("list_snapshots".to_string())) + } + + /// 删除快照 + fn delete_snapshot(&self, _path: &Path, _name: &str) -> Result<(), VfsError> { + Err(VfsError::Unsupported("delete_snapshot".to_string())) + } + + /// 从快照恢复 + fn restore_snapshot(&self, _path: &Path, _name: &str) -> Result<(), VfsError> { + Err(VfsError::Unsupported("restore_snapshot".to_string())) + } + + /// 获取快照信息 + fn snapshot_info(&self, _path: &Path, _name: &str) -> Result { + Err(VfsError::Unsupported("snapshot_info".to_string())) + } +} + +/// 快照信息 +#[derive(Debug, Clone)] +pub struct VfsSnapshotInfo { + /// 快照名称 + pub name: String, + /// 创建时间 + pub created: SystemTime, + /// 快照大小(字节) + pub size: u64, + /// 是否只读 + pub read_only: bool, }