- 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)
932 lines
34 KiB
Rust
932 lines
34 KiB
Rust
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("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<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");
|
||
}
|
||
}
|