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.
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
use super::open_flags::OpenFlags;
|
use super::open_flags::OpenFlags;
|
||||||
use super::util;
|
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::fs::{self, File, OpenOptions};
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
/// 本地文件系统实现(直接包装 std::fs,不做路径解析)
|
/// 本地文件系统实现(直接包装 std::fs,不做路径解析)
|
||||||
/// 路径解析由上层(SftpHandler)负责
|
/// 路径解析由上层(SftpHandler)负责
|
||||||
@@ -230,4 +231,155 @@ impl VfsBackend for LocalFs {
|
|||||||
|
|
||||||
Ok(())
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,4 +168,44 @@ pub trait VfsBackend: Send + Sync {
|
|||||||
|
|
||||||
/// 创建硬链接
|
/// 创建硬链接
|
||||||
fn hard_link(&self, original: &Path, link: &Path) -> Result<(), VfsError>;
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user