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#[derive(Parser, Debug)]
26pub struct SubvolumeShowCommand {
27 #[clap(short = 'r', long = "rootid")]
29 pub rootid: Option<u64>,
30
31 #[clap(short = 'u', long = "uuid", conflicts_with = "rootid")]
33 pub uuid: Option<ParsedUuid>,
34
35 #[clap(flatten)]
36 pub units: UnitMode,
37
38 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
208fn 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
222fn 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}