filetrack/
inode_aware.rs

1use std::{
2    cmp::Ordering,
3    fs::File,
4    io::{self, BufReader},
5    ops::{Deref, DerefMut},
6    path::Path,
7};
8
9use serde::{Deserialize, Serialize};
10
11use crate::{path_utils::glob_rotated_logs, Multireader};
12
13/// Structure that can be used as persistent offset into rotated logs. See `InodeAwareReader` for more info.
14#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
15pub struct InodeAwareOffset {
16    pub inode: u64,
17    pub offset: u64,
18}
19
20/// Reader that keeps track of what inode it reads from.
21///
22/// This reader supports persistent indexing using `InodeAwareOffset`. It allows easy persistent reading of rotated logs.
23/// Scheme of persistent is to be implemented by user. For a ready-to-use recipe with simple file storage see `TrackedReader`.
24///
25/// ```rust no_run
26/// # use std::io::{Read, BufRead, self};
27/// # use filetrack::{InodeAwareOffset, InodeAwareReader};
28/// # fn load_state() -> io::Result<InodeAwareOffset> {Ok(InodeAwareOffset{inode: 0, offset: 0})}
29/// # fn save_state(state: InodeAwareOffset) -> io::Result<()> {Ok(())}
30/// let mut reader = InodeAwareReader::from_rotated_logs("/var/log/mail.log")?;
31/// reader.seek_persistent(load_state()?)?;
32/// # let mut buf = vec![];
33/// reader.read_exact(& mut buf)?;
34/// save_state(reader.get_persistent_offset())?;
35/// # Ok::<(), std::io::Error>(())
36/// ```
37///
38/// During initialization, this reader searches for rotated versions of provided path and notes their inodes. After that inodes can be
39/// used for simple persistent indexing when combined with local offset.
40pub struct InodeAwareReader {
41    inner: Multireader<BufReader<File>>,
42    inodes: Vec<u64>,
43}
44
45impl InodeAwareReader {
46    /// Construct `InodeAwareMultireader` searching for up to two rotated logs.
47    pub fn from_rotated_logs(path: impl AsRef<Path>) -> io::Result<Self> {
48        Self::from_rotated_logs_with_depth(path, 2)
49    }
50
51    /// Construct `InodeAwareMultireader` searching for up to `max_depth` rotated logs.
52    pub fn from_rotated_logs_with_depth(
53        path: impl AsRef<Path>,
54        max_depth: usize,
55    ) -> io::Result<Self> {
56        let paths_and_inodes = glob_rotated_logs(path, max_depth)?;
57        let (paths, inodes): (Vec<_>, Vec<_>) = paths_and_inodes.into_iter().unzip();
58        let files = paths
59            .into_iter()
60            .map(|path| -> io::Result<BufReader<File>> { Ok(BufReader::new(File::open(path)?)) })
61            .collect::<io::Result<Vec<BufReader<File>>>>()?;
62        let multireader = Multireader::new(files)?;
63
64        Ok(Self {
65            inner: multireader,
66            inodes,
67        })
68    }
69
70    /// Get offset that can be used across restarts and log rotations.
71    pub fn get_persistent_offset(&self) -> InodeAwareOffset {
72        let inode = self.get_current_inode();
73        let offset = self.get_local_offset();
74        InodeAwareOffset { inode, offset }
75    }
76
77    /// Seek by persistent offset.
78    ///
79    /// Will return NotFound io error if file with given inode was not found.
80    pub fn seek_persistent(&mut self, offset: InodeAwareOffset) -> io::Result<()> {
81        let Some(inode_index) = self.get_item_index_by_inode(offset.inode) else {
82            return Err(io::Error::new(
83                io::ErrorKind::NotFound,
84                "provided inode does not exist",
85            ));
86        };
87        self.seek_by_local_index(inode_index, io::SeekFrom::Start(offset.offset))?;
88        Ok(())
89    }
90
91    /// Get slice of inodes for current execution.
92    pub fn get_inodes(&self) -> &[u64] {
93        &self.inodes
94    }
95
96    // Destroy struct and return underlying reader and inodes.
97    pub fn into_inner(self) -> (Multireader<BufReader<File>>, Vec<u64>) {
98        (self.inner, self.inodes)
99    }
100
101    /// Get inode of an item that is currently read.
102    pub fn get_current_inode(&self) -> u64 {
103        let item_index = self.get_current_item_index();
104        self.inodes[item_index]
105    }
106
107    /// Search for item index by given inode.
108    pub fn get_item_index_by_inode(&self, inode: u64) -> Option<usize> {
109        self.get_inodes()
110            .iter()
111            .cloned()
112            .enumerate()
113            .find(|&(_, i)| i == inode)
114            .map(|(idx, _)| idx)
115    }
116
117    /// Compare two offsets as if they were pointing into one large buffer. Returns None if any of the offsets do not belong
118    /// to underlying files.
119    pub fn compare_offsets(
120        &self,
121        first: InodeAwareOffset,
122        second: InodeAwareOffset,
123    ) -> Option<Ordering> {
124        let first_index = self.get_item_index_by_inode(first.inode)?;
125        let second_index = self.get_item_index_by_inode(second.inode)?;
126        if first_index == second_index {
127            return Some(first.offset.cmp(&second.offset));
128        }
129        Some(first_index.cmp(&second_index))
130    }
131}
132
133impl Deref for InodeAwareReader {
134    type Target = Multireader<BufReader<File>>;
135
136    fn deref(&self) -> &Self::Target {
137        &self.inner
138    }
139}
140
141impl DerefMut for InodeAwareReader {
142    fn deref_mut(&mut self) -> &mut Self::Target {
143        &mut self.inner
144    }
145}