Implement SMB Previous versions (shadow copy) at VFS layer
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- 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:
Warren
2026-06-20 22:26:58 +08:00
parent 716eea788a
commit 837ffa923d
2 changed files with 161 additions and 1 deletions

View File

@@ -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<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 {
@@ -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)]