- Add VfsQuota and VfsQuotaUsage structs - Add quota methods to VfsBackend trait: - set_quota: set space/file limits - get_quota: retrieve quota settings - get_quota_usage: current usage stats - check_quota: pre-write check - Implement LocalFs quota support: - Uses .quota metadata file - JSON storage for quota limits - Recursive size/file counting - Hidden files excluded (.quota, .snapshots) Enables SMB per-share/user quota enforcement. All 229 tests pass.
489 lines
16 KiB
Rust
489 lines
16 KiB
Rust
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<usize, VfsError> {
|
||
self.file.read(buf).map_err(|e| VfsError::Io(e.to_string()))
|
||
}
|
||
|
||
fn write(&mut self, buf: &[u8]) -> Result<usize, VfsError> {
|
||
self.file
|
||
.write(buf)
|
||
.map_err(|e| VfsError::Io(e.to_string()))
|
||
}
|
||
|
||
fn seek(&mut self, pos: SeekFrom) -> Result<u64, VfsError> {
|
||
self.file.seek(pos).map_err(|e| VfsError::Io(e.to_string()))
|
||
}
|
||
|
||
fn flush(&mut self) -> Result<(), VfsError> {
|
||
self.file.flush().map_err(|e| VfsError::Io(e.to_string()))
|
||
}
|
||
|
||
fn stat(&mut self) -> Result<VfsStat, VfsError> {
|
||
let meta = self
|
||
.file
|
||
.metadata()
|
||
.map_err(|e| VfsError::Io(e.to_string()))?;
|
||
Ok(util::stat_from_metadata(&meta, false))
|
||
}
|
||
|
||
fn set_len(&mut self, size: u64) -> Result<(), VfsError> {
|
||
self.file
|
||
.set_len(size)
|
||
.map_err(|e| VfsError::Io(e.to_string()))
|
||
}
|
||
}
|
||
|
||
impl VfsBackend for LocalFs {
|
||
fn clone_boxed(&self) -> Box<dyn VfsBackend> {
|
||
Box::new(Self {})
|
||
}
|
||
|
||
fn read_dir(&self, path: &Path) -> Result<Vec<VfsDirEntry>, VfsError> {
|
||
let dir = fs::read_dir(path).map_err(|e| util::map_io_error(path, e))?;
|
||
|
||
let mut entries = Vec::new();
|
||
for entry in dir {
|
||
let entry = entry.map_err(|e| util::map_io_error(path, e))?;
|
||
let name = entry.file_name().to_string_lossy().to_string();
|
||
let file_type = entry.file_type().map_err(|e| util::map_io_error(path, e))?;
|
||
let meta = entry.metadata().map_err(|e| util::map_io_error(path, e))?;
|
||
let stat = util::stat_from_metadata(&meta, file_type.is_symlink());
|
||
let long_name = util::build_long_name(&stat, &name);
|
||
|
||
entries.push(VfsDirEntry {
|
||
name,
|
||
long_name,
|
||
stat,
|
||
});
|
||
}
|
||
|
||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||
Ok(entries)
|
||
}
|
||
|
||
fn open_file(&self, path: &Path, flags: &OpenFlags) -> Result<Box<dyn VfsFile>, VfsError> {
|
||
let mut opts = OpenOptions::new();
|
||
opts.read(flags.read);
|
||
opts.write(flags.write);
|
||
opts.append(flags.append);
|
||
opts.create(flags.create);
|
||
opts.truncate(flags.truncate);
|
||
opts.create_new(flags.exclusive);
|
||
|
||
let file = opts.open(path).map_err(|e| util::map_io_error(path, e))?;
|
||
|
||
#[cfg(unix)]
|
||
if flags.create && !flags.exclusive {
|
||
if let Ok(meta) = file.metadata() {
|
||
if flags.mode != 0 && meta.permissions().mode() != flags.mode {
|
||
fs::set_permissions(path, std::fs::Permissions::from_mode(flags.mode)).ok();
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(Box::new(LocalFile { file }))
|
||
}
|
||
|
||
fn stat(&self, path: &Path) -> Result<VfsStat, VfsError> {
|
||
let meta = fs::metadata(path).map_err(|e| util::map_io_error(path, e))?;
|
||
Ok(util::stat_from_metadata(&meta, false))
|
||
}
|
||
|
||
fn lstat(&self, path: &Path) -> Result<VfsStat, VfsError> {
|
||
let meta = fs::symlink_metadata(path).map_err(|e| util::map_io_error(path, e))?;
|
||
let is_symlink = path.is_symlink() || meta.file_type().is_symlink();
|
||
Ok(util::stat_from_metadata(&meta, is_symlink))
|
||
}
|
||
|
||
fn create_dir(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
|
||
fs::create_dir(path).map_err(|e| util::map_io_error(path, e))?;
|
||
|
||
#[cfg(unix)]
|
||
{
|
||
fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
|
||
.map_err(|e| util::map_io_error(path, e))?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn create_dir_all(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
|
||
fs::create_dir_all(path).map_err(|e| util::map_io_error(path, e))?;
|
||
|
||
#[cfg(unix)]
|
||
{
|
||
if mode != 0 {
|
||
fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
|
||
.map_err(|e| util::map_io_error(path, e))?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
|
||
fs::remove_dir(path).map_err(|e| util::map_io_error(path, e))
|
||
}
|
||
|
||
fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
|
||
fs::remove_file(path).map_err(|e| util::map_io_error(path, e))
|
||
}
|
||
|
||
fn rename(&self, from: &Path, to: &Path) -> Result<(), VfsError> {
|
||
fs::rename(from, to).map_err(|e| util::map_io_error(from, e))
|
||
}
|
||
|
||
fn set_stat(&self, path: &Path, stat: &VfsStat) -> Result<(), VfsError> {
|
||
#[cfg(unix)]
|
||
{
|
||
if stat.mode != 0 {
|
||
fs::set_permissions(path, std::fs::Permissions::from_mode(stat.mode))
|
||
.map_err(|e| util::map_io_error(path, e))?;
|
||
}
|
||
}
|
||
|
||
if let (Some(atime), Some(mtime)) = (
|
||
stat.atime.duration_since(std::time::UNIX_EPOCH).ok(),
|
||
stat.mtime.duration_since(std::time::UNIX_EPOCH).ok(),
|
||
) {
|
||
filetime::set_file_times(
|
||
path,
|
||
filetime::FileTime::from_unix_time(atime.as_secs() as i64, 0),
|
||
filetime::FileTime::from_unix_time(mtime.as_secs() as i64, 0),
|
||
)
|
||
.map_err(|e| util::map_io_error(path, e))?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn read_link(&self, path: &Path) -> Result<PathBuf, VfsError> {
|
||
let target = fs::read_link(path).map_err(|e| util::map_io_error(path, e))?;
|
||
Ok(target)
|
||
}
|
||
|
||
fn create_symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError> {
|
||
#[cfg(unix)]
|
||
{
|
||
std::os::unix::fs::symlink(target, link).map_err(|e| util::map_io_error(link, e))?;
|
||
}
|
||
|
||
#[cfg(not(unix))]
|
||
{
|
||
std::os::windows::fs::symlink_file(target, link)
|
||
.map_err(|e| util::map_io_error(link, e))?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn real_path(&self, path: &Path) -> Result<PathBuf, VfsError> {
|
||
let canonical = path
|
||
.canonicalize()
|
||
.map_err(|e| util::map_io_error(path, e))?;
|
||
Ok(canonical)
|
||
}
|
||
|
||
fn exists(&self, path: &Path) -> bool {
|
||
path.exists()
|
||
}
|
||
|
||
fn hard_link(&self, original: &Path, link: &Path) -> Result<(), VfsError> {
|
||
#[cfg(unix)]
|
||
{
|
||
fs::hard_link(original, link).map_err(|e| util::map_io_error(original, e))?;
|
||
}
|
||
|
||
#[cfg(not(unix))]
|
||
{
|
||
return Err(VfsError::Unsupported(
|
||
"hard_link not supported on non-Unix systems".to_string(),
|
||
));
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ===== Snapshot support =====
|
||
|
||
fn create_snapshot(&self, path: &Path, name: &str) -> Result<(), VfsError> {
|
||
let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots");
|
||
fs::create_dir_all(&snapshot_dir).map_err(|e| util::map_io_error(&snapshot_dir, e))?;
|
||
|
||
let snapshot_path = snapshot_dir.join(name);
|
||
if path.is_dir() {
|
||
self.copy_dir_recursive(path, &snapshot_path)?;
|
||
} else {
|
||
fs::copy(path, &snapshot_path).map_err(|e| util::map_io_error(path, e))?;
|
||
}
|
||
|
||
let meta_path = snapshot_path.with_extension("meta");
|
||
let meta = VfsSnapshotMeta {
|
||
name: name.to_string(),
|
||
created: SystemTime::now(),
|
||
source_path: path.to_string_lossy().to_string(),
|
||
};
|
||
let meta_json = serde_json::to_string(&meta)
|
||
.map_err(|e| VfsError::Io(format!("Failed to serialize snapshot meta: {}", e)))?;
|
||
fs::write(&meta_path, meta_json).map_err(|e| util::map_io_error(&meta_path, e))?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn list_snapshots(&self, path: &Path) -> Result<Vec<String>, VfsError> {
|
||
let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots");
|
||
if !snapshot_dir.exists() {
|
||
return Ok(Vec::new());
|
||
}
|
||
|
||
let mut snapshots = Vec::new();
|
||
for entry in fs::read_dir(&snapshot_dir).map_err(|e| util::map_io_error(&snapshot_dir, e))? {
|
||
let entry = entry.map_err(|e| VfsError::Io(e.to_string()))?;
|
||
let name = entry.file_name().to_string_lossy().to_string();
|
||
if !name.ends_with(".meta") && !name.starts_with('.') {
|
||
snapshots.push(name);
|
||
}
|
||
}
|
||
|
||
Ok(snapshots)
|
||
}
|
||
|
||
fn delete_snapshot(&self, path: &Path, name: &str) -> Result<(), VfsError> {
|
||
let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots");
|
||
let snapshot_path = snapshot_dir.join(name);
|
||
let meta_path = snapshot_path.with_extension("meta");
|
||
|
||
if snapshot_path.is_dir() {
|
||
fs::remove_dir_all(&snapshot_path).map_err(|e| util::map_io_error(&snapshot_path, e))?;
|
||
} else {
|
||
fs::remove_file(&snapshot_path).map_err(|e| util::map_io_error(&snapshot_path, e))?;
|
||
}
|
||
|
||
if meta_path.exists() {
|
||
fs::remove_file(&meta_path).map_err(|e| util::map_io_error(&meta_path, e))?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn restore_snapshot(&self, path: &Path, name: &str) -> Result<(), VfsError> {
|
||
let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots");
|
||
let snapshot_path = snapshot_dir.join(name);
|
||
|
||
if !snapshot_path.exists() {
|
||
return Err(VfsError::NotFound(format!("Snapshot '{}' not found", name)));
|
||
}
|
||
|
||
if path.exists() {
|
||
if path.is_dir() {
|
||
fs::remove_dir_all(path).map_err(|e| util::map_io_error(path, e))?;
|
||
} else {
|
||
fs::remove_file(path).map_err(|e| util::map_io_error(path, e))?;
|
||
}
|
||
}
|
||
|
||
if snapshot_path.is_dir() {
|
||
self.copy_dir_recursive(&snapshot_path, path)?;
|
||
} else {
|
||
fs::copy(&snapshot_path, path).map_err(|e| util::map_io_error(&snapshot_path, e))?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn snapshot_info(&self, path: &Path, name: &str) -> Result<VfsSnapshotInfo, VfsError> {
|
||
let snapshot_dir = path.parent().unwrap_or(path).join(".snapshots");
|
||
let meta_path = snapshot_dir.join(format!("{}.meta", name));
|
||
|
||
if !meta_path.exists() {
|
||
return Err(VfsError::NotFound(format!("Snapshot meta '{}' not found", name)));
|
||
}
|
||
|
||
let meta_json = fs::read_to_string(&meta_path).map_err(|e| util::map_io_error(&meta_path, e))?;
|
||
let meta: VfsSnapshotMeta = serde_json::from_str(&meta_json)
|
||
.map_err(|e| VfsError::Io(format!("Failed to parse snapshot meta: {}", e)))?;
|
||
|
||
let snapshot_path = snapshot_dir.join(name);
|
||
let size = self.calculate_size(&snapshot_path)?;
|
||
|
||
Ok(VfsSnapshotInfo {
|
||
name: meta.name,
|
||
created: meta.created,
|
||
size,
|
||
read_only: true,
|
||
})
|
||
}
|
||
|
||
// ===== Quota support =====
|
||
|
||
fn set_quota(&self, path: &Path, quota: &VfsQuota) -> Result<(), VfsError> {
|
||
let meta = VfsQuotaMeta {
|
||
space_limit: quota.space_limit,
|
||
file_limit: quota.file_limit,
|
||
soft_limit: quota.soft_limit,
|
||
grace_period: quota.grace_period,
|
||
user_id: quota.user_id.clone(),
|
||
};
|
||
Self::write_quota_meta(path, &meta)
|
||
}
|
||
|
||
fn get_quota(&self, path: &Path) -> Result<VfsQuota, VfsError> {
|
||
let meta = Self::read_quota_meta(path)?;
|
||
Ok(VfsQuota {
|
||
space_limit: meta.space_limit,
|
||
file_limit: meta.file_limit,
|
||
soft_limit: meta.soft_limit,
|
||
grace_period: meta.grace_period,
|
||
user_id: meta.user_id,
|
||
})
|
||
}
|
||
|
||
fn get_quota_usage(&self, path: &Path) -> Result<VfsQuotaUsage, VfsError> {
|
||
let space_used = self.calculate_size(path)?;
|
||
let files_used = Self::count_files(path)?;
|
||
|
||
let quota = self.get_quota(path)?;
|
||
let over_soft_limit = quota.soft_limit > 0 && space_used >= quota.soft_limit;
|
||
let over_hard_limit = quota.space_limit > 0 && space_used >= quota.space_limit;
|
||
|
||
Ok(VfsQuotaUsage {
|
||
space_used,
|
||
files_used,
|
||
over_soft_limit,
|
||
over_hard_limit,
|
||
})
|
||
}
|
||
|
||
fn check_quota(&self, path: &Path, size: u64) -> Result<bool, VfsError> {
|
||
let quota = self.get_quota(path)?;
|
||
if quota.space_limit == 0 {
|
||
return Ok(true);
|
||
}
|
||
|
||
let usage = self.get_quota_usage(path)?;
|
||
Ok(usage.space_used + size <= quota.space_limit)
|
||
}
|
||
}
|
||
|
||
impl LocalFs {
|
||
fn copy_dir_recursive(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
|
||
fs::create_dir_all(dst).map_err(|e| util::map_io_error(dst, e))?;
|
||
|
||
for entry in fs::read_dir(src).map_err(|e| util::map_io_error(src, e))? {
|
||
let entry = entry.map_err(|e| VfsError::Io(e.to_string()))?;
|
||
let src_path = entry.path();
|
||
let dst_path = dst.join(entry.file_name());
|
||
|
||
if src_path.is_dir() {
|
||
self.copy_dir_recursive(&src_path, &dst_path)?;
|
||
} else {
|
||
fs::copy(&src_path, &dst_path).map_err(|e| util::map_io_error(&src_path, e))?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn calculate_size(&self, path: &Path) -> Result<u64, VfsError> {
|
||
if path.is_dir() {
|
||
let mut total = 0;
|
||
for entry in fs::read_dir(path).map_err(|e| util::map_io_error(path, e))? {
|
||
let entry = entry.map_err(|e| VfsError::Io(e.to_string()))?;
|
||
total += self.calculate_size(&entry.path())?;
|
||
}
|
||
Ok(total)
|
||
} else {
|
||
let meta = path.metadata().map_err(|e| util::map_io_error(path, e))?;
|
||
Ok(meta.len())
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== Quota implementation =====
|
||
|
||
impl LocalFs {
|
||
fn quota_file(path: &Path) -> PathBuf {
|
||
path.join(".quota")
|
||
}
|
||
|
||
fn read_quota_meta(path: &Path) -> Result<VfsQuotaMeta, VfsError> {
|
||
let quota_file = Self::quota_file(path);
|
||
if !quota_file.exists() {
|
||
return Ok(VfsQuotaMeta::default());
|
||
}
|
||
let json = fs::read_to_string("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)
|
||
}
|
||
}
|
||
}
|
||
|
||
#[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,
|
||
}
|