diff --git a/markbase-core/src/vfs/local_fs.rs b/markbase-core/src/vfs/local_fs.rs index c081cd9..4da7b01 100644 --- a/markbase-core/src/vfs/local_fs.rs +++ b/markbase-core/src/vfs/local_fs.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct VfsSnapshotMeta { name: String, diff --git a/markbase-core/src/vfs/mod.rs b/markbase-core/src/vfs/mod.rs index 44da662..b233717 100644 --- a/markbase-core/src/vfs/mod.rs +++ b/markbase-core/src/vfs/mod.rs @@ -195,6 +195,28 @@ pub trait VfsBackend: Send + Sync { fn snapshot_info(&self, _path: &Path, _name: &str) -> Result { 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 { + Err(VfsError::Unsupported("get_quota".to_string())) + } + + /// 获取配额使用情况 + fn get_quota_usage(&self, _path: &Path) -> Result { + Err(VfsError::Unsupported("get_quota_usage".to_string())) + } + + /// 检查配额(写入前检查) + fn check_quota(&self, _path: &Path, _size: u64) -> Result { + 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, + /// 软限制(字节),超过时警告 + 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, +}