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)
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}