Skip to main content

btrfs_cli/subvolume/
find_new.rs

1use crate::{Format, Runnable, util::open_path};
2use anyhow::{Context, Result};
3use btrfs_uapi::{
4    filesystem::sync,
5    inode::ino_paths,
6    raw::{
7        BTRFS_EXTENT_DATA_KEY, BTRFS_FILE_EXTENT_INLINE,
8        BTRFS_FILE_EXTENT_PREALLOC, BTRFS_FILE_EXTENT_REG,
9        btrfs_file_extent_item,
10    },
11    subvolume::subvolume_info,
12    tree_search::{SearchKey, tree_search},
13    util::read_le_u64,
14};
15use clap::Parser;
16use std::{mem, os::unix::io::AsFd, path::PathBuf};
17
18/// List the recently modified files in a subvolume
19///
20/// Prints all files that have been modified since the given generation number.
21/// The generation can be found with `btrfs subvolume show`.
22#[derive(Parser, Debug)]
23pub struct SubvolumeFindNewCommand {
24    /// Path to the subvolume to search
25    path: PathBuf,
26
27    /// Only show files modified at or after this generation number
28    last_gen: u64,
29}
30
31impl Runnable for SubvolumeFindNewCommand {
32    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
33        let file = open_path(&self.path)?;
34
35        // Sync first so we see the latest data.
36        sync(file.as_fd()).with_context(|| {
37            format!("failed to sync '{}'", self.path.display())
38        })?;
39
40        // Get the current generation for the "transid marker" output.
41        let info = subvolume_info(file.as_fd()).with_context(|| {
42            format!(
43                "failed to get subvolume info for '{}'",
44                self.path.display()
45            )
46        })?;
47        let max_gen = info.generation;
48
49        // Search tree 0 (the subvolume's own tree, relative to the fd) for
50        // EXTENT_DATA_KEY items.  The min_transid filter restricts results to
51        // items whose metadata block was written at or after last_gen.
52        let mut key = SearchKey::for_type(0, BTRFS_EXTENT_DATA_KEY);
53        key.min_transid = self.last_gen;
54
55        let mut cache_ino: u64 = 0;
56        let mut cache_name: Option<String> = None;
57
58        tree_search(file.as_fd(), key, |hdr, data| {
59            let gen_off = mem::offset_of!(btrfs_file_extent_item, generation);
60            let type_off = mem::offset_of!(btrfs_file_extent_item, type_);
61            let compression_off = mem::offset_of!(btrfs_file_extent_item, compression);
62
63            // Need at least enough data to read the generation and type fields.
64            if data.len() < type_off + 1 {
65                return Ok(());
66            }
67
68            let found_gen = read_le_u64(data, gen_off);
69            if found_gen < self.last_gen {
70                return Ok(());
71            }
72
73            let extent_type = data[type_off];
74            let compressed = data.get(compression_off).copied().unwrap_or(0) != 0;
75
76            let (disk_start, disk_offset, len) =
77                if extent_type == BTRFS_FILE_EXTENT_REG as u8
78                    || extent_type == BTRFS_FILE_EXTENT_PREALLOC as u8
79                {
80                    let disk_bytenr_off = mem::offset_of!(btrfs_file_extent_item, disk_bytenr);
81                    let offset_off = mem::offset_of!(btrfs_file_extent_item, offset);
82                    let num_bytes_off = mem::offset_of!(btrfs_file_extent_item, num_bytes);
83
84                    if data.len() < num_bytes_off + 8 {
85                        return Ok(());
86                    }
87                    (
88                        read_le_u64(data, disk_bytenr_off),
89                        read_le_u64(data, offset_off),
90                        read_le_u64(data, num_bytes_off),
91                    )
92                } else if extent_type == BTRFS_FILE_EXTENT_INLINE as u8 {
93                    let ram_bytes_off = mem::offset_of!(btrfs_file_extent_item, ram_bytes);
94                    if data.len() < ram_bytes_off + 8 {
95                        return Ok(());
96                    }
97                    (0, 0, read_le_u64(data, ram_bytes_off))
98                } else {
99                    return Ok(());
100                };
101
102            // Resolve inode to path (with caching for consecutive extents
103            // of the same inode).
104            let name = if hdr.objectid == cache_ino {
105                cache_name.as_deref().unwrap_or("unknown")
106            } else {
107                let resolved = match ino_paths(file.as_fd(), hdr.objectid) {
108                    Ok(paths) if !paths.is_empty() => Some(paths.into_iter().next().unwrap()),
109                    _ => None,
110                };
111                cache_ino = hdr.objectid;
112                cache_name = resolved;
113                cache_name.as_deref().unwrap_or("unknown")
114            };
115
116            // Build flags string.
117            let mut flags = String::new();
118            if compressed {
119                flags.push_str("COMPRESS");
120            }
121            if extent_type == BTRFS_FILE_EXTENT_PREALLOC as u8 {
122                if !flags.is_empty() {
123                    flags.push('|');
124                }
125                flags.push_str("PREALLOC");
126            }
127            if extent_type == BTRFS_FILE_EXTENT_INLINE as u8 {
128                if !flags.is_empty() {
129                    flags.push('|');
130                }
131                flags.push_str("INLINE");
132            }
133            if flags.is_empty() {
134                flags.push_str("NONE");
135            }
136
137            println!(
138                "inode {} file offset {} len {} disk start {} offset {} gen {} flags {flags} {name}",
139                hdr.objectid, hdr.offset, len, disk_start, disk_offset, found_gen,
140            );
141
142            Ok(())
143        })
144        .with_context(|| format!("tree search failed for '{}'", self.path.display()))?;
145
146        println!("transid marker was {max_gen}");
147
148        Ok(())
149    }
150}