diff --git a/crates/degu/src/commands/quota.rs b/crates/degu/src/commands/quota.rs index cbd2d6b..9661182 100644 --- a/crates/degu/src/commands/quota.rs +++ b/crates/degu/src/commands/quota.rs @@ -1,6 +1,4 @@ -mod model; mod output; -mod platform; use crate::cli::QuotaArgs; use crate::commands::next_action::{self, OutputMode, Request, Workflow}; @@ -15,7 +13,7 @@ pub(crate) fn run(args: QuotaArgs, ui: Ui) -> Result<()> { let target_text = escape_terminal_text(&target.display().to_string()); let canonical = std::fs::canonicalize(&target) .with_context(|| format!("quota target is unavailable: {target_text}"))?; - let report = platform::probe(&canonical)?; + let report = crate::quota::probe(&canonical)?; output::print(&report, json, ui)?; next_action::print(Request { output: output_mode(json, ui), diff --git a/crates/degu/src/commands/quota/output.rs b/crates/degu/src/commands/quota/output.rs index 5b9ecf3..f39bfd1 100644 --- a/crates/degu/src/commands/quota/output.rs +++ b/crates/degu/src/commands/quota/output.rs @@ -1,6 +1,6 @@ -use super::model::{QuotaDimension, QuotaGrace, QuotaGraceState, QuotaReport}; use crate::output::stdoutln; use crate::presentation::{escape_terminal_text, human_bytes, ratio_bar}; +use crate::quota::model::{QuotaDimension, QuotaGrace, QuotaGraceState, QuotaSnapshot}; use crate::runtime::{Glyphs, Ui}; use anyhow::Result; use std::cmp::Ordering; @@ -10,7 +10,7 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; const PERCENT_SCALE: f64 = 100.0; const DETAIL_INDENT: usize = 11; -pub(super) fn print(report: &QuotaReport, json: bool, ui: Ui) -> Result<()> { +pub(super) fn print(report: &QuotaSnapshot, json: bool, ui: Ui) -> Result<()> { if json { stdoutln!("{}", serde_json::to_string_pretty(report)?)?; } else { @@ -19,7 +19,7 @@ pub(super) fn print(report: &QuotaReport, json: bool, ui: Ui) -> Result<()> { Ok(()) } -fn render_human(report: &QuotaReport, ui: Ui) -> String { +fn render_human(report: &QuotaSnapshot, ui: Ui) -> String { let glyphs = ui.glyphs; let separator = glyphs.separator; let target = escape_terminal_text(&report.scope.path.display().to_string()); diff --git a/crates/degu/src/commands/quota/output/tests.rs b/crates/degu/src/commands/quota/output/tests.rs index 2df5926..14c2fb3 100644 --- a/crates/degu/src/commands/quota/output/tests.rs +++ b/crates/degu/src/commands/quota/output/tests.rs @@ -1,6 +1,7 @@ use super::{format_count, render_dimension, render_human}; -use crate::commands::quota::model::{ - ActiveQuota, QuotaDimension, QuotaGrace, QuotaGraceState, QuotaLimits, QuotaReport, QuotaScope, +use crate::quota::model::{ + ActiveQuota, QuotaDimension, QuotaGrace, QuotaGraceState, QuotaLimits, QuotaScope, + QuotaSnapshot, }; use crate::runtime::{Glyphs, Ui}; use std::path::PathBuf; @@ -12,7 +13,7 @@ fn quota_human_escapes_scope_and_provider_terminal_controls() { PathBuf::from("/mnt/\tdata"), "ext4\x1b[31m".to_owned(), ); - let report = QuotaReport::active( + let report = QuotaSnapshot::active( scope, 1000, ActiveQuota { @@ -43,7 +44,7 @@ fn quota_human_reflows_fields_for_a_narrow_terminal() { PathBuf::from("/home/user/a-very-long-mount-point"), "ext4".to_owned(), ); - let report = QuotaReport::active( + let report = QuotaSnapshot::active( scope, 1000, ActiveQuota { diff --git a/crates/degu/src/lib.rs b/crates/degu/src/lib.rs index 36e4566..6c67ec9 100644 --- a/crates/degu/src/lib.rs +++ b/crates/degu/src/lib.rs @@ -13,6 +13,7 @@ mod findings_table; mod lifecycle; mod output; mod presentation; +mod quota; mod runtime; mod source_selection; mod value_parser; diff --git a/crates/degu/src/quota.rs b/crates/degu/src/quota.rs new file mode 100644 index 0000000..7335278 --- /dev/null +++ b/crates/degu/src/quota.rs @@ -0,0 +1,12 @@ +pub(crate) mod model; +mod platform; + +use std::path::Path; + +pub(crate) use model::QuotaSnapshot; +pub(crate) use platform::ProbeError; + +/// Read one authoritative quota snapshot for an already-resolved path. +pub(crate) fn probe(path: &Path) -> Result { + platform::probe(path) +} diff --git a/crates/degu/src/commands/quota/model.rs b/crates/degu/src/quota/model.rs similarity index 80% rename from crates/degu/src/commands/quota/model.rs rename to crates/degu/src/quota/model.rs index 83294cc..ad44335 100644 --- a/crates/degu/src/commands/quota/model.rs +++ b/crates/degu/src/quota/model.rs @@ -2,19 +2,19 @@ use serde::Serialize; use std::path::PathBuf; #[derive(Debug, Serialize)] -pub(super) struct QuotaReport { - pub(super) scope: QuotaScope, - pub(super) subject: QuotaSubject, - pub(super) provider: &'static str, - pub(super) data_source: &'static str, - pub(super) state: &'static str, - pub(super) space: QuotaDimension, - pub(super) inodes: QuotaDimension, +pub(crate) struct QuotaSnapshot { + pub(crate) scope: QuotaScope, + pub(crate) subject: QuotaSubject, + pub(crate) provider: &'static str, + pub(crate) data_source: &'static str, + pub(crate) state: &'static str, + pub(crate) space: QuotaDimension, + pub(crate) inodes: QuotaDimension, } #[cfg(any(target_os = "linux", test))] -impl QuotaReport { - pub(super) fn active(scope: QuotaScope, subject_id: u32, quota: ActiveQuota) -> Self { +impl QuotaSnapshot { + pub(crate) fn active(scope: QuotaScope, subject_id: u32, quota: ActiveQuota) -> Self { Self { scope, subject: QuotaSubject::user(subject_id), @@ -28,23 +28,23 @@ impl QuotaReport { } #[cfg(any(target_os = "linux", test))] -pub(super) struct ActiveQuota { - pub(super) provider: &'static str, - pub(super) data_source: &'static str, - pub(super) space: QuotaDimension, - pub(super) inodes: QuotaDimension, +pub(crate) struct ActiveQuota { + pub(crate) provider: &'static str, + pub(crate) data_source: &'static str, + pub(crate) space: QuotaDimension, + pub(crate) inodes: QuotaDimension, } #[derive(Debug, Serialize)] -pub(super) struct QuotaScope { - pub(super) path: PathBuf, - pub(super) mount_point: PathBuf, - pub(super) filesystem: String, +pub(crate) struct QuotaScope { + pub(crate) path: PathBuf, + pub(crate) mount_point: PathBuf, + pub(crate) filesystem: String, } #[cfg(any(target_os = "linux", test))] impl QuotaScope { - pub(super) fn new(path: PathBuf, mount_point: PathBuf, filesystem: String) -> Self { + pub(crate) fn new(path: PathBuf, mount_point: PathBuf, filesystem: String) -> Self { Self { path, mount_point, @@ -54,9 +54,9 @@ impl QuotaScope { } #[derive(Debug, Serialize)] -pub(super) struct QuotaSubject { - pub(super) kind: &'static str, - pub(super) id: u32, +pub(crate) struct QuotaSubject { + pub(crate) kind: &'static str, + pub(crate) id: u32, } #[cfg(any(target_os = "linux", test))] @@ -67,18 +67,18 @@ impl QuotaSubject { } #[derive(Debug, Serialize)] -pub(super) struct QuotaDimension { - pub(super) used: u64, - pub(super) soft_limit: Option, - pub(super) hard_limit: Option, - pub(super) headroom_to_soft_limit: Option, - pub(super) headroom_to_hard_limit: Option, - pub(super) grace: Option, +pub(crate) struct QuotaDimension { + pub(crate) used: u64, + pub(crate) soft_limit: Option, + pub(crate) hard_limit: Option, + pub(crate) headroom_to_soft_limit: Option, + pub(crate) headroom_to_hard_limit: Option, + pub(crate) grace: Option, } #[cfg(any(target_os = "linux", test))] impl QuotaDimension { - pub(super) fn new(used: u64, limits: QuotaLimits, grace: Option) -> Self { + pub(crate) fn new(used: u64, limits: QuotaLimits, grace: Option) -> Self { let soft_limit = nonzero_limit(limits.soft); let hard_limit = nonzero_limit(limits.hard); Self { @@ -93,18 +93,18 @@ impl QuotaDimension { } #[derive(Debug, Serialize)] -pub(super) struct QuotaGrace { - pub(super) state: QuotaGraceState, +pub(crate) struct QuotaGrace { + pub(crate) state: QuotaGraceState, /// `None` means the provider reported an expired grace period without a /// deadline (Lustre prints `none` or `expired`); degu never synthesizes /// one. Serialized as an explicit `null`: the key is part of the frozen /// JSON contract and must never be omitted. - pub(super) expires_at_unix: Option, + pub(crate) expires_at_unix: Option, } #[cfg(any(target_os = "linux", test))] impl QuotaGrace { - pub(super) fn from_kernel_deadline(deadline: u64, observed_at_unix: u64) -> Option { + pub(crate) fn from_kernel_deadline(deadline: u64, observed_at_unix: u64) -> Option { nonzero_limit(deadline).map(|expires_at_unix| Self { state: if expires_at_unix > observed_at_unix { QuotaGraceState::Active @@ -118,7 +118,7 @@ impl QuotaGrace { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] -pub(super) enum QuotaGraceState { +pub(crate) enum QuotaGraceState { #[cfg_attr(all(not(target_os = "linux"), not(test)), allow(dead_code))] Active, #[cfg_attr(all(not(target_os = "linux"), not(test)), allow(dead_code))] @@ -126,14 +126,14 @@ pub(super) enum QuotaGraceState { } #[cfg(any(target_os = "linux", test))] -pub(super) struct QuotaLimits { - pub(super) soft: u64, - pub(super) hard: u64, +pub(crate) struct QuotaLimits { + pub(crate) soft: u64, + pub(crate) hard: u64, } #[cfg(any(target_os = "linux", test))] impl QuotaLimits { - pub(super) fn new(soft: u64, hard: u64) -> Self { + pub(crate) fn new(soft: u64, hard: u64) -> Self { Self { soft, hard } } } @@ -151,8 +151,8 @@ fn headroom(limit: Option, used: u64) -> Option { #[cfg(test)] mod tests { use super::{ - ActiveQuota, QuotaDimension, QuotaGrace, QuotaGraceState, QuotaLimits, QuotaReport, - QuotaScope, + ActiveQuota, QuotaDimension, QuotaGrace, QuotaGraceState, QuotaLimits, QuotaScope, + QuotaSnapshot, }; use std::path::PathBuf; @@ -235,7 +235,7 @@ mod tests { PathBuf::from("/home"), "ext4".to_owned(), ); - let report = QuotaReport::active( + let report = QuotaSnapshot::active( scope, 1000, ActiveQuota { diff --git a/crates/degu/src/commands/quota/platform.rs b/crates/degu/src/quota/platform.rs similarity index 95% rename from crates/degu/src/commands/quota/platform.rs rename to crates/degu/src/quota/platform.rs index 1983c4d..0dc5430 100644 --- a/crates/degu/src/commands/quota/platform.rs +++ b/crates/degu/src/quota/platform.rs @@ -3,9 +3,9 @@ mod linux; #[cfg(target_os = "macos")] mod macos; -use super::model::QuotaReport; #[cfg(target_os = "linux")] use super::model::QuotaScope; +use super::model::QuotaSnapshot; use crate::presentation::escape_terminal_text as escaped; use std::fmt; use std::path::Path; @@ -29,7 +29,7 @@ impl MountInfo { } #[derive(Debug)] -pub(super) enum ProbeError { +pub(crate) enum ProbeError { #[cfg(target_os = "linux")] NotConfigured { filesystem: String, @@ -161,17 +161,17 @@ fn failure_fields(error: &ProbeError) -> (&'static str, &str, &str, &str) { } #[cfg(target_os = "linux")] -pub(super) fn probe(path: &Path) -> Result { +pub(super) fn probe(path: &Path) -> Result { linux::probe(path) } #[cfg(target_os = "macos")] -pub(super) fn probe(path: &Path) -> Result { +pub(super) fn probe(path: &Path) -> Result { macos::probe(path) } #[cfg(not(any(target_os = "linux", target_os = "macos")))] -pub(super) fn probe(path: &Path) -> Result { +pub(super) fn probe(path: &Path) -> Result { Err(ProbeError::Unsupported { filesystem: "unknown".to_owned(), mount_point: path.display().to_string(), diff --git a/crates/degu/src/commands/quota/platform/linux.rs b/crates/degu/src/quota/platform/linux.rs similarity index 97% rename from crates/degu/src/commands/quota/platform/linux.rs rename to crates/degu/src/quota/platform/linux.rs index f124adf..9468d14 100644 --- a/crates/degu/src/commands/quota/platform/linux.rs +++ b/crates/degu/src/quota/platform/linux.rs @@ -1,9 +1,7 @@ mod lustre; use super::{MountInfo, ProbeError}; -use crate::commands::quota::model::{ - ActiveQuota, QuotaDimension, QuotaGrace, QuotaLimits, QuotaReport, -}; +use crate::quota::model::{ActiveQuota, QuotaDimension, QuotaGrace, QuotaLimits, QuotaSnapshot}; use std::ffi::{CString, OsString}; use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::path::{Path, PathBuf}; @@ -25,7 +23,7 @@ struct QueryResult { inodes: QuotaDimension, } -pub(super) fn probe(path: &Path) -> Result { +pub(super) fn probe(path: &Path) -> Result { let mount = inspect_mount(path)?; // SAFETY: geteuid has no preconditions and does not mutate process state. let subject_id = unsafe { libc::geteuid() }; @@ -36,10 +34,10 @@ pub(super) fn probe(path: &Path) -> Result { } } -fn probe_vfs(mount: MountInfo, path: &Path, subject_id: u32) -> Result { +fn probe_vfs(mount: MountInfo, path: &Path, subject_id: u32) -> Result { let result = query_current_user(&mount, subject_id)?; let scope = mount.scope(path); - Ok(QuotaReport::active( + Ok(QuotaSnapshot::active( scope, subject_id, ActiveQuota { @@ -225,7 +223,7 @@ mod tests { use super::{ MountInfo, ProbeError, QueryResult, classify_error, incomplete, normalize, parse_mountinfo, }; - use crate::commands::quota::model::QuotaGraceState; + use crate::quota::model::QuotaGraceState; use std::path::{Path, PathBuf}; #[test] diff --git a/crates/degu/src/commands/quota/platform/linux/lustre.rs b/crates/degu/src/quota/platform/linux/lustre.rs similarity index 99% rename from crates/degu/src/commands/quota/platform/linux/lustre.rs rename to crates/degu/src/quota/platform/linux/lustre.rs index 76cfc5c..d2df2b9 100644 --- a/crates/degu/src/commands/quota/platform/linux/lustre.rs +++ b/crates/degu/src/quota/platform/linux/lustre.rs @@ -18,8 +18,8 @@ //! a number it could not verify. use super::{MountInfo, ProbeError}; -use crate::commands::quota::model::{ - ActiveQuota, QuotaDimension, QuotaGrace, QuotaGraceState, QuotaLimits, QuotaReport, +use crate::quota::model::{ + ActiveQuota, QuotaDimension, QuotaGrace, QuotaGraceState, QuotaLimits, QuotaSnapshot, }; use std::io::Read; use std::os::unix::ffi::OsStrExt; @@ -116,7 +116,7 @@ pub(super) fn probe( mount: MountInfo, path: &Path, subject_id: u32, -) -> Result { +) -> Result { require_rooted_mount_point(&mount)?; verify_statfs_is_lustre(&mount)?; let execution = capture(Path::new(LFS_BINARY), &mount, subject_id, LFS_TIMEOUT)?; @@ -128,7 +128,7 @@ pub(super) fn probe( let parsed = parse(&stdout, &mount.mount_point, subject_id, observed_at_unix) .map_err(|reason| super::incomplete(&mount, reason))?; let scope = mount.scope(path); - Ok(QuotaReport::active( + Ok(QuotaSnapshot::active( scope, subject_id, ActiveQuota { diff --git a/crates/degu/src/commands/quota/platform/macos.rs b/crates/degu/src/quota/platform/macos.rs similarity index 93% rename from crates/degu/src/commands/quota/platform/macos.rs rename to crates/degu/src/quota/platform/macos.rs index 91a9fd3..002ee4f 100644 --- a/crates/degu/src/commands/quota/platform/macos.rs +++ b/crates/degu/src/quota/platform/macos.rs @@ -1,11 +1,11 @@ use super::{MountInfo, ProbeError}; -use crate::commands::quota::model::QuotaReport; +use crate::quota::model::QuotaSnapshot; use std::ffi::CString; use std::mem::MaybeUninit; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; -pub(super) fn probe(path: &Path) -> Result { +pub(super) fn probe(path: &Path) -> Result { let mount = inspect_mount(path)?; Err(ProbeError::Unsupported { filesystem: mount.filesystem,