Skip to main content

btrfs_cli/subvolume/
find_new.rs

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