use crate::{RunContext, Runnable, util::is_mounted};
use anyhow::{Context, Result, bail};
use btrfs_disk::{
items::{FileExtentBody, FileExtentItem},
raw,
tree::{DiskKey, KeyType},
};
use btrfs_transaction::{
filesystem::Filesystem,
items,
path::BtrfsPath,
search::{self, SearchIntent},
transaction::Transaction,
};
use clap::Parser;
use std::{
fs::OpenOptions,
io::{Read, Seek, Write},
path::PathBuf,
};
const ROOT_TREE_OBJECTID: u64 = 1;
const FS_TREE_OBJECTID: u64 = raw::BTRFS_FS_TREE_OBJECTID as u64;
const FIRST_FREE_OBJECTID: u64 = raw::BTRFS_FIRST_FREE_OBJECTID as u64;
const LAST_FREE_OBJECTID: u64 =
(raw::BTRFS_LAST_FREE_OBJECTID as i64).cast_unsigned();
const FREE_INO_OBJECTID: u64 =
(raw::BTRFS_FREE_INO_OBJECTID as i64).cast_unsigned();
const FREE_SPACE_OBJECTID: u64 =
(raw::BTRFS_FREE_SPACE_OBJECTID as i64).cast_unsigned();
fn is_fs_tree(objectid: u64) -> bool {
objectid == FS_TREE_OBJECTID
|| (FIRST_FREE_OBJECTID..=LAST_FREE_OBJECTID).contains(&objectid)
}
#[derive(Parser, Debug)]
pub struct RescueClearInoCacheCommand {
device: PathBuf,
}
#[derive(Debug)]
struct InoCacheItem {
key: DiskKey,
extent: Option<ExtentRef>,
}
#[derive(Debug)]
struct ExtentRef {
disk_bytenr: u64,
disk_num_bytes: u64,
}
impl Runnable for RescueClearInoCacheCommand {
fn run(&self, _ctx: &RunContext) -> Result<()> {
if is_mounted(&self.device) {
bail!("{} is currently mounted", self.device.display());
}
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&self.device)
.with_context(|| {
format!("failed to open '{}'", self.device.display())
})?;
let mut fs = Filesystem::open(file).with_context(|| {
format!("failed to open filesystem on '{}'", self.device.display())
})?;
let fs_tree_ids = collect_fs_tree_ids(&mut fs)?;
let mut total_subvols = 0usize;
let mut total_items = 0usize;
let mut total_extents = 0usize;
for tree_id in fs_tree_ids {
if fs.root_bytenr(tree_id).is_none() {
continue;
}
let items_to_clear = collect_ino_cache_items(&mut fs, tree_id)?;
if items_to_clear.is_empty() {
continue;
}
let mut trans = Transaction::start(&mut fs)
.context("failed to start transaction")?;
for item in &items_to_clear {
if let Some(ext) = &item.extent
&& ext.disk_bytenr != 0
{
trans.delayed_refs.drop_data_ref(
ext.disk_bytenr,
ext.disk_num_bytes,
tree_id,
FREE_INO_OBJECTID,
0,
1,
);
total_extents += 1;
}
delete_one_item(&mut trans, &mut fs, tree_id, &item.key)?;
}
trans
.commit(&mut fs)
.context("failed to commit transaction")?;
fs.sync().context("failed to sync to disk")?;
total_subvols += 1;
total_items += items_to_clear.len();
}
if total_subvols == 0 {
println!("no inode cache items found on {}", self.device.display());
} else {
println!(
"cleared inode cache on {} ({} subvolume(s), {} item(s), {} data extent(s) freed)",
self.device.display(),
total_subvols,
total_items,
total_extents,
);
}
Ok(())
}
}
fn collect_fs_tree_ids<R: Read + Write + Seek>(
fs: &mut Filesystem<R>,
) -> Result<Vec<u64>> {
let start = DiskKey {
objectid: 0,
key_type: KeyType::from_raw(0),
offset: 0,
};
let mut path = BtrfsPath::new();
search::search_slot(
None,
fs,
ROOT_TREE_OBJECTID,
&start,
&mut path,
SearchIntent::ReadOnly,
false,
)
.context("failed to walk root tree for ROOT_ITEMs")?;
let mut ids: Vec<u64> = Vec::new();
loop {
let Some(leaf) = path.nodes[0].as_ref() else {
break;
};
let nritems = leaf.nritems() as usize;
if path.slots[0] >= nritems {
if !search::next_leaf(fs, &mut path).context("next_leaf failed")? {
break;
}
continue;
}
let key = leaf.item_key(path.slots[0]);
if key.key_type == KeyType::RootItem && is_fs_tree(key.objectid) {
if ids.last().copied() != Some(key.objectid) {
ids.push(key.objectid);
}
}
path.slots[0] += 1;
}
path.release();
Ok(ids)
}
fn collect_ino_cache_items<R: Read + Write + Seek>(
fs: &mut Filesystem<R>,
tree_id: u64,
) -> Result<Vec<InoCacheItem>> {
let mut out: Vec<InoCacheItem> = Vec::new();
for objectid in [FREE_INO_OBJECTID, FREE_SPACE_OBJECTID] {
collect_for_objectid(fs, tree_id, objectid, &mut out)?;
}
Ok(out)
}
fn collect_for_objectid<R: Read + Write + Seek>(
fs: &mut Filesystem<R>,
tree_id: u64,
objectid: u64,
out: &mut Vec<InoCacheItem>,
) -> Result<()> {
let start = DiskKey {
objectid,
key_type: KeyType::from_raw(0),
offset: 0,
};
let mut path = BtrfsPath::new();
search::search_slot(
None,
fs,
tree_id,
&start,
&mut path,
SearchIntent::ReadOnly,
false,
)
.with_context(|| {
format!("failed to search tree {tree_id} for objectid {objectid:#x}")
})?;
loop {
let Some(leaf) = path.nodes[0].as_ref() else {
break;
};
let nritems = leaf.nritems() as usize;
if path.slots[0] >= nritems {
if !search::next_leaf(fs, &mut path).context("next_leaf failed")? {
break;
}
continue;
}
let key = leaf.item_key(path.slots[0]);
if key.objectid != objectid {
break;
}
let extent = if key.key_type == KeyType::ExtentData {
let data = leaf.item_data(path.slots[0]);
let fei = FileExtentItem::parse(data).with_context(|| {
format!(
"failed to parse FILE_EXTENT for ino cache at offset {}",
key.offset
)
})?;
match fei.body {
FileExtentBody::Regular {
disk_bytenr,
disk_num_bytes,
..
} => Some(ExtentRef {
disk_bytenr,
disk_num_bytes,
}),
FileExtentBody::Inline { .. } => None,
}
} else {
None
};
out.push(InoCacheItem { key, extent });
path.slots[0] += 1;
}
path.release();
Ok(())
}
fn delete_one_item<R: Read + Write + Seek>(
trans: &mut Transaction<R>,
fs: &mut Filesystem<R>,
tree_id: u64,
key: &DiskKey,
) -> Result<bool> {
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(trans),
fs,
tree_id,
key,
&mut path,
SearchIntent::Delete,
true,
)
.with_context(|| {
format!("failed to search for {key:?} in tree {tree_id}")
})?;
if !found {
path.release();
return Ok(false);
}
let leaf = path.nodes[0]
.as_mut()
.ok_or_else(|| anyhow::anyhow!("delete_one_item: no leaf in path"))?;
items::del_items(leaf, path.slots[0], 1);
fs.mark_dirty(leaf);
path.release();
Ok(true)
}