btrfs_cli/filesystem/
show.rs1use super::UnitMode;
2use crate::{
3 Format, RunContext, Runnable,
4 util::{SizeFormat, fmt_size},
5};
6use anyhow::{Context, Result};
7use btrfs_uapi::{
8 device::device_info_all,
9 filesystem::{filesystem_info, label_get},
10 space::space_info,
11};
12use clap::Parser;
13use cols::Cols;
14use std::{collections::HashSet, fs::File, os::unix::io::AsFd};
15
16#[derive(Parser, Debug)]
18pub struct FilesystemShowCommand {
19 #[clap(long, short = 'd')]
21 pub all_devices: bool,
22
23 #[clap(long, short)]
25 pub mounted: bool,
26
27 #[clap(flatten)]
28 pub units: UnitMode,
29
30 pub filter: Option<String>,
32}
33
34struct FsEntry {
35 label: String,
36 uuid: String,
37 num_devices: u64,
38 used_bytes: u64,
39 devices: Vec<DevEntry>,
40}
41
42struct DevEntry {
43 devid: u64,
44 total_bytes: u64,
45 bytes_used: u64,
46 path: String,
47}
48
49#[derive(Cols)]
50struct DevRow {
51 #[column(header = "DEVID", right)]
52 devid: u64,
53 #[column(header = "SIZE", right)]
54 size: String,
55 #[column(header = "USED", right)]
56 used: String,
57 #[column(header = "PATH")]
58 path: String,
59}
60
61impl Runnable for FilesystemShowCommand {
62 fn run(&self, ctx: &RunContext) -> Result<()> {
63 if self.all_devices {
64 anyhow::bail!("--all-devices is not yet implemented");
65 }
66
67 let mode = self.units.resolve();
68 let entries = self.collect_entries()?;
69
70 if entries.is_empty() {
71 println!("No btrfs filesystem found.");
72 return Ok(());
73 }
74
75 match ctx.format {
76 Format::Modern => print_modern(&entries, &mode),
77 Format::Text | Format::Json => print_text(&entries, &mode),
78 }
79
80 Ok(())
81 }
82}
83
84impl FilesystemShowCommand {
85 fn collect_entries(&self) -> Result<Vec<FsEntry>> {
86 let mounts =
87 parse_btrfs_mounts().context("failed to read /proc/self/mounts")?;
88
89 let mut entries = Vec::new();
90 let mut seen_uuids = HashSet::new();
91
92 for mount in &mounts {
93 let Ok(file) = File::open(mount) else {
94 continue;
95 };
96 let fd = file.as_fd();
97
98 let Ok(info) = filesystem_info(fd) else {
99 continue;
100 };
101
102 if let Some(filter) = &self.filter {
103 let uuid_str = info.uuid.as_hyphenated().to_string();
104 let label = label_get(fd).unwrap_or_default();
105 let label_str = label.to_string_lossy();
106 if mount != filter
107 && uuid_str != *filter
108 && label_str != filter.as_str()
109 {
110 continue;
111 }
112 }
113
114 if !seen_uuids.insert(info.uuid) {
115 continue;
116 }
117
118 let label = label_get(fd)
119 .map(|l| l.to_string_lossy().into_owned())
120 .unwrap_or_default();
121
122 let devices = device_info_all(fd, &info).with_context(|| {
123 format!("failed to get device info for '{mount}'")
124 })?;
125
126 let used_bytes = space_info(fd)
127 .map(|entries| {
128 entries.iter().map(|e| e.used_bytes).sum::<u64>()
129 })
130 .unwrap_or(0);
131
132 entries.push(FsEntry {
133 label,
134 uuid: info.uuid.as_hyphenated().to_string(),
135 num_devices: info.num_devices,
136 used_bytes,
137 devices: devices
138 .iter()
139 .map(|d| DevEntry {
140 devid: d.devid,
141 total_bytes: d.total_bytes,
142 bytes_used: d.bytes_used,
143 path: d.path.clone(),
144 })
145 .collect(),
146 });
147 }
148
149 Ok(entries)
150 }
151}
152
153fn print_text(entries: &[FsEntry], mode: &SizeFormat) {
154 for (i, entry) in entries.iter().enumerate() {
155 if i > 0 {
156 println!();
157 }
158
159 if entry.label.is_empty() {
160 print!("Label: none ");
161 } else {
162 print!("Label: '{}' ", entry.label);
163 }
164 println!(" uuid: {}", entry.uuid);
165 println!(
166 "\tTotal devices {} FS bytes used {}",
167 entry.num_devices,
168 fmt_size(entry.used_bytes, mode)
169 );
170
171 for dev in &entry.devices {
172 println!(
173 "\tdevid {:4} size {} used {} path {}",
174 dev.devid,
175 fmt_size(dev.total_bytes, mode),
176 fmt_size(dev.bytes_used, mode),
177 dev.path,
178 );
179 }
180 }
181}
182
183fn print_modern(entries: &[FsEntry], mode: &SizeFormat) {
184 for (i, entry) in entries.iter().enumerate() {
185 if i > 0 {
186 println!();
187 }
188
189 if entry.label.is_empty() {
190 println!("Label: none");
191 } else {
192 println!("Label: {}", entry.label);
193 }
194 println!("UUID: {}", entry.uuid);
195 println!(
196 "Total: {} {}, {} used",
197 entry.num_devices,
198 if entry.num_devices == 1 {
199 "device"
200 } else {
201 "devices"
202 },
203 fmt_size(entry.used_bytes, mode)
204 );
205 println!();
206
207 let rows: Vec<DevRow> = entry
208 .devices
209 .iter()
210 .map(|d| DevRow {
211 devid: d.devid,
212 size: fmt_size(d.total_bytes, mode),
213 used: fmt_size(d.bytes_used, mode),
214 path: d.path.clone(),
215 })
216 .collect();
217 let mut out = std::io::stdout().lock();
218 let _ = DevRow::print_table(&rows, &mut out);
219 }
220}
221
222fn parse_btrfs_mounts() -> Result<Vec<String>> {
223 let contents = std::fs::read_to_string("/proc/self/mounts")
224 .context("failed to read /proc/self/mounts")?;
225 let mounts = contents
226 .lines()
227 .filter_map(|line| {
228 let mut fields = line.splitn(6, ' ');
229 let _device = fields.next()?;
230 let mountpoint = fields.next()?;
231 let fstype = fields.next()?;
232 if fstype == "btrfs" {
233 Some(mountpoint.to_owned())
234 } else {
235 None
236 }
237 })
238 .collect();
239 Ok(mounts)
240}