1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
From 78d2936b67064e3b5e700a2859d00ea3dd6eda4c Mon Sep 17 00:00:00 2001
From: Benedikt Peetz <benedikt.peetz@b-peetz.de>
Date: Sun, 18 May 2025 20:22:04 +0200
Subject: [PATCH 1/3] disk_space: Support btrfs backend
Btrfs is too smart for the statvfs based backend (i.e., only counting
blocks leads to wrong numbers).
For example, a btrfs disk with a lot of de-duplicated blocks (via the copy
on write mechanism) might have a drastically over-reported disk usage.
The btrfs backend is currently implemented by parsing the output of the
`btrfs filesystem usage --raw` command. This is suboptimal, as this now
relies on the command output not changing.
Vendoring the algorithm used internally by the `btrfs` command does not
seem to be a reasonable alternative, considering that the code[1] is
rather complex, low level and would require semi-constant maintenance.
Additionally, the c code would need bindings to be usable from rust.
I assume, that the `btrfs` command output will stay rather similar in
the future, as a lot of tools rely on directly parsing it (see the
various scripts in the issue, this commit fixes).
[1]: https://github.com/kdave/btrfs-progs/blob/eeab081e9d9fbdf4583122ed1caedf541383cf2d/cmds/filesystem-usage.c#L442
Fixes: #1654
---
src/blocks/disk_space.rs | 112 +++++++++++++++++++++++++++++++++++----
1 file changed, 101 insertions(+), 11 deletions(-)
diff --git a/src/blocks/disk_space.rs b/src/blocks/disk_space.rs
index 79bfebd27..da0d3f518 100644
--- a/src/blocks/disk_space.rs
+++ b/src/blocks/disk_space.rs
@@ -12,6 +12,7 @@
//! `alert` | A value which will trigger critical block state | `10.0`
//! `info_type` | Determines which information will affect the block state. Possible values are `"available"`, `"free"` and `"used"` | `"available"`
//! `alert_unit` | The unit of `alert` and `warning` options. If not set, percents are used. Possible values are `"B"`, `"KB"`, `"KiB"`, `"MB"`, `"MiB"`, `"GB"`, `"Gib"`, `"TB"` and `"TiB"` | `None`
+//! `backend` | The backend to use when querying disk usage. Possible values are `"vfs"` (like `du(1)`) and `"btrfs"` | `"vfs"`
//!
//! Placeholder | Value | Type | Unit
//! -------------|--------------------------------------------------------------------|--------|-------
@@ -63,9 +64,12 @@
// make_log_macro!(debug, "disk_space");
+use std::cell::OnceCell;
+
use super::prelude::*;
use crate::formatting::prefix::Prefix;
use nix::sys::statvfs::statvfs;
+use tokio::process::Command;
#[derive(Copy, Clone, Debug, Deserialize, SmartDefault)]
#[serde(rename_all = "lowercase")]
@@ -76,11 +80,20 @@ pub enum InfoType {
Used,
}
+#[derive(Copy, Clone, Debug, Deserialize, SmartDefault)]
+#[serde(rename_all = "lowercase")]
+pub enum Backend {
+ #[default]
+ Vfs,
+ Btrfs,
+}
+
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
#[default("/".into())]
pub path: ShellString,
+ pub backend: Backend,
pub info_type: InfoType,
pub format: FormatConfig,
pub format_alt: Option<FormatConfig>,
@@ -128,17 +141,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
loop {
let mut widget = Widget::new().with_format(format.clone());
- let statvfs = statvfs(&*path).error("failed to retrieve statvfs")?;
-
- // Casting to be compatible with 32-bit systems
- #[allow(clippy::unnecessary_cast)]
- let (total, used, available, free) = {
- let total = (statvfs.blocks() as u64) * (statvfs.fragment_size() as u64);
- let used = ((statvfs.blocks() as u64) - (statvfs.blocks_free() as u64))
- * (statvfs.fragment_size() as u64);
- let available = (statvfs.blocks_available() as u64) * (statvfs.block_size() as u64);
- let free = (statvfs.blocks_free() as u64) * (statvfs.block_size() as u64);
- (total, used, available, free)
+ let (total, used, available, free) = match config.backend {
+ Backend::Vfs => get_vfs(&*path)?,
+ Backend::Btrfs => get_btrfs(&path).await?,
};
let result = match config.info_type {
@@ -205,3 +210,88 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
}
}
}
+
+fn get_vfs<P>(path: &P) -> Result<(u64, u64, u64, u64)>
+where
+ P: ?Sized + nix::NixPath,
+{
+ let statvfs = statvfs(path).error("failed to retrieve statvfs")?;
+
+ // Casting to be compatible with 32-bit systems
+ #[allow(clippy::unnecessary_cast)]
+ {
+ let total = (statvfs.blocks() as u64) * (statvfs.fragment_size() as u64);
+ let used = ((statvfs.blocks() as u64) - (statvfs.blocks_free() as u64))
+ * (statvfs.fragment_size() as u64);
+ let available = (statvfs.blocks_available() as u64) * (statvfs.block_size() as u64);
+ let free = (statvfs.blocks_free() as u64) * (statvfs.block_size() as u64);
+
+ Ok((total, used, available, free))
+ }
+}
+
+async fn get_btrfs(path: &str) -> Result<(u64, u64, u64, u64)> {
+ const OUTPUT_CHANGED: &str = "Btrfs filesystem usage output format changed";
+
+ fn remove_estimate_min(estimate_str: &str) -> Result<&str> {
+ estimate_str.trim_matches('\t')
+ .split_once("\t")
+ .ok_or(Error::new(OUTPUT_CHANGED))
+ .map(|v| v.0)
+ }
+
+ macro_rules! get {
+ ($source:expr, $name:expr, $variable:ident) => {
+ get!(@pre_op (|a| {Ok::<_, Error>(a)}), $source, $name, $variable)
+ };
+ (@pre_op $function:expr, $source:expr, $name:expr, $variable:ident) => {
+ if $source.starts_with(concat!($name, ":")) {
+ let (found_name, variable_str) =
+ $source.split_once(":").ok_or(Error::new(OUTPUT_CHANGED))?;
+
+ let variable_str = $function(variable_str)?;
+
+ debug_assert_eq!(found_name, $name);
+ $variable
+ .set(variable_str.trim().parse().error(OUTPUT_CHANGED)?)
+ .map_err(|_| Error::new(OUTPUT_CHANGED))?;
+ }
+ };
+ }
+
+ let filesystem_usage = Command::new("btrfs")
+ .args(["filesystem", "usage", "--raw", path])
+ .output()
+ .await
+ .error("Failed to collect btrfs filesystem usage info")?
+ .stdout;
+
+ {
+ let final_total = OnceCell::new();
+ let final_used = OnceCell::new();
+ let final_free = OnceCell::new();
+
+ let mut lines = filesystem_usage.lines();
+ while let Some(line) = lines
+ .next_line()
+ .await
+ .error("Failed to read output of btrfs filesystem usage")?
+ {
+ let line = line.trim();
+
+ // See btrfs-filesystem(8) for an explanation for the rows.
+ get!(line, "Device size", final_total);
+ get!(line, "Used", final_used);
+ get!(@pre_op remove_estimate_min, line, "Free (estimated)", final_free);
+ }
+
+ Ok((
+ *final_total.get().ok_or(Error::new(OUTPUT_CHANGED))?,
+ *final_used.get().ok_or(Error::new(OUTPUT_CHANGED))?,
+ // HACK(@bpeetz): We also return the free disk space as the available one, because btrfs
+ // does not tell us which disk space is reserved for the fs. <2025-05-18>
+ *final_free.get().ok_or(Error::new(OUTPUT_CHANGED))?,
+ *final_free.get().ok_or(Error::new(OUTPUT_CHANGED))?,
+ ))
+ }
+}
--
2.49.0
|