use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::store::{reset_usage, set_usage};
use super::types::{QuotaError, QuotaKey, QuotaResult, QuotaType, QuotaUsage};
#[derive(Debug, Clone, Default)]
pub struct ScanResult {
pub dataset: String,
pub files_scanned: u64,
pub dirs_scanned: u64,
pub total_bytes: u64,
pub users_found: u32,
pub groups_found: u32,
pub duration_ns: u64,
pub errors: Vec<String>,
}
impl ScanResult {
pub fn new(dataset: &str) -> Self {
Self {
dataset: dataset.to_string(),
..Default::default()
}
}
pub fn add_error(&mut self, msg: String) {
self.errors.push(msg);
}
pub fn is_ok(&self) -> bool {
self.errors.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct FileInfo {
pub path: String,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub is_dir: bool,
pub project_id: Option<u32>,
}
impl FileInfo {
pub fn new(path: &str, uid: u32, gid: u32, size: u64, is_dir: bool) -> Self {
Self {
path: path.to_string(),
uid,
gid,
size,
is_dir,
project_id: None,
}
}
pub fn with_project(mut self, project_id: u32) -> Self {
self.project_id = Some(project_id);
self
}
}
pub trait QuotaScanner: Send + Sync {
fn scan(&self, dataset: &str) -> QuotaResult<Vec<FileInfo>>;
}
#[derive(Debug, Default)]
pub struct MemoryScanner {
files: BTreeMap<String, Vec<FileInfo>>,
}
impl MemoryScanner {
pub fn new() -> Self {
Self::default()
}
pub fn add_files(&mut self, dataset: &str, files: Vec<FileInfo>) {
self.files.insert(dataset.to_string(), files);
}
pub fn clear(&mut self) {
self.files.clear();
}
}
impl QuotaScanner for MemoryScanner {
fn scan(&self, dataset: &str) -> QuotaResult<Vec<FileInfo>> {
Ok(self.files.get(dataset).cloned().unwrap_or_default())
}
}
pub fn rescan<S: QuotaScanner>(dataset: &str, scanner: &S) -> QuotaResult<ScanResult> {
let start = 0u64; let mut result = ScanResult::new(dataset);
reset_usage(dataset)?;
let files = scanner.scan(dataset)?;
let mut user_usage: BTreeMap<u32, QuotaUsage> = BTreeMap::new();
let mut group_usage: BTreeMap<u32, QuotaUsage> = BTreeMap::new();
let mut project_usage: BTreeMap<u32, QuotaUsage> = BTreeMap::new();
for file in &files {
if file.is_dir {
result.dirs_scanned += 1;
} else {
result.files_scanned += 1;
}
result.total_bytes += file.size;
let user = user_usage.entry(file.uid).or_default();
user.add_bytes(file.size);
user.add_inode();
let group = group_usage.entry(file.gid).or_default();
group.add_bytes(file.size);
group.add_inode();
if let Some(pid) = file.project_id {
let project = project_usage.entry(pid).or_default();
project.add_bytes(file.size);
project.add_inode();
}
}
result.users_found = user_usage.len() as u32;
result.groups_found = group_usage.len() as u32;
for (uid, usage) in user_usage {
if let Err(e) = set_usage(dataset, QuotaKey::user(uid), usage) {
result.add_error(alloc::format!("failed to set user {} usage: {}", uid, e));
}
}
for (gid, usage) in group_usage {
if let Err(e) = set_usage(dataset, QuotaKey::group(gid), usage) {
result.add_error(alloc::format!("failed to set group {} usage: {}", gid, e));
}
}
for (pid, usage) in project_usage {
if let Err(e) = set_usage(dataset, QuotaKey::project(pid), usage) {
result.add_error(alloc::format!("failed to set project {} usage: {}", pid, e));
}
}
result.duration_ns = 0u64.saturating_sub(start);
Ok(result)
}
pub fn rescan_user<S: QuotaScanner>(
dataset: &str,
uid: u32,
scanner: &S,
) -> QuotaResult<QuotaUsage> {
let files = scanner.scan(dataset)?;
let mut usage = QuotaUsage::zero();
for file in files {
if file.uid == uid {
usage.add_bytes(file.size);
usage.add_inode();
}
}
set_usage(dataset, QuotaKey::user(uid), usage)?;
Ok(usage)
}
pub fn rescan_group<S: QuotaScanner>(
dataset: &str,
gid: u32,
scanner: &S,
) -> QuotaResult<QuotaUsage> {
let files = scanner.scan(dataset)?;
let mut usage = QuotaUsage::zero();
for file in files {
if file.gid == gid {
usage.add_bytes(file.size);
usage.add_inode();
}
}
set_usage(dataset, QuotaKey::group(gid), usage)?;
Ok(usage)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quota::store::clear_quotas;
use alloc::vec;
fn clean_test_dataset(name: &str) {
let _ = clear_quotas(name);
}
#[test]
fn test_file_info() {
let file = FileInfo::new("/test/file.txt", 1000, 100, 4096, false);
assert_eq!(file.uid, 1000);
assert_eq!(file.gid, 100);
assert_eq!(file.size, 4096);
assert!(!file.is_dir);
}
#[test]
fn test_file_info_with_project() {
let file = FileInfo::new("/test/file.txt", 1000, 100, 4096, false).with_project(42);
assert_eq!(file.project_id, Some(42));
}
#[test]
fn test_memory_scanner() {
let mut scanner = MemoryScanner::new();
scanner.add_files(
"test",
vec![
FileInfo::new("/a", 1, 1, 100, false),
FileInfo::new("/b", 1, 1, 200, false),
],
);
let files = scanner.scan("test").unwrap();
assert_eq!(files.len(), 2);
let empty = scanner.scan("nonexistent").unwrap();
assert!(empty.is_empty());
}
#[test]
fn test_rescan() {
let dataset = "test_rescan";
clean_test_dataset(dataset);
let mut scanner = MemoryScanner::new();
scanner.add_files(
dataset,
vec![
FileInfo::new("/home/user1/file1", 1000, 100, 1000, false),
FileInfo::new("/home/user1/file2", 1000, 100, 2000, false),
FileInfo::new("/home/user2/file1", 1001, 100, 500, false),
FileInfo::new("/shared", 1000, 200, 1500, true),
],
);
let result = rescan(dataset, &scanner).unwrap();
assert_eq!(result.files_scanned, 3);
assert_eq!(result.dirs_scanned, 1);
assert_eq!(result.total_bytes, 5000);
assert_eq!(result.users_found, 2);
assert!(result.is_ok());
clean_test_dataset(dataset);
}
#[test]
fn test_rescan_user() {
let dataset = "test_rescan_user";
clean_test_dataset(dataset);
let mut scanner = MemoryScanner::new();
scanner.add_files(
dataset,
vec![
FileInfo::new("/user1/a", 1000, 100, 1000, false),
FileInfo::new("/user1/b", 1000, 100, 2000, false),
FileInfo::new("/user2/a", 1001, 100, 500, false),
],
);
let usage = rescan_user(dataset, 1000, &scanner).unwrap();
assert_eq!(usage.bytes_used, 3000);
assert_eq!(usage.inodes_used, 2);
clean_test_dataset(dataset);
}
#[test]
fn test_rescan_group() {
let dataset = "test_rescan_group";
clean_test_dataset(dataset);
let mut scanner = MemoryScanner::new();
scanner.add_files(
dataset,
vec![
FileInfo::new("/a", 1000, 100, 1000, false),
FileInfo::new("/b", 1001, 100, 2000, false),
FileInfo::new("/c", 1002, 200, 500, false),
],
);
let usage = rescan_group(dataset, 100, &scanner).unwrap();
assert_eq!(usage.bytes_used, 3000);
assert_eq!(usage.inodes_used, 2);
clean_test_dataset(dataset);
}
#[test]
fn test_scan_result() {
let mut result = ScanResult::new("test");
result.files_scanned = 100;
result.total_bytes = 1_000_000;
assert!(result.is_ok());
result.add_error("test error".into());
assert!(!result.is_ok());
}
}