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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use super::open_flags::OpenFlags;
|
use super::open_flags::OpenFlags;
|
||||||
use super::util;
|
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::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};
|
||||||
@@ -391,6 +391,100 @@ impl VfsBackend for LocalFs {
|
|||||||
let usage = self.get_quota_usage(path)?;
|
let usage = self.get_quota_usage(path)?;
|
||||||
Ok(usage.space_used + size <= quota.space_limit)
|
Ok(usage.space_used + size <= quota.space_limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_previous_versions(&self, path: &Path) -> Result<Vec<VfsPreviousVersion>, VfsError> {
|
||||||
|
let snapshots_dir = path.join(".snapshots");
|
||||||
|
if !snapshots_dir.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let versions: Vec<VfsPreviousVersion> = 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<Box<dyn VfsFile>, 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 {
|
impl LocalFs {
|
||||||
@@ -469,6 +563,42 @@ impl LocalFs {
|
|||||||
Ok(1)
|
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)]
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
|
|||||||
@@ -218,6 +218,23 @@ pub trait VfsBackend: Send + Sync {
|
|||||||
fn check_quota(&self, _path: &Path, _size: u64) -> Result<bool, VfsError> {
|
fn check_quota(&self, _path: &Path, _size: u64) -> Result<bool, VfsError> {
|
||||||
Ok(true) // Default: no quota, always allow
|
Ok(true) // Default: no quota, always allow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Previous versions (shadow copy) =====
|
||||||
|
|
||||||
|
/// 列出文件的所有历史版本
|
||||||
|
fn list_previous_versions(&self, _path: &Path) -> Result<Vec<VfsPreviousVersion>, VfsError> {
|
||||||
|
Err(VfsError::Unsupported("list_previous_versions".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开历史版本文件(通过 @GMT- token)
|
||||||
|
fn open_previous_version(&self, _path: &Path, _gmt_token: &str) -> Result<Box<dyn VfsFile>, 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,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum VfsCompression {
|
pub enum VfsCompression {
|
||||||
|
|||||||
Reference in New Issue
Block a user