Implement VFS quota support
- 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:
@@ -1,6 +1,6 @@
|
|||||||
use super::open_flags::OpenFlags;
|
use super::open_flags::OpenFlags;
|
||||||
use super::util;
|
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::fs::{self, File, OpenOptions};
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
@@ -341,6 +341,56 @@ impl VfsBackend for LocalFs {
|
|||||||
read_only: true,
|
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 {
|
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("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)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
struct VfsSnapshotMeta {
|
struct VfsSnapshotMeta {
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
@@ -195,6 +195,28 @@ pub trait VfsBackend: Send + Sync {
|
|||||||
fn snapshot_info(&self, _path: &Path, _name: &str) -> Result<VfsSnapshotInfo, VfsError> {
|
fn snapshot_info(&self, _path: &Path, _name: &str) -> Result<VfsSnapshotInfo, VfsError> {
|
||||||
Err(VfsError::Unsupported("snapshot_info".to_string()))
|
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,
|
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,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user