Skip to main content

btrfs_cli/subvolume/
find_new.rs

1use 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/// List the recently modified files in a subvolume
15///
16/// Prints all files that have been modified since the given generation number.
17/// The generation can be found with `btrfs subvolume show`.
18#[derive(Parser, Debug)]
19pub struct SubvolumeFindNewCommand {
20    /// Path to the subvolume to search
21    path: PathBuf,
22
23    /// Only show files modified at or after this generation number
24    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 first so we see the latest data.
32        sync(file.as_fd()).with_context(|| {
33            format!("failed to sync '{}'", self.path.display())
34        })?;
35
36        // Get the current generation for the "transid marker" output.
37        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        // Search tree 0 (the subvolume's own tree, relative to the fd) for
46        // EXTENT_DATA_KEY items.  The min_transid filter restricts results to
47        // items whose metadata block was written at or after last_gen.
48        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            // Resolve inode to path (with caching for consecutive extents
79            // of the same inode).
80            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            // Build flags string.
95            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}