Implement VFS snapshot support (ZFS-style)
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- 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.
This commit is contained in:
Warren
2026-06-20 22:13:17 +08:00
parent 7b033e5276
commit f016525687
2 changed files with 193 additions and 1 deletions

View File

@@ -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<Vec<String>, 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<VfsSnapshotInfo, VfsError> {
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<u64, VfsError> {
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,
}

View File

@@ -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<Vec<String>, 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<VfsSnapshotInfo, VfsError> {
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,
}