Skip to main content

btrfs_cli/subvolume/
show.rs

1use crate::{
2    Format, RunContext, Runnable,
3    filesystem::UnitMode,
4    util::{
5        ParsedUuid, SizeFormat, fmt_size, format_time, open_path, print_json,
6    },
7};
8use anyhow::{Context, Result};
9use btrfs_uapi::{
10    quota::{self, QgroupInfo},
11    send_receive::subvolume_search_by_uuid,
12    subvolume::{SubvolumeInfo, subvolume_info, subvolume_info_by_id},
13};
14use clap::Parser;
15use serde::Serialize;
16use std::{os::unix::io::AsFd, path::PathBuf, time::SystemTime};
17
18/// Show detailed information about a subvolume
19///
20/// Displays UUIDs, generation numbers, creation time, flags, and send/receive
21/// transaction IDs for the subvolume that contains the given path.
22///
23/// The subvolume can be specified by path (default), or by root id or UUID
24/// that are looked up relative to the given path.
25#[derive(Parser, Debug)]
26pub struct SubvolumeShowCommand {
27    /// Look up subvolume by its root ID instead of path
28    #[clap(short = 'r', long = "rootid")]
29    pub rootid: Option<u64>,
30
31    /// Look up subvolume by its UUID instead of path
32    #[clap(short = 'u', long = "uuid", conflicts_with = "rootid")]
33    pub uuid: Option<ParsedUuid>,
34
35    #[clap(flatten)]
36    pub units: UnitMode,
37
38    /// Path to a subvolume or any file within it
39    pub path: PathBuf,
40}
41
42#[derive(Serialize)]
43struct SubvolShowJson {
44    name: String,
45    uuid: String,
46    parent_uuid: String,
47    received_uuid: String,
48    creation_time: String,
49    subvolume_id: u64,
50    generation: u64,
51    gen_at_creation: u64,
52    parent_id: u64,
53    flags: String,
54    send_transid: u64,
55    send_time: String,
56    receive_transid: u64,
57    receive_time: String,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    quota: Option<QuotaJson>,
60}
61
62#[derive(Serialize)]
63struct QuotaJson {
64    qgroupid: String,
65    limit_referenced: Option<u64>,
66    limit_exclusive: Option<u64>,
67    usage_referenced: u64,
68    usage_exclusive: u64,
69}
70
71fn format_time_for_json(t: SystemTime) -> String {
72    format_time(t)
73}
74
75fn subvol_to_json(
76    info: &SubvolumeInfo,
77    qg: Option<&QgroupInfo>,
78) -> SubvolShowJson {
79    SubvolShowJson {
80        name: info.name.clone(),
81        uuid: format_uuid(&info.uuid),
82        parent_uuid: format_uuid(&info.parent_uuid),
83        received_uuid: format_uuid(&info.received_uuid),
84        creation_time: format_time_for_json(info.otime),
85        subvolume_id: info.id,
86        generation: info.generation,
87        gen_at_creation: info.otransid,
88        parent_id: info.parent_id,
89        flags: info.flags.to_string(),
90        send_transid: info.stransid,
91        send_time: format_time_for_json(info.stime),
92        receive_transid: info.rtransid,
93        receive_time: format_time_for_json(info.rtime),
94        quota: qg.map(|q| QuotaJson {
95            qgroupid: format!("0/{}", info.id),
96            limit_referenced: if q.max_rfer == 0 || q.max_rfer == u64::MAX {
97                None
98            } else {
99                Some(q.max_rfer)
100            },
101            limit_exclusive: if q.max_excl == 0 || q.max_excl == u64::MAX {
102                None
103            } else {
104                Some(q.max_excl)
105            },
106            usage_referenced: q.rfer,
107            usage_exclusive: q.excl,
108        }),
109    }
110}
111
112impl Runnable for SubvolumeShowCommand {
113    fn supported_formats(&self) -> &[Format] {
114        &[Format::Text, Format::Json, Format::Modern]
115    }
116
117    fn run(&self, ctx: &RunContext) -> Result<()> {
118        let file = open_path(&self.path)?;
119
120        let info = if let Some(rootid) = self.rootid {
121            subvolume_info_by_id(file.as_fd(), rootid).with_context(|| {
122                format!("failed to get subvolume info for rootid {rootid}")
123            })?
124        } else if let Some(ref uuid) = self.uuid {
125            let inner = &**uuid;
126            let rootid = subvolume_search_by_uuid(file.as_fd(), inner)
127                .with_context(|| {
128                    format!("failed to find subvolume with UUID {inner}")
129                })?;
130            subvolume_info_by_id(file.as_fd(), rootid).with_context(|| {
131                format!("failed to get subvolume info for UUID {inner}")
132            })?
133        } else {
134            subvolume_info(file.as_fd()).with_context(|| {
135                format!(
136                    "failed to get subvolume info for '{}'",
137                    self.path.display()
138                )
139            })?
140        };
141
142        let qg = query_qgroup(file.as_fd(), info.id);
143
144        match ctx.format {
145            Format::Modern | Format::Text => {
146                println!("{}", self.path.display());
147                println!("\tName: \t\t\t{}", info.name);
148                println!("\tUUID: \t\t\t{}", format_uuid(&info.uuid));
149                println!(
150                    "\tParent UUID: \t\t{}",
151                    format_uuid(&info.parent_uuid)
152                );
153                println!(
154                    "\tReceived UUID: \t\t{}",
155                    format_uuid(&info.received_uuid)
156                );
157                println!("\tCreation time: \t\t{}", format_time(info.otime));
158                println!("\tSubvolume ID: \t\t{}", info.id);
159                println!("\tGeneration: \t\t{}", info.generation);
160                println!("\tGen at creation: \t{}", info.otransid);
161                println!("\tParent ID: \t\t{}", info.parent_id);
162                println!("\tTop level ID: \t\t{}", info.parent_id);
163                println!("\tFlags: \t\t\t{}", info.flags);
164                println!("\tSend transid: \t\t{}", info.stransid);
165                println!("\tSend time: \t\t{}", format_time(info.stime));
166                println!("\tReceive transid: \t{}", info.rtransid);
167                println!("\tReceive time: \t\t{}", format_time(info.rtime));
168
169                if let Some(ref qg) = qg {
170                    let mode = self.units.resolve();
171                    println!("\tQuota group:\t\t0/{}", info.id);
172                    println!(
173                        "\t  Limit referenced:\t{}",
174                        format_limit(qg.max_rfer, &mode)
175                    );
176                    println!(
177                        "\t  Limit exclusive:\t{}",
178                        format_limit(qg.max_excl, &mode)
179                    );
180                    println!(
181                        "\t  Usage referenced:\t{}",
182                        fmt_size(qg.rfer, &mode)
183                    );
184                    println!(
185                        "\t  Usage exclusive:\t{}",
186                        fmt_size(qg.excl, &mode)
187                    );
188                }
189            }
190            Format::Json => {
191                let json = subvol_to_json(&info, qg.as_ref());
192                print_json("subvolume-show", &json)?;
193            }
194        }
195
196        Ok(())
197    }
198}
199
200fn format_uuid(uuid: &uuid::Uuid) -> String {
201    if uuid.is_nil() {
202        "-".to_string()
203    } else {
204        uuid.hyphenated().to_string()
205    }
206}
207
208/// Query the qgroup entry for a specific subvolume ID.
209///
210/// Returns `None` if quotas are not enabled or the subvolume has no qgroup.
211fn query_qgroup(
212    fd: std::os::unix::io::BorrowedFd,
213    subvol_id: u64,
214) -> Option<QgroupInfo> {
215    let list = quota::qgroup_list(fd).ok()?;
216    list.qgroups.into_iter().find(|q| {
217        quota::qgroupid_level(q.qgroupid) == 0
218            && quota::qgroupid_subvolid(q.qgroupid) == subvol_id
219    })
220}
221
222/// Format a qgroup limit value: "-" if no limit, formatted size otherwise.
223fn format_limit(limit: u64, mode: &SizeFormat) -> String {
224    if limit == 0 || limit == u64::MAX {
225        "-".to_string()
226    } else {
227        fmt_size(limit, mode)
228    }
229}