Files
markbase/markbase-core/src/vfs/local_fs.rs
Warren 9c44bd5929
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
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.
2026-06-20 22:17:50 +08:00

489 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(&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,
created: SystemTime,
source_path: String,
}