From 837ffa923d8df352a7310b57e185e255e331bf76 Mon Sep 17 00:00:00 2001 From: Warren Date: Sat, 20 Jun 2026 22:26:58 +0800 Subject: [PATCH] Implement SMB Previous versions (shadow copy) at VFS layer - Add VfsPreviousVersion struct (snapshot_name, gmt_token, created, size) - Add VfsBackend methods: - list_previous_versions() - enumerate snapshot versions - open_previous_version() - open file from snapshot by GMT token - restore_previous_version() - restore file from snapshot - LocalFs implementation: - systemtime_to_gmt_token() - convert SystemTime to @GMT-YYYY.MM.DD-HH.MM.SS - scan .snapshots directory for matching versions - use existing restore_snapshot() for restoration - Foundation for SMB shadow copy (@GMT- token support) All 229 tests pass. --- markbase-core/src/vfs/local_fs.rs | 132 +++++++++++++++++++++++++++++- markbase-core/src/vfs/mod.rs | 30 +++++++ 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/markbase-core/src/vfs/local_fs.rs b/markbase-core/src/vfs/local_fs.rs index 4da7b01..3217276 100644 --- a/markbase-core/src/vfs/local_fs.rs +++ b/markbase-core/src/vfs/local_fs.rs @@ -1,6 +1,6 @@ use super::open_flags::OpenFlags; use super::util; -use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsQuota, VfsQuotaUsage, VfsSnapshotInfo, VfsStat}; +use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsPreviousVersion, VfsQuota, VfsQuotaUsage, VfsSnapshotInfo, VfsStat}; use std::fs::{self, File, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; use std::os::unix::fs::{MetadataExt, PermissionsExt}; @@ -391,6 +391,100 @@ impl VfsBackend for LocalFs { let usage = self.get_quota_usage(path)?; Ok(usage.space_used + size <= quota.space_limit) } + + fn list_previous_versions(&self, path: &Path) -> Result, VfsError> { + let snapshots_dir = path.join(".snapshots"); + if !snapshots_dir.exists() { + return Ok(Vec::new()); + } + + let versions: Vec = fs::read_dir(&snapshots_dir) + .map_err(|e| util::map_io_error(&snapshots_dir, e))? + .filter_map(|entry| entry.ok()) + .filter_map(|entry| { + let snapshot_name = entry.file_name().to_string_lossy().to_string(); + let snapshot_path = entry.path(); + + if snapshot_name.starts_with('.') { + return None; + } + + let meta_file = snapshot_path.join(".meta"); + if !meta_file.exists() { + return None; + } + + let meta_json = fs::read_to_string(&meta_file).ok()?; + let meta: VfsSnapshotMeta = serde_json::from_str(&meta_json).ok()?; + + let gmt_token = Self::systemtime_to_gmt_token(meta.created); + + Some(VfsPreviousVersion { + snapshot_name, + gmt_token, + created: meta.created, + size: entry.metadata().ok()?.len(), + }) + }) + .collect(); + + Ok(versions) + } + + fn open_previous_version(&self, path: &Path, gmt_token: &str) -> Result, VfsError> { + let snapshots_dir = path.join(".snapshots"); + + for entry in fs::read_dir(&snapshots_dir) + .map_err(|e| util::map_io_error(&snapshots_dir, e))? { + let entry = entry.map_err(|e| VfsError::Io(e.to_string()))?; + let snapshot_name = entry.file_name().to_string_lossy().to_string(); + let snapshot_path = entry.path(); + + let meta_file = snapshot_path.join(".meta"); + if meta_file.exists() { + let meta_json = fs::read_to_string(&meta_file) + .map_err(|e| util::map_io_error(&meta_file, e))?; + let meta: VfsSnapshotMeta = serde_json::from_str(&meta_json) + .map_err(|e| VfsError::Io(format!("Failed to parse meta: {}", e)))?; + + let expected_gmt = Self::systemtime_to_gmt_token(meta.created); + if expected_gmt == gmt_token { + let file_path = snapshot_path.join(path.file_name().unwrap_or_default()); + let file = File::open(&file_path) + .map_err(|e| util::map_io_error(&file_path, e))?; + return Ok(Box::new(LocalFile { file })); + } + } + } + + Err(VfsError::NotFound(format!("No snapshot found with GMT token: {}", gmt_token))) + } + + fn restore_previous_version(&self, path: &Path, gmt_token: &str) -> Result<(), VfsError> { + let snapshots_dir = path.join(".snapshots"); + + for entry in fs::read_dir(&snapshots_dir) + .map_err(|e| util::map_io_error(&snapshots_dir, e))? { + let entry = entry.map_err(|e| VfsError::Io(e.to_string()))?; + let snapshot_name = entry.file_name().to_string_lossy().to_string(); + let snapshot_path = entry.path(); + + let meta_file = snapshot_path.join(".meta"); + if meta_file.exists() { + let meta_json = fs::read_to_string(&meta_file) + .map_err(|e| util::map_io_error(&meta_file, e))?; + let meta: VfsSnapshotMeta = serde_json::from_str(&meta_json) + .map_err(|e| VfsError::Io(format!("Failed to parse meta: {}", e)))?; + + let expected_gmt = Self::systemtime_to_gmt_token(meta.created); + if expected_gmt == gmt_token { + return self.restore_snapshot(path, &snapshot_name); + } + } + } + + Err(VfsError::NotFound(format!("No snapshot found with GMT token: {}", gmt_token))) + } } impl LocalFs { @@ -469,6 +563,42 @@ impl LocalFs { Ok(1) } } + + fn systemtime_to_gmt_token(time: SystemTime) -> String { + use std::time::UNIX_EPOCH; + let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default(); + let secs = duration.as_secs(); + + let days = secs / 86400; + let rem_secs = secs % 86400; + let hours = rem_secs / 3600; + let minutes = (rem_secs % 3600) / 60; + let seconds = rem_secs % 60; + + // Days since Unix epoch to YYYY.MM.DD + let mut year = 1970; + let mut remaining_days = days; + + while remaining_days >= 365 { + let leap = if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) { 1 } else { 0 }; + remaining_days -= 365 + leap; + year += 1; + } + + let leap = if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) { 1 } else { 0 }; + let days_in_months = [31, 28 + leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let mut month = 1; + for days_in_month in days_in_months.iter() { + if remaining_days < *days_in_month { + break; + } + remaining_days -= *days_in_month; + month += 1; + } + let day = remaining_days + 1; + + format!("@GMT-{:04}.{:02}.{:02}-{:02}.{:02}.{:02}", year, month, day, hours, minutes, seconds) + } } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] diff --git a/markbase-core/src/vfs/mod.rs b/markbase-core/src/vfs/mod.rs index 9305d04..22a8e3e 100644 --- a/markbase-core/src/vfs/mod.rs +++ b/markbase-core/src/vfs/mod.rs @@ -218,6 +218,23 @@ pub trait VfsBackend: Send + Sync { fn check_quota(&self, _path: &Path, _size: u64) -> Result { Ok(true) // Default: no quota, always allow } + + // ===== Previous versions (shadow copy) ===== + + /// 列出文件的所有历史版本 + fn list_previous_versions(&self, _path: &Path) -> Result, VfsError> { + Err(VfsError::Unsupported("list_previous_versions".to_string())) + } + + /// 打开历史版本文件(通过 @GMT- token) + fn open_previous_version(&self, _path: &Path, _gmt_token: &str) -> Result, VfsError> { + Err(VfsError::Unsupported("open_previous_version".to_string())) + } + + /// 从历史版本恢复文件 + fn restore_previous_version(&self, _path: &Path, _gmt_token: &str) -> Result<(), VfsError> { + Err(VfsError::Unsupported("restore_previous_version".to_string())) + } } /// 快照信息 @@ -261,6 +278,19 @@ pub struct VfsQuotaUsage { pub over_hard_limit: bool, } +/// 历史版本信息(SMB shadow copy) +#[derive(Debug, Clone)] +pub struct VfsPreviousVersion { + /// 快照名称 + pub snapshot_name: String, + /// GMT token (@GMT-YYYY.MM.DD-HH.MM.SS) + pub gmt_token: String, + /// 创建时间 + pub created: SystemTime, + /// 版本大小(字节) + pub size: u64, +} + /// 压缩算法类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VfsCompression {