btrfs_cli/subvolume/
find_new.rs1use crate::{RunContext, Runnable, util::open_path};
2use anyhow::{Context, Result};
3use btrfs_disk::items::{FileExtentBody, FileExtentItem, FileExtentType};
4use btrfs_uapi::{
5 filesystem::sync,
6 inode::ino_paths,
7 raw::BTRFS_EXTENT_DATA_KEY,
8 subvolume::subvolume_info,
9 tree_search::{SearchFilter, tree_search},
10};
11use clap::Parser;
12use std::{os::unix::io::AsFd, path::PathBuf};
13
14#[derive(Parser, Debug)]
19pub struct SubvolumeFindNewCommand {
20 path: PathBuf,
22
23 last_gen: u64,
25}
26
27impl Runnable for SubvolumeFindNewCommand {
28 fn run(&self, _ctx: &RunContext) -> Result<()> {
29 let file = open_path(&self.path)?;
30
31 sync(file.as_fd()).with_context(|| {
33 format!("failed to sync '{}'", self.path.display())
34 })?;
35
36 let info = subvolume_info(file.as_fd()).with_context(|| {
38 format!(
39 "failed to get subvolume info for '{}'",
40 self.path.display()
41 )
42 })?;
43 let max_gen = info.generation;
44
45 let mut key = SearchFilter::for_type(0, BTRFS_EXTENT_DATA_KEY);
49 key.min_transid = self.last_gen;
50
51 let mut cache_ino: u64 = 0;
52 let mut cache_name: Option<String> = None;
53
54 tree_search(file.as_fd(), key, |hdr, data| {
55 let Some(fe) = FileExtentItem::parse(data) else {
56 return Ok(());
57 };
58
59 if fe.generation < self.last_gen {
60 return Ok(());
61 }
62
63 let compressed =
64 !matches!(fe.compression, btrfs_disk::items::CompressionType::None);
65
66 let (disk_start, disk_offset, len) = match &fe.body {
67 FileExtentBody::Regular {
68 disk_bytenr,
69 num_bytes,
70 offset,
71 ..
72 } => (*disk_bytenr, *offset, *num_bytes),
73 FileExtentBody::Inline { inline_size } => {
74 (0, 0, *inline_size as u64)
75 }
76 };
77
78 let name = if hdr.objectid == cache_ino {
81 cache_name.as_deref().unwrap_or("unknown")
82 } else {
83 let resolved = match ino_paths(file.as_fd(), hdr.objectid) {
84 Ok(paths) if !paths.is_empty() => {
85 Some(paths.into_iter().next().unwrap())
86 }
87 _ => None,
88 };
89 cache_ino = hdr.objectid;
90 cache_name = resolved;
91 cache_name.as_deref().unwrap_or("unknown")
92 };
93
94 let mut flags = String::new();
96 if compressed {
97 flags.push_str("COMPRESS");
98 }
99 if fe.extent_type == FileExtentType::Prealloc {
100 if !flags.is_empty() {
101 flags.push('|');
102 }
103 flags.push_str("PREALLOC");
104 }
105 if fe.extent_type == FileExtentType::Inline {
106 if !flags.is_empty() {
107 flags.push('|');
108 }
109 flags.push_str("INLINE");
110 }
111 if flags.is_empty() {
112 flags.push_str("NONE");
113 }
114
115 println!(
116 "inode {} file offset {} len {} disk start {} offset {} gen {} flags {flags} {name}",
117 hdr.objectid, hdr.offset, len, disk_start, disk_offset, fe.generation,
118 );
119
120 Ok(())
121 })
122 .with_context(|| format!("tree search failed for '{}'", self.path.display()))?;
123
124 println!("transid marker was {max_gen}");
125
126 Ok(())
127 }
128}