Files
markbase/markbase-core/src/vfs/local_fs.rs
Warren 837ffa923d
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
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.
2026-06-20 22:26:58 +08:00

619 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::open_flags::OpenFlags;
use super::util;
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};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
/// 本地文件系统实现(直接包装 std::fs不做路径解析
/// 路径解析由上层SftpHandler负责
pub struct LocalFs;
impl Default for LocalFs {
fn default() -> Self {
Self::new()
}
}
impl LocalFs {
pub fn new() -> Self {
Self
}
}
struct LocalFile {
file: File,
}
impl VfsFile for LocalFile {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, VfsError> {
self.file.read(buf).map_err(|e| VfsError::Io(e.to_string()))
}
fn write(&mut self, buf: &[u8]) -> Result<usize, VfsError> {
self.file
.write(buf)
.map_err(|e| VfsError::Io(e.to_string()))
}
fn seek(&mut self, pos: SeekFrom) -> Result<u64, VfsError> {
self.file.seek(pos).map_err(|e| VfsError::Io(e.to_string()))
}
fn flush(&mut self) -> Result<(), VfsError> {
self.file.flush().map_err(|e| VfsError::Io(e.to_string()))
}
fn stat(&mut self) -> Result<VfsStat, VfsError> {
let meta = self
.file
.metadata()
.map_err(|e| VfsError::Io(e.to_string()))?;
Ok(util::stat_from_metadata(&meta, false))
}
fn set_len(&mut self, size: u64) -> Result<(), VfsError> {
self.file
.set_len(size)
.map_err(|e| VfsError::Io(e.to_string()))
}
}
impl VfsBackend for LocalFs {
fn clone_boxed(&self) -> Box<dyn VfsBackend> {
Box::new(Self {})
}
fn read_dir(&self, path: &Path) -> Result<Vec<VfsDirEntry>, VfsError> {
let dir = fs::read_dir(path).map_err(|e| util::map_io_error(path, e))?;
let mut entries = Vec::new();
for entry in dir {
let entry = entry.map_err(|e| util::map_io_error(path, e))?;
let name = entry.file_name().to_string_lossy().to_string();
let file_type = entry.file_type().map_err(|e| util::map_io_error(path, e))?;
let meta = entry.metadata().map_err(|e| util::map_io_error(path, e))?;
let stat = util::stat_from_metadata(&meta, file_type.is_symlink());
let long_name = util::build_long_name(&stat, &name);
entries.push(VfsDirEntry {
name,
long_name,
stat,
});
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
}
fn open_file(&self, path: &Path, flags: &OpenFlags) -> Result<Box<dyn VfsFile>, VfsError> {
let mut opts = OpenOptions::new();
opts.read(flags.read);
opts.write(flags.write);
opts.append(flags.append);
opts.create(flags.create);
opts.truncate(flags.truncate);
opts.create_new(flags.exclusive);
let file = opts.open(path).map_err(|e| util::map_io_error(path, e))?;
#[cfg(unix)]
if flags.create && !flags.exclusive {
if let Ok(meta) = file.metadata() {
if flags.mode != 0 && meta.permissions().mode() != flags.mode {
fs::set_permissions(path, std::fs::Permissions::from_mode(flags.mode)).ok();
}
}
}
Ok(Box::new(LocalFile { file }))
}
fn stat(&self, path: &Path) -> Result<VfsStat, VfsError> {
let meta = fs::metadata(path).map_err(|e| util::map_io_error(path, e))?;
Ok(util::stat_from_metadata(&meta, false))
}
fn lstat(&self, path: &Path) -> Result<VfsStat, VfsError> {
let meta = fs::symlink_metadata(path).map_err(|e| util::map_io_error(path, e))?;
let is_symlink = path.is_symlink() || meta.file_type().is_symlink();
Ok(util::stat_from_metadata(&meta, is_symlink))
}
fn create_dir(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
fs::create_dir(path).map_err(|e| util::map_io_error(path, e))?;
#[cfg(unix)]
{
fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
.map_err(|e| util::map_io_error(path, e))?;
}
Ok(())
}
fn create_dir_all(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
fs::create_dir_all(path).map_err(|e| util::map_io_error(path, e))?;
#[cfg(unix)]
{
if mode != 0 {
fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
.map_err(|e| util::map_io_error(path, e))?;
}
}
Ok(())
}
fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
fs::remove_dir(path).map_err(|e| util::map_io_error(path, e))
}
fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
fs::remove_file(path).map_err(|e| util::map_io_error(path, e))
}
fn rename(&self, from: &Path, to: &Path) -> Result<(), VfsError> {
fs::rename(from, to).map_err(|e| util::map_io_error(from, e))
}
fn set_stat(&self, path: &Path, stat: &VfsStat) -> Result<(), VfsError> {
#[cfg(unix)]
{
if stat.mode != 0 {
fs::set_permissions(path, std::fs::Permissions::from_mode(stat.mode))
.map_err(|e| util::map_io_error(path, e))?;
}
}
if let (Some(atime), Some(mtime)) = (
stat.atime.duration_since(std::time::UNIX_EPOCH).ok(),
stat.mtime.duration_since(std::time::UNIX_EPOCH).ok(),
) {
filetime::set_file_times(
path,
filetime::FileTime::from_unix_time(atime.as_secs() as i64, 0),
filetime::FileTime::from_unix_time(mtime.as_secs() as i64, 0),
)
.map_err(|e| util::map_io_error(path, e))?;
}
Ok(())
}
fn read_link(&self, path: &Path) -> Result<PathBuf, VfsError> {
let target = fs::read_link(path).map_err(|e| util::map_io_error(path, e))?;
Ok(target)
}
fn create_symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link).map_err(|e| util::map_io_error(link, e))?;
}
#[cfg(not(unix))]
{
std::os::windows::fs::symlink_file(target, link)
.map_err(|e| util::map_io_error(link, e))?;
}
Ok(())
}
fn real_path(&self, path: &Path) -> Result<PathBuf, VfsError> {
let canonical = path
.canonicalize()
.map_err(|e| util::map_io_error(path, e))?;
Ok(canonical)
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn hard_link(&self, original: &Path, link: &Path) -> Result<(), VfsError> {
#[cfg(unix)]
{
fs::hard_link(original, link).map_err(|e| util::map_io_error(original, e))?;
}
#[cfg(not(unix))]
{
return Err(VfsError::Unsupported(
"hard_link not supported on non-Unix systems".to_string(),
));
}
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,
})
}
// ===== Quota support =====
fn set_quota(&self, path: &Path, quota: &VfsQuota) -> Result<(), VfsError> {
let meta = VfsQuotaMeta {
space_limit: quota.space_limit,
file_limit: quota.file_limit,
soft_limit: quota.soft_limit,
grace_period: quota.grace_period,
user_id: quota.user_id.clone(),
};
Self::write_quota_meta(path, &meta)
}
fn get_quota(&self, path: &Path) -> Result<VfsQuota, VfsError> {
let meta = Self::read_quota_meta(path)?;
Ok(VfsQuota {
space_limit: meta.space_limit,
file_limit: meta.file_limit,
soft_limit: meta.soft_limit,
grace_period: meta.grace_period,
user_id: meta.user_id,
})
}
fn get_quota_usage(&self, path: &Path) -> Result<VfsQuotaUsage, VfsError> {
let space_used = self.calculate_size(path)?;
let files_used = Self::count_files(path)?;
let quota = self.get_quota(path)?;
let over_soft_limit = quota.soft_limit > 0 && space_used >= quota.soft_limit;
let over_hard_limit = quota.space_limit > 0 && space_used >= quota.space_limit;
Ok(VfsQuotaUsage {
space_used,
files_used,
over_soft_limit,
over_hard_limit,
})
}
fn check_quota(&self, path: &Path, size: u64) -> Result<bool, VfsError> {
let quota = self.get_quota(path)?;
if quota.space_limit == 0 {
return Ok(true);
}
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 {
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())
}
}
}
// ===== Quota implementation =====
impl LocalFs {
fn quota_file(path: &Path) -> PathBuf {
path.join(".quota")
}
fn read_quota_meta(path: &Path) -> Result<VfsQuotaMeta, VfsError> {
let quota_file = Self::quota_file(path);
if !quota_file.exists() {
return Ok(VfsQuotaMeta::default());
}
let json = fs::read_to_string(&quota_file)
.map_err(|e| util::map_io_error(&quota_file, e))?;
serde_json::from_str(&json)
.map_err(|e| VfsError::Io(format!("Failed to parse quota meta: {}", e)))
}
fn write_quota_meta(path: &Path, meta: &VfsQuotaMeta) -> Result<(), VfsError> {
let quota_file = Self::quota_file(path);
let json = serde_json::to_string(meta)
.map_err(|e| VfsError::Io(format!("Failed to serialize quota meta: {}", e)))?;
fs::write(&quota_file, json)
.map_err(|e| util::map_io_error(&quota_file, e))
}
fn count_files(path: &Path) -> Result<u64, VfsError> {
if path.is_dir() {
let mut count = 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()))?;
let entry_path = entry.path();
if entry_path.file_name().map(|n| n.to_string_lossy().starts_with('.')).unwrap_or(false) {
continue; // Skip hidden files like .quota, .snapshots
}
count += Self::count_files(&entry_path)?;
}
Ok(count)
} else {
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)]
struct VfsQuotaMeta {
space_limit: u64,
file_limit: u64,
soft_limit: u64,
grace_period: u64,
user_id: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct VfsSnapshotMeta {
name: String,
created: SystemTime,
source_path: String,
}