Implement VFS quota support
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- 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.
This commit is contained in:
Warren
2026-06-20 22:17:50 +08:00
parent f016525687
commit 9c44bd5929
2 changed files with 154 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
use super::open_flags::OpenFlags;
use super::util;
use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsSnapshotInfo, VfsStat};
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};
@@ -341,6 +341,56 @@ impl VfsBackend for LocalFs {
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 {
@@ -377,6 +427,59 @@ impl LocalFs {
}
}
// ===== 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)
}
}
}
#[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,

View File

@@ -195,6 +195,28 @@ pub trait VfsBackend: Send + Sync {
fn snapshot_info(&self, _path: &Path, _name: &str) -> Result<VfsSnapshotInfo, VfsError> {
Err(VfsError::Unsupported("snapshot_info".to_string()))
}
// ===== Quota support =====
/// 设置配额限制(字节)
fn set_quota(&self, _path: &Path, _quota: &VfsQuota) -> Result<(), VfsError> {
Err(VfsError::Unsupported("set_quota".to_string()))
}
/// 获取配额信息
fn get_quota(&self, _path: &Path) -> Result<VfsQuota, VfsError> {
Err(VfsError::Unsupported("get_quota".to_string()))
}
/// 获取配额使用情况
fn get_quota_usage(&self, _path: &Path) -> Result<VfsQuotaUsage, VfsError> {
Err(VfsError::Unsupported("get_quota_usage".to_string()))
}
/// 检查配额(写入前检查)
fn check_quota(&self, _path: &Path, _size: u64) -> Result<bool, VfsError> {
Ok(true) // Default: no quota, always allow
}
}
/// 快照信息
@@ -209,3 +231,31 @@ pub struct VfsSnapshotInfo {
/// 是否只读
pub read_only: bool,
}
/// 配额设置
#[derive(Debug, Clone)]
pub struct VfsQuota {
/// 空间限制字节0表示无限制
pub space_limit: u64,
/// 文件数量限制0表示无限制
pub file_limit: u64,
/// 用户ID可选
pub user_id: Option<String>,
/// 软限制(字节),超过时警告
pub soft_limit: u64,
/// 宽限期(秒)
pub grace_period: u64,
}
/// 配额使用情况
#[derive(Debug, Clone)]
pub struct VfsQuotaUsage {
/// 已使用空间(字节)
pub space_used: u64,
/// 文件数量
pub files_used: u64,
/// 是否超过软限制
pub over_soft_limit: bool,
/// 是否超过硬限制
pub over_hard_limit: bool,
}