btrfs_cli/filesystem/
resize.rs1use crate::{
2 RunContext, Runnable,
3 util::{is_mounted, open_path, parse_size_with_suffix},
4};
5use anyhow::{Context, Result, bail};
6use btrfs_disk::{
7 items::DeviceItem,
8 tree::{DiskKey, KeyType},
9};
10use btrfs_transaction::{
11 filesystem::Filesystem,
12 path::BtrfsPath,
13 search::{self, SearchIntent},
14 transaction::Transaction,
15};
16use btrfs_uapi::filesystem::{ResizeAmount, ResizeArgs, resize};
17use clap::Parser;
18use std::{
19 fs::OpenOptions,
20 os::unix::io::AsFd,
21 path::{Path, PathBuf},
22};
23
24#[derive(Parser, Debug)]
26pub struct FilesystemResizeCommand {
27 #[clap(long)]
29 pub enqueue: bool,
30
31 #[clap(long)]
33 pub offline: bool,
34
35 pub size: String,
38
39 pub path: PathBuf,
40}
41
42fn parse_resize_amount(s: &str) -> Result<ResizeAmount> {
43 if s == "cancel" {
44 return Ok(ResizeAmount::Cancel);
45 }
46 if s == "max" {
47 return Ok(ResizeAmount::Max);
48 }
49 let (modifier, rest) = if let Some(r) = s.strip_prefix('+') {
50 (1i32, r)
51 } else if let Some(r) = s.strip_prefix('-') {
52 (-1i32, r)
53 } else {
54 (0i32, s)
55 };
56 let bytes = parse_size_with_suffix(rest)?;
57 Ok(match modifier {
58 1 => ResizeAmount::Add(bytes),
59 -1 => ResizeAmount::Sub(bytes),
60 _ => ResizeAmount::Set(bytes),
61 })
62}
63
64fn parse_resize_args(s: &str) -> Result<ResizeArgs> {
65 if let Some(colon) = s.find(':')
66 && let Ok(devid) = s[..colon].parse::<u64>()
67 {
68 let amount = parse_resize_amount(&s[colon + 1..])?;
69 return Ok(ResizeArgs::new(amount).with_devid(devid));
70 }
71 Ok(ResizeArgs::new(parse_resize_amount(s)?))
72}
73
74impl Runnable for FilesystemResizeCommand {
75 fn run(&self, ctx: &RunContext) -> Result<()> {
76 if self.offline {
77 if self.enqueue {
78 bail!("--enqueue is not compatible with --offline");
79 }
80 return run_offline(&self.path, &self.size, ctx);
81 }
82
83 if self.enqueue {
84 bail!("--enqueue is not yet implemented");
85 }
86
87 let args = parse_resize_args(&self.size).with_context(|| {
88 format!("invalid resize argument: '{}'", self.size)
89 })?;
90
91 let file = open_path(&self.path)?;
92
93 resize(file.as_fd(), args).with_context(|| {
94 format!("resize failed on '{}'", self.path.display())
95 })?;
96
97 Ok(())
98 }
99
100 fn supports_dry_run(&self) -> bool {
101 self.offline
104 }
105}
106
107#[allow(clippy::too_many_lines)]
120fn run_offline(path: &Path, amount: &str, ctx: &RunContext) -> Result<()> {
121 if is_mounted(path) {
122 bail!("{} must not be mounted to use --offline", path.display());
123 }
124
125 let args = parse_resize_args(amount)
126 .with_context(|| format!("invalid resize argument: '{amount}'"))?;
127
128 if matches!(args.amount, ResizeAmount::Cancel) {
129 bail!("cannot cancel an offline resize");
130 }
131
132 let file = OpenOptions::new()
133 .read(true)
134 .write(true)
135 .open(path)
136 .with_context(|| format!("failed to open '{}'", path.display()))?;
137
138 let metadata = file
139 .metadata()
140 .with_context(|| format!("failed to stat '{}'", path.display()))?;
141 let is_regular_file = metadata.file_type().is_file();
142
143 let mut fs = Filesystem::open(file).with_context(|| {
144 format!("failed to open filesystem on '{}'", path.display())
145 })?;
146
147 if fs.superblock.num_devices != 1 {
148 bail!(
149 "multi-device filesystems are not supported with --offline ({} devices)",
150 fs.superblock.num_devices
151 );
152 }
153
154 let sectorsize = u64::from(fs.superblock.sectorsize);
155 let old_total = fs.superblock.total_bytes;
156 let devid = fs.superblock.dev_item.devid;
157 let old_device_bytes = fs.superblock.dev_item.total_bytes;
158
159 if let Some(requested_devid) = args.devid
160 && requested_devid != devid
161 {
162 bail!(
163 "invalid device id {requested_devid} (only devid {devid} is present)"
164 );
165 }
166
167 let new_device_bytes = match args.amount {
169 ResizeAmount::Set(bytes) => bytes,
170 ResizeAmount::Add(bytes) => old_device_bytes
171 .checked_add(bytes)
172 .context("resize overflow")?,
173 ResizeAmount::Sub(_) => {
174 bail!("offline resize does not support shrinking")
175 }
176 ResizeAmount::Max => {
177 if is_regular_file {
182 metadata.len()
183 } else {
184 bail!("--offline max is only supported on regular file images");
185 }
186 }
187 ResizeAmount::Cancel => unreachable!("rejected above"),
188 };
189
190 let new_device_bytes = (new_device_bytes / sectorsize) * sectorsize;
192 if new_device_bytes < old_device_bytes {
193 bail!("offline resize does not support shrinking");
194 }
195 if new_device_bytes == old_device_bytes {
196 if !ctx.quiet {
197 println!(
198 "{}: already at the requested size ({} bytes)",
199 path.display(),
200 old_device_bytes
201 );
202 }
203 return Ok(());
204 }
205
206 let diff = new_device_bytes - old_device_bytes;
207 let new_total_bytes = old_total
208 .checked_add(diff)
209 .context("superblock total_bytes overflow")?;
210
211 if !ctx.quiet {
212 println!(
213 "resize '{}' from {} to {} ({:+} bytes){}",
214 path.display(),
215 old_device_bytes,
216 new_device_bytes,
217 diff.cast_signed(),
218 if ctx.dry_run { " [dry-run]" } else { "" },
219 );
220 }
221
222 if ctx.dry_run {
223 return Ok(());
224 }
225
226 let mut trans =
228 Transaction::start(&mut fs).context("failed to start transaction")?;
229
230 let key = DiskKey {
231 objectid: 1, key_type: KeyType::DeviceItem,
233 offset: devid,
234 };
235 let mut bpath = BtrfsPath::new();
236 let found = search::search_slot(
237 Some(&mut trans),
238 &mut fs,
239 3, &key,
241 &mut bpath,
242 SearchIntent::ReadOnly,
243 true, )
245 .context("failed to search chunk tree for DEV_ITEM")?;
246 if !found {
247 bpath.release();
248 bail!("DEV_ITEM for devid {devid} not found in chunk tree");
249 }
250
251 {
252 let leaf = bpath.nodes[0]
253 .as_mut()
254 .context("DEV_ITEM search returned no leaf")?;
255 let slot = bpath.slots[0];
256 let data = leaf.item_data(slot);
257 let mut item = DeviceItem::parse(data).with_context(|| {
258 format!("failed to parse DEV_ITEM for devid {devid}")
259 })?;
260 item.total_bytes = new_device_bytes;
261 let mut buf = Vec::with_capacity(data.len());
262 item.write_bytes(&mut buf);
263 let target = leaf.item_data_mut(slot);
264 if target.len() < buf.len() {
265 bpath.release();
266 bail!("DEV_ITEM payload smaller than expected");
267 }
268 target[..buf.len()].copy_from_slice(&buf);
269 fs.mark_dirty(leaf);
270 }
271 bpath.release();
272
273 fs.superblock.total_bytes = new_total_bytes;
277 fs.superblock.dev_item.total_bytes = new_device_bytes;
278
279 trans
280 .commit(&mut fs)
281 .context("failed to commit transaction")?;
282 fs.sync().context("failed to sync to disk")?;
283
284 if is_regular_file {
288 let f = fs.reader_mut().inner_mut();
289 f.set_len(new_device_bytes).with_context(|| {
290 format!(
291 "failed to resize backing file '{}' to {new_device_bytes}",
292 path.display()
293 )
294 })?;
295 }
296
297 Ok(())
298}