use crate::{
RunContext, Runnable,
util::{is_mounted, open_path, parse_size_with_suffix},
};
use anyhow::{Context, Result, bail};
use btrfs_disk::{
items::DeviceItem,
tree::{DiskKey, KeyType},
};
use btrfs_transaction::{
filesystem::Filesystem,
path::BtrfsPath,
search::{self, SearchIntent},
transaction::Transaction,
};
use btrfs_uapi::filesystem::{ResizeAmount, ResizeArgs, resize};
use clap::Parser;
use std::{
fs::OpenOptions,
os::unix::io::AsFd,
path::{Path, PathBuf},
};
#[derive(Parser, Debug)]
pub struct FilesystemResizeCommand {
#[clap(long)]
pub enqueue: bool,
#[clap(long)]
pub offline: bool,
pub size: String,
pub path: PathBuf,
}
fn parse_resize_amount(s: &str) -> Result<ResizeAmount> {
if s == "cancel" {
return Ok(ResizeAmount::Cancel);
}
if s == "max" {
return Ok(ResizeAmount::Max);
}
let (modifier, rest) = if let Some(r) = s.strip_prefix('+') {
(1i32, r)
} else if let Some(r) = s.strip_prefix('-') {
(-1i32, r)
} else {
(0i32, s)
};
let bytes = parse_size_with_suffix(rest)?;
Ok(match modifier {
1 => ResizeAmount::Add(bytes),
-1 => ResizeAmount::Sub(bytes),
_ => ResizeAmount::Set(bytes),
})
}
fn parse_resize_args(s: &str) -> Result<ResizeArgs> {
if let Some(colon) = s.find(':')
&& let Ok(devid) = s[..colon].parse::<u64>()
{
let amount = parse_resize_amount(&s[colon + 1..])?;
return Ok(ResizeArgs::new(amount).with_devid(devid));
}
Ok(ResizeArgs::new(parse_resize_amount(s)?))
}
impl Runnable for FilesystemResizeCommand {
fn run(&self, ctx: &RunContext) -> Result<()> {
if self.offline {
if self.enqueue {
bail!("--enqueue is not compatible with --offline");
}
return run_offline(&self.path, &self.size, ctx);
}
if self.enqueue {
bail!("--enqueue is not yet implemented");
}
let args = parse_resize_args(&self.size).with_context(|| {
format!("invalid resize argument: '{}'", self.size)
})?;
let file = open_path(&self.path)?;
resize(file.as_fd(), args).with_context(|| {
format!("resize failed on '{}'", self.path.display())
})?;
Ok(())
}
fn supports_dry_run(&self) -> bool {
self.offline
}
}
#[allow(clippy::too_many_lines)]
fn run_offline(path: &Path, amount: &str, ctx: &RunContext) -> Result<()> {
if is_mounted(path) {
bail!("{} must not be mounted to use --offline", path.display());
}
let args = parse_resize_args(amount)
.with_context(|| format!("invalid resize argument: '{amount}'"))?;
if matches!(args.amount, ResizeAmount::Cancel) {
bail!("cannot cancel an offline resize");
}
let file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.with_context(|| format!("failed to open '{}'", path.display()))?;
let metadata = file
.metadata()
.with_context(|| format!("failed to stat '{}'", path.display()))?;
let is_regular_file = metadata.file_type().is_file();
let mut fs = Filesystem::open(file).with_context(|| {
format!("failed to open filesystem on '{}'", path.display())
})?;
if fs.superblock.num_devices != 1 {
bail!(
"multi-device filesystems are not supported with --offline ({} devices)",
fs.superblock.num_devices
);
}
let sectorsize = u64::from(fs.superblock.sectorsize);
let old_total = fs.superblock.total_bytes;
let devid = fs.superblock.dev_item.devid;
let old_device_bytes = fs.superblock.dev_item.total_bytes;
if let Some(requested_devid) = args.devid
&& requested_devid != devid
{
bail!(
"invalid device id {requested_devid} (only devid {devid} is present)"
);
}
let new_device_bytes = match args.amount {
ResizeAmount::Set(bytes) => bytes,
ResizeAmount::Add(bytes) => old_device_bytes
.checked_add(bytes)
.context("resize overflow")?,
ResizeAmount::Sub(_) => {
bail!("offline resize does not support shrinking")
}
ResizeAmount::Max => {
if is_regular_file {
metadata.len()
} else {
bail!("--offline max is only supported on regular file images");
}
}
ResizeAmount::Cancel => unreachable!("rejected above"),
};
let new_device_bytes = (new_device_bytes / sectorsize) * sectorsize;
if new_device_bytes < old_device_bytes {
bail!("offline resize does not support shrinking");
}
if new_device_bytes == old_device_bytes {
if !ctx.quiet {
println!(
"{}: already at the requested size ({} bytes)",
path.display(),
old_device_bytes
);
}
return Ok(());
}
let diff = new_device_bytes - old_device_bytes;
let new_total_bytes = old_total
.checked_add(diff)
.context("superblock total_bytes overflow")?;
if !ctx.quiet {
println!(
"resize '{}' from {} to {} ({:+} bytes){}",
path.display(),
old_device_bytes,
new_device_bytes,
diff.cast_signed(),
if ctx.dry_run { " [dry-run]" } else { "" },
);
}
if ctx.dry_run {
return Ok(());
}
let mut trans =
Transaction::start(&mut fs).context("failed to start transaction")?;
let key = DiskKey {
objectid: 1, key_type: KeyType::DeviceItem,
offset: devid,
};
let mut bpath = BtrfsPath::new();
let found = search::search_slot(
Some(&mut trans),
&mut fs,
3, &key,
&mut bpath,
SearchIntent::ReadOnly,
true, )
.context("failed to search chunk tree for DEV_ITEM")?;
if !found {
bpath.release();
bail!("DEV_ITEM for devid {devid} not found in chunk tree");
}
{
let leaf = bpath.nodes[0]
.as_mut()
.context("DEV_ITEM search returned no leaf")?;
let slot = bpath.slots[0];
let data = leaf.item_data(slot);
let mut item = DeviceItem::parse(data).with_context(|| {
format!("failed to parse DEV_ITEM for devid {devid}")
})?;
item.total_bytes = new_device_bytes;
let mut buf = Vec::with_capacity(data.len());
item.write_bytes(&mut buf);
let target = leaf.item_data_mut(slot);
if target.len() < buf.len() {
bpath.release();
bail!("DEV_ITEM payload smaller than expected");
}
target[..buf.len()].copy_from_slice(&buf);
fs.mark_dirty(leaf);
}
bpath.release();
fs.superblock.total_bytes = new_total_bytes;
fs.superblock.dev_item.total_bytes = new_device_bytes;
trans
.commit(&mut fs)
.context("failed to commit transaction")?;
fs.sync().context("failed to sync to disk")?;
if is_regular_file {
let f = fs.reader_mut().single_device_mut();
f.set_len(new_device_bytes).with_context(|| {
format!(
"failed to resize backing file '{}' to {new_device_bytes}",
path.display()
)
})?;
}
Ok(())
}