Files
markbase/markbase-core/src/vfs/local_fs.rs
Warren 9b02bbac27
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
A: Code quality improvements - fix clippy warnings
- Remove unused imports in server.rs (Body, HeaderValue, RwLock)
- Remove unused imports in forward_acl.rs (tests still need Ipv4Addr)
- Remove unused imports in host_key.rs (Read, Write)
- Remove unused imports in kex_exchange.rs (HostKeyType)
- Remove unused imports in known_hosts.rs (tests need Ipv4Addr)
- Remove unused imports in multiplex.rs (Arc)
- Auto-fix other unused imports via clippy --fix

Tests: 303 passed, 0 failed (4 new tests added)
2026-06-21 23:08:07 +08:00

932 lines
34 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::{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<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_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<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(())
}
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<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() {
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<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)))
}
fn get_acl(&self, path: &Path) -> Result<VfsAcl, VfsError> {
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<bool, VfsError> {
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<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,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct VfsAceMeta {
ace_type: String,
flags: Vec<String>,
mask: Vec<String>,
principal: String,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct VfsAclMeta {
aces: Vec<VfsAceMeta>,
default_acl: Option<Box<VfsAclMeta>>,
}
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");
}
}