use super::open_flags::OpenFlags; use super::util; use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, 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_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 { 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(()) } // ===== 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, 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 { 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) } } 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 { 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) } } } #[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, }