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::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)]
|
||||
|
||||
Reference in New Issue
Block a user