Skip to main content

btrfs_cli/filesystem/
show.rs

1use 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/// Show information about one or more mounted or unmounted filesystems
17#[derive(Parser, Debug)]
18pub struct FilesystemShowCommand {
19    /// Search all devices, including unmounted ones
20    #[clap(long, short = 'd')]
21    pub all_devices: bool,
22
23    /// Search only mounted filesystems
24    #[clap(long, short)]
25    pub mounted: bool,
26
27    #[clap(flatten)]
28    pub units: UnitMode,
29
30    /// Path, UUID, device or label to show (shows all if omitted)
31    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).map_or(0, |entries| {
127                entries.iter().map(|e| e.used_bytes).sum::<u64>()
128            });
129
130            entries.push(FsEntry {
131                label,
132                uuid: info.uuid.as_hyphenated().to_string(),
133                num_devices: info.num_devices,
134                used_bytes,
135                devices: devices
136                    .iter()
137                    .map(|d| DevEntry {
138                        devid: d.devid,
139                        total_bytes: d.total_bytes,
140                        bytes_used: d.bytes_used,
141                        path: d.path.clone(),
142                    })
143                    .collect(),
144            });
145        }
146
147        Ok(entries)
148    }
149}
150
151fn print_text(entries: &[FsEntry], mode: &SizeFormat) {
152    for (i, entry) in entries.iter().enumerate() {
153        if i > 0 {
154            println!();
155        }
156
157        if entry.label.is_empty() {
158            print!("Label: none ");
159        } else {
160            print!("Label: '{}' ", entry.label);
161        }
162        println!(" uuid: {}", entry.uuid);
163        println!(
164            "\tTotal devices {} FS bytes used {}",
165            entry.num_devices,
166            fmt_size(entry.used_bytes, mode)
167        );
168
169        for dev in &entry.devices {
170            println!(
171                "\tdevid {:4} size {} used {} path {}",
172                dev.devid,
173                fmt_size(dev.total_bytes, mode),
174                fmt_size(dev.bytes_used, mode),
175                dev.path,
176            );
177        }
178    }
179}
180
181fn print_modern(entries: &[FsEntry], mode: &SizeFormat) {
182    for (i, entry) in entries.iter().enumerate() {
183        if i > 0 {
184            println!();
185        }
186
187        if entry.label.is_empty() {
188            println!("Label: none");
189        } else {
190            println!("Label: {}", entry.label);
191        }
192        println!("UUID:  {}", entry.uuid);
193        println!(
194            "Total: {} {}, {} used",
195            entry.num_devices,
196            if entry.num_devices == 1 {
197                "device"
198            } else {
199                "devices"
200            },
201            fmt_size(entry.used_bytes, mode)
202        );
203        println!();
204
205        let rows: Vec<DevRow> = entry
206            .devices
207            .iter()
208            .map(|d| DevRow {
209                devid: d.devid,
210                size: fmt_size(d.total_bytes, mode),
211                used: fmt_size(d.bytes_used, mode),
212                path: d.path.clone(),
213            })
214            .collect();
215        let mut out = std::io::stdout().lock();
216        let _ = DevRow::print_table(&rows, &mut out);
217    }
218}
219
220fn parse_btrfs_mounts() -> Result<Vec<String>> {
221    let contents = std::fs::read_to_string("/proc/self/mounts")
222        .context("failed to read /proc/self/mounts")?;
223    let mounts = contents
224        .lines()
225        .filter_map(|line| {
226            let mut fields = line.splitn(6, ' ');
227            let _device = fields.next()?;
228            let mountpoint = fields.next()?;
229            let fstype = fields.next()?;
230            if fstype == "btrfs" {
231                Some(mountpoint.to_owned())
232            } else {
233                None
234            }
235        })
236        .collect();
237    Ok(mounts)
238}