dirx 0.1.0

Creates an in-memory index of all the files in a directory tree, and allows efficient scanning of only those files that have been modified since the index got created
Documentation
use crate::index::DirIndex;
use crate::index::ExtractStale;
use crate::index::IndexedMeta;
use std::fmt;
use std::fs::File;
use std::fs::Metadata;
use std::io;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::result;

mod error;
mod event;
mod fs;
mod options;

use error::IntoScanError;
use fs::open_at;
use fs::read_dir;
use fs::read_dir_at;
use fs::DirEntry;
use fs::FileType;
use fs::ReadDir;
use fs::SameFile;

pub use error::Error;
pub use error::ErrorOperation;
pub use error::FilesystemLoopError;
pub use event::Event;
pub use options::ScanOptions;

macro_rules! ok {
    ( $expr:expr ) => {
        match $expr {
            Ok(ok) => ok,
            Err(err) => return Some(Err(err)),
        }
    };
}

#[cfg(feature = "unstable")]
#[allow(non_upper_case_globals)]
const FilesystemLoop: io::ErrorKind = io::ErrorKind::FilesystemLoop;

#[cfg(not(feature = "unstable"))]
#[allow(non_upper_case_globals)]
const FilesystemLoop: io::ErrorKind = io::ErrorKind::Other;

pub type Result<T> = result::Result<T, Error>;

#[must_use = "this is an iterator that must be consumed for the scan to complete"]
pub struct Scanner<'i, T, F> {
    opts: ScanOptions,
    index: &'i mut DirIndex<T>,
    stack: Vec<ReadDir>,
    stales: ExtractStale<T>,
    generator: F,
}

impl<'i, T, F> Scanner<'i, T, F>
where
    F: FnMut(&Path, &mut File, &Metadata) -> io::Result<T>,
{
    #[inline]
    fn new<P: AsRef<Path>>(
        mut opts: ScanOptions,
        index: &'i mut DirIndex<T>,
        path: P,
        generator: F,
    ) -> io::Result<Self> {
        let mut dir = read_dir(path)?;
        if let Some(prefix) = opts.add_prefix.take() {
            dir.path = prefix;
        }
        let stack = vec![dir];
        let stales = index.extract_stale();

        index.mark_stale();

        Ok(Self {
            opts,
            index,
            stack,
            generator,
            stales,
        })
    }

    #[inline]
    pub fn index(&self) -> &DirIndex<T> {
        &*self.index
    }

    #[inline]
    pub fn index_mut(&mut self) -> &mut DirIndex<T> {
        self.index
    }

    fn enter_dir(&mut self, dir: ReadDir) -> Result<()> {
        let loop_start_path = self.stack.iter().find_map(|prev| {
            if dir.is_same_as(prev) {
                Some(prev.path.clone())
            } else {
                None
            }
        });
        if let Some(loop_start_path) = loop_start_path {
            return Err(Error::from_io_error(
                dir.path,
                ErrorOperation::OpenDirectory,
                io::Error::new(FilesystemLoop, FilesystemLoopError::new(loop_start_path)),
            ));
        }

        self.stack.push(dir);
        Ok(())
    }

    fn leave_dir(&mut self) {
        self.stack.pop();
    }

    fn scan_next(&mut self) -> Option<Result<Event>> {
        loop {
            let cur = self.stack.last_mut()?;

            let dir_entry = match cur.next() {
                Some(Ok(dir_entry)) => dir_entry,
                Some(Err(err)) => return Some(Err(err)),
                None => {
                    self.leave_dir();
                    continue;
                }
            };

            match dir_entry.file_type {
                FileType::File => {
                    if let Some(res) = self.scan_file(dir_entry) {
                        return Some(res);
                    }
                }
                FileType::Directory => {
                    if let Err(err) = self.scan_dir(dir_entry) {
                        return Some(Err(err));
                    }
                }
                FileType::Other => (),
            }
        }
    }

    fn scan_dir(&mut self, dir_entry: DirEntry) -> Result<()> {
        let cur = self
            .stack
            .last()
            .expect("stack cannot be empty when calling `scan_dir`");
        let name = dir_entry.file_name();

        if name == "." || name == ".." {
            return Ok(());
        }

        let dir = read_dir_at(cur, name)?;

        if self.opts.one_file_system && dir.metadata.dev() != cur.metadata.dev() {
            return Ok(());
        }

        self.enter_dir(dir)
    }

    fn scan_file(&mut self, dir_entry: DirEntry) -> Option<Result<Event>> {
        let cur = self
            .stack
            .last()
            .expect("stack cannot be empty when calling `scan_file`");
        let name = dir_entry.file_name();
        let path = cur.path.join(name);

        let mut file = ok!(open_at(cur, name));
        let md = match file.metadata() {
            Ok(md) => md,
            Err(err) => return Some(Err(err.into_scan_error(path, ErrorOperation::ReadMetadata))),
        };

        // Since the time the `DirEntry` was obtained, to the time the file gets opened, it
        // is possible that the file got replaced, and maybe became something that is not a file.
        if !md.file_type().is_file() {
            return None;
        }

        let size = md.size();
        let modified = match md.modified() {
            Ok(modified) => modified,
            Err(err) => return Some(Err(err.into_scan_error(path, ErrorOperation::ReadMetadata))),
        };

        if let Some((meta, data)) = self.index.get_with_meta_mut(&path) {
            if size == meta.size && modified == meta.modified {
                self.index.touch(&path);
                return None;
            }

            (*meta, *data) = ok!(Self::prepare_file(
                &mut self.generator,
                &path,
                &mut file,
                &md
            ));
            self.index.touch(&path);
            Some(Ok(Event::Modified {
                path,
                file,
                metadata: md,
            }))
        } else {
            let (meta, data) = ok!(Self::prepare_file(
                &mut self.generator,
                &path,
                &mut file,
                &md
            ));
            self.index.insert(path.clone(), meta, data);
            Some(Ok(Event::Added {
                path,
                file,
                metadata: md,
            }))
        }
    }

    fn prepare_file<P: AsRef<Path>>(
        generator: &mut F,
        path: P,
        file: &mut File,
        md: &Metadata,
    ) -> Result<(IndexedMeta, T)> {
        let path = path.as_ref();
        let meta = md.try_into().map_err(|err: io::Error| {
            err.into_scan_error(path.to_path_buf(), ErrorOperation::ReadMetadata)
        })?;
        let data = (generator)(path, file, md)
            .map_err(|err| err.into_scan_error(path.to_path_buf(), ErrorOperation::GenerateData))?;
        Ok((meta, data))
    }

    fn remove_next(&mut self) -> Option<Result<Event>> {
        self.stales
            .next(self.index)
            .map(|(path, _)| Ok(Event::Removed { path }))
    }
}

impl<'i, T, F> Iterator for Scanner<'i, T, F>
where
    F: FnMut(&Path, &mut File, &Metadata) -> io::Result<T>,
{
    type Item = Result<Event>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        self.scan_next().or_else(|| self.remove_next())
    }
}

impl<'i, T, F> fmt::Debug for Scanner<'i, T, F> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Scanner")
            .field("opts", &self.opts)
            .field("index", &self.index)
            .finish_non_exhaustive()
    }
}