use super::open_flags::OpenFlags; use super::util; use super::{VfsAce, VfsAceFlag, VfsAceMask, VfsAceType, VfsAcl, 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 { self.file.read(buf).map_err(|e| VfsError::Io(e.to_string())) } fn write(&mut self, buf: &[u8]) -> Result { self.file .write(buf) .map_err(|e| VfsError::Io(e.to_string())) } fn seek(&mut self, pos: SeekFrom) -> Result { 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 { 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 { Box::new(Self {}) } fn read_dir(&self, path: &Path) -> Result, 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, 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 { 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 { 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_dir_all(&self, path: &Path) -> Result<(), VfsError> { fs::remove_dir_all(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 set_times(&self, path: &Path, atime: SystemTime, mtime: SystemTime) -> Result<(), VfsError> { let at = atime.duration_since(std::time::UNIX_EPOCH) .map_err(|_| VfsError::Io("atime before UNIX_EPOCH".to_string()))?; let mt = mtime.duration_since(std::time::UNIX_EPOCH) .map_err(|_| VfsError::Io("mtime before UNIX_EPOCH".to_string()))?; filetime::set_file_times( path, filetime::FileTime::from_unix_time(at.as_secs() as i64, at.subsec_nanos()), filetime::FileTime::from_unix_time(mt.as_secs() as i64, mt.subsec_nanos()), ) .map_err(|e| util::map_io_error(path, e)) } fn set_atime(&self, path: &Path, atime: SystemTime) -> Result<(), VfsError> { let at = atime.duration_since(std::time::UNIX_EPOCH) .map_err(|_| VfsError::Io("atime before UNIX_EPOCH".to_string()))?; filetime::set_file_atime( path, filetime::FileTime::from_unix_time(at.as_secs() as i64, at.subsec_nanos()), ) .map_err(|e| util::map_io_error(path, e)) } fn set_mtime(&self, path: &Path, mtime: SystemTime) -> Result<(), VfsError> { let mt = mtime.duration_since(std::time::UNIX_EPOCH) .map_err(|_| VfsError::Io("mtime before UNIX_EPOCH".to_string()))?; filetime::set_file_mtime( path, filetime::FileTime::from_unix_time(mt.as_secs() as i64, mt.subsec_nanos()), ) .map_err(|e| util::map_io_error(path, e)) } fn read_link(&self, path: &Path) -> Result { 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 { 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(()) } fn copy(&self, from: &Path, to: &Path) -> Result<(), VfsError> { // Check if source is a directory if from.is_dir() { return copy_dir_recursive_impl(from, to); } fs::copy(from, to).map_err(|e| util::map_io_error(from, e))?; 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() { copy_dir_recursive_impl(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, 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() { copy_dir_recursive_impl(&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 { 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 { 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 { 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 { 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, 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))) } fn get_acl(&self, path: &Path) -> Result { let acl_file = path.join(".acl"); if !acl_file.exists() { return Ok(VfsAcl::default()); } let json = fs::read_to_string(&acl_file) .map_err(|e| util::map_io_error(&acl_file, e))?; let acl_meta: VfsAclMeta = serde_json::from_str(&json) .map_err(|e| VfsError::Io(format!("Failed to parse ACL meta: {}", e)))?; Ok(acl_meta.to_acl()) } fn set_acl(&self, path: &Path, acl: &VfsAcl) -> Result<(), VfsError> { let acl_file = path.join(".acl"); let acl_meta = VfsAclMeta::from_acl(acl); let json = serde_json::to_string(&acl_meta) .map_err(|e| VfsError::Io(format!("Failed to serialize ACL meta: {}", e)))?; fs::write(&acl_file, json) .map_err(|e| util::map_io_error(&acl_file, e)) } fn check_acl(&self, path: &Path, principal: &str, mask: VfsAceMask) -> Result { let acl = self.get_acl(path)?; for ace in &acl.aces { if (ace.principal == principal || ace.principal == "*") && ace.mask.contains(&mask) { return Ok(ace.ace_type == VfsAceType::Allow); } } Ok(true) } fn add_ace(&self, path: &Path, ace: &VfsAce) -> Result<(), VfsError> { let mut acl = self.get_acl(path)?; acl.aces.push(ace.clone()); self.set_acl(path, &acl) } fn remove_ace(&self, path: &Path, ace_index: usize) -> Result<(), VfsError> { let mut acl = self.get_acl(path)?; if ace_index >= acl.aces.len() { return Err(VfsError::NotFound(format!("ACE index {} out of range", ace_index))); } acl.aces.remove(ace_index); self.set_acl(path, &acl) } } impl LocalFs { fn calculate_size(&self, path: &Path) -> Result { 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 { let quota_file = Self::quota_file(path); if !quota_file.exists() { return Ok(VfsQuotaMeta::default()); } let json = fs::read_to_string("a_file) .map_err(|e| util::map_io_error("a_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("a_file, json) .map_err(|e| util::map_io_error("a_file, e)) } fn count_files(path: &Path) -> Result { 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, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct VfsSnapshotMeta { name: String, created: SystemTime, source_path: String, } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] struct VfsAceMeta { ace_type: String, flags: Vec, mask: Vec, principal: String, } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] struct VfsAclMeta { aces: Vec, default_acl: Option>, } impl VfsAclMeta { fn from_acl(acl: &VfsAcl) -> Self { Self { aces: acl.aces.iter().map(|ace| VfsAceMeta { ace_type: match ace.ace_type { VfsAceType::Allow => "allow".to_string(), VfsAceType::Deny => "deny".to_string(), VfsAceType::Audit => "audit".to_string(), VfsAceType::Alarm => "alarm".to_string(), }, flags: ace.flags.iter().map(|f| match f { VfsAceFlag::FileInherit => "file_inherit".to_string(), VfsAceFlag::DirectoryInherit => "directory_inherit".to_string(), VfsAceFlag::NoPropagateInherit => "no_propagate".to_string(), VfsAceFlag::InheritOnly => "inherit_only".to_string(), VfsAceFlag::Inherited => "inherited".to_string(), VfsAceFlag::SuccessfulAccess => "successful_access".to_string(), VfsAceFlag::FailedAccess => "failed_access".to_string(), }).collect(), mask: ace.mask.iter().map(|m| match m { VfsAceMask::ReadData => "read_data".to_string(), VfsAceMask::WriteData => "write_data".to_string(), VfsAceMask::Execute => "execute".to_string(), VfsAceMask::ListDirectory => "list_directory".to_string(), VfsAceMask::AddFile => "add_file".to_string(), VfsAceMask::AddSubdirectory => "add_subdirectory".to_string(), VfsAceMask::DeleteChild => "delete_child".to_string(), VfsAceMask::Delete => "delete".to_string(), VfsAceMask::ReadAttributes => "read_attributes".to_string(), VfsAceMask::WriteAttributes => "write_attributes".to_string(), VfsAceMask::ReadNfsAcl => "read_acl".to_string(), VfsAceMask::WriteNfsAcl => "write_acl".to_string(), VfsAceMask::ReadOwner => "read_owner".to_string(), VfsAceMask::WriteOwner => "write_owner".to_string(), VfsAceMask::Synchronize => "synchronize".to_string(), VfsAceMask::FullControl => "full_control".to_string(), }).collect(), principal: ace.principal.clone(), }).collect(), default_acl: acl.default_acl.as_ref().map(|dacl| Box::new(Self::from_acl(dacl))), } } fn to_acl(&self) -> VfsAcl { VfsAcl { aces: self.aces.iter().map(|ace| VfsAce { ace_type: match ace.ace_type.as_str() { "allow" => VfsAceType::Allow, "deny" => VfsAceType::Deny, "audit" => VfsAceType::Audit, "alarm" => VfsAceType::Alarm, _ => VfsAceType::Allow, }, flags: ace.flags.iter().map(|f| match f.as_str() { "file_inherit" => VfsAceFlag::FileInherit, "directory_inherit" => VfsAceFlag::DirectoryInherit, "no_propagate" => VfsAceFlag::NoPropagateInherit, "inherit_only" => VfsAceFlag::InheritOnly, "inherited" => VfsAceFlag::Inherited, "successful_access" => VfsAceFlag::SuccessfulAccess, "failed_access" => VfsAceFlag::FailedAccess, _ => VfsAceFlag::FileInherit, }).collect(), mask: ace.mask.iter().map(|m| match m.as_str() { "read_data" => VfsAceMask::ReadData, "write_data" => VfsAceMask::WriteData, "execute" => VfsAceMask::Execute, "list_directory" => VfsAceMask::ListDirectory, "add_file" => VfsAceMask::AddFile, "add_subdirectory" => VfsAceMask::AddSubdirectory, "delete_child" => VfsAceMask::DeleteChild, "delete" => VfsAceMask::Delete, "read_attributes" => VfsAceMask::ReadAttributes, "write_attributes" => VfsAceMask::WriteAttributes, "read_acl" => VfsAceMask::ReadNfsAcl, "write_acl" => VfsAceMask::WriteNfsAcl, "read_owner" => VfsAceMask::ReadOwner, "write_owner" => VfsAceMask::WriteOwner, "synchronize" => VfsAceMask::Synchronize, "full_control" => VfsAceMask::FullControl, _ => VfsAceMask::ReadData, }).collect(), principal: ace.principal.clone(), }).collect(), default_acl: self.default_acl.as_ref().map(|dacl| Box::new(dacl.to_acl())), } } } /// Recursive directory copy helper (used by VfsBackend::copy) fn copy_dir_recursive_impl(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| util::map_io_error(src, e))?; let src_entry = entry.path(); let dst_entry = dst.join(entry.file_name()); if src_entry.is_dir() { copy_dir_recursive_impl(&src_entry, &dst_entry)?; } else { fs::copy(&src_entry, &dst_entry).map_err(|e| util::map_io_error(&src_entry, e))?; } } Ok(()) } #[cfg(test)] mod tests { use super::*; use std::fs; use std::time::Duration; use tempfile::TempDir; fn setup_snapshots(base_dir: &Path, snapshot_name: &str) -> PathBuf { let snapshots_dir = base_dir.join(".snapshots"); let snapshot_path = snapshots_dir.join(snapshot_name); fs::create_dir_all(&snapshot_path).unwrap(); let meta = VfsSnapshotMeta { name: snapshot_name.to_string(), created: SystemTime::now(), source_path: base_dir.to_string_lossy().to_string(), }; let meta_file = snapshot_path.join(".meta"); let meta_json = serde_json::to_string(&meta).unwrap(); fs::write(&meta_file, &meta_json).unwrap(); snapshot_path } #[test] fn test_list_previous_versions_with_snapshot() { let temp_dir = TempDir::new().unwrap(); let fs_backend = LocalFs::new(); setup_snapshots(temp_dir.path(), "snapshot_1"); let versions = fs_backend.list_previous_versions(temp_dir.path()).unwrap(); assert_eq!(versions.len(), 1); let version = &versions[0]; assert_eq!(version.snapshot_name, "snapshot_1"); assert!(version.gmt_token.starts_with("@GMT-")); } #[test] fn test_list_previous_versions_multiple() { let temp_dir = TempDir::new().unwrap(); let fs_backend = LocalFs::new(); setup_snapshots(temp_dir.path(), "snapshot_1"); std::thread::sleep(Duration::from_secs(2)); setup_snapshots(temp_dir.path(), "snapshot_2"); let versions = fs_backend.list_previous_versions(temp_dir.path()).unwrap(); assert_eq!(versions.len(), 2); } #[test] fn test_open_previous_version_not_found() { let temp_dir = TempDir::new().unwrap(); let fs_backend = LocalFs::new(); let result = fs_backend.open_previous_version(temp_dir.path(), "@GMT-2021.01.01-00.00.00"); assert!(result.is_err()); } #[test] fn test_list_previous_versions_empty() { let temp_dir = TempDir::new().unwrap(); let fs_backend = LocalFs::new(); let versions = fs_backend.list_previous_versions(temp_dir.path()).unwrap(); assert_eq!(versions.len(), 0); } #[test] fn test_gmt_token_unique() { let temp_dir = TempDir::new().unwrap(); setup_snapshots(temp_dir.path(), "snapshot_1"); std::thread::sleep(Duration::from_secs(2)); setup_snapshots(temp_dir.path(), "snapshot_2"); let fs_backend = LocalFs::new(); let versions = fs_backend.list_previous_versions(temp_dir.path()).unwrap(); let gmt_tokens: Vec<&str> = versions.iter().map(|v| v.gmt_token.as_str()).collect(); assert_ne!(gmt_tokens[0], gmt_tokens[1]); } #[test] fn test_skip_hidden_snapshots() { let temp_dir = TempDir::new().unwrap(); let fs_backend = LocalFs::new(); setup_snapshots(temp_dir.path(), "snapshot_1"); let hidden_snapshot_path = temp_dir.path().join(".snapshots").join(".hidden"); fs::create_dir_all(&hidden_snapshot_path).unwrap(); let versions = fs_backend.list_previous_versions(temp_dir.path()).unwrap(); assert_eq!(versions.len(), 1); } #[test] fn test_snapshot_meta_parse() { let temp_dir = TempDir::new().unwrap(); let snapshot_path = setup_snapshots(temp_dir.path(), "test_snapshot"); let meta_file = snapshot_path.join(".meta"); let meta_json = fs::read_to_string(&meta_file).unwrap(); let meta: VfsSnapshotMeta = serde_json::from_str(&meta_json).unwrap(); assert_eq!(meta.name, "test_snapshot"); } }