teamy-mft 0.7.1

TeamDman's Master File Table CLI and library for NTFS.
use crate::mft::mft_convert_to_path_collection::convert_mft_file_to_path_collection;
use crate::mft::mft_file::MftFile;
use crate::search_index::format::SearchIndexHeader;
use crate::search_index::format::SearchIndexPathRow;
use crate::search_index::search_index_bytes::SearchIndexBytesMut;
use crate::sync::DriveSyncInfo;
use crate::sync::IfExistsOutputBehaviour;
use eyre::Context;
use eyre::bail;
use itertools::Itertools;
use tracing::debug;
use tracing::info;
use tracing::info_span;
use uom::si::information::byte;

#[derive(Debug)]
pub struct SyncIndex;

impl SyncIndex {
    /// Validate the sync can proceed before any index writes begin.
    ///
    /// # Errors
    ///
    /// Returns an error if `if_exists` is `Abort` and any index output already exists.
    pub fn invoke_preflight(
        drive_infos: Vec<DriveSyncInfo>,
        if_exists: &IfExistsOutputBehaviour,
    ) -> eyre::Result<Vec<DriveSyncInfo>> {
        let mut rtn = Vec::with_capacity(drive_infos.len());
        for info in drive_infos {
            let index_exists = info.index_output_path.exists();
            match (index_exists, if_exists) {
                (false, _) | (true, IfExistsOutputBehaviour::Overwrite) => {
                    rtn.push(info);
                }
                (true, IfExistsOutputBehaviour::Skip) => {
                    debug!(
                        drive = %info.drive_letter,
                        path = %info.index_output_path.display(),
                        "Skipping existing index output"
                    );
                }
                (true, IfExistsOutputBehaviour::Abort) => {
                    bail!(
                        "Aborting sync: {} already exists",
                        info.index_output_path.display()
                    );
                }
            }
        }
        Ok(rtn)
    }

    /// Build `.mft_search_index` files from cached MFT snapshots.
    ///
    /// Does not call the preflight check.
    ///
    /// # Errors
    ///
    /// Returns an error if the sync directory cannot be retrieved, matching drives cannot be
    /// resolved, or index files cannot be read, built, or written.
    pub fn invoke(drive_infos: Vec<DriveSyncInfo>) -> eyre::Result<()> {
        info!(
            "Building search indexes for drives: {}",
            drive_infos.iter().map(|info| info.drive_letter).join(", ")
        );

        for info in drive_infos {
            let _span = info_span!(
                "sync_index_for_drive",
                drive = %info.drive_letter,
                mft_path = %info.mft_output_path.display(),
                index_path = %info.index_output_path.display(),
            )
            .entered();
            Self::invoke_for_mft_path(&info)?;
        }

        info!("Index sync stage completed");

        Ok(())
    }

    /// Build a search index from an already-loaded in-memory `MftFile`.
    ///
    /// # Errors
    ///
    /// Returns an error if path conversion or index writing fails.
    pub fn invoke_for_mft_file(info: &DriveSyncInfo, mft_file: &MftFile) -> eyre::Result<()> {
        info!(
            drive_letter = %info.drive_letter,
            mft_path = %info.mft_output_path.display(),
            "Building search index",
        );
        let rows = {
            let _span = info_span!(
                "build_search_index_rows",
                drive = %info.drive_letter,
                mft_size = %mft_file.size().get::<byte>(),
            )
            .entered();
            Self::build_rows_for_mft_file(info, mft_file)?
        };
        {
            let _span = info_span!(
                "write_search_index_output",
                drive = %info.drive_letter,
                row_count = rows.len(),
                output_path = %info.index_output_path.display(),
            )
            .entered();
            Self::write_index_output(info, mft_file, &rows)
        }
    }

    fn invoke_for_mft_path(info: &DriveSyncInfo) -> eyre::Result<()> {
        if !info.mft_output_path.is_file() {
            bail!(
                "Cannot build index for drive {}: missing {}",
                info.drive_letter,
                info.mft_output_path.display()
            );
        }

        let mft_file = {
            let _span = info_span!(
                "load_cached_mft_for_index",
                drive = %info.drive_letter,
                mft_path = %info.mft_output_path.display(),
            )
            .entered();
            MftFile::from_path(&info.mft_output_path).wrap_err_with(|| {
                format!(
                    "Failed parsing MFT snapshot for drive {} from {}",
                    info.drive_letter,
                    info.mft_output_path.display()
                )
            })?
        };

        Self::invoke_for_mft_file(info, &mft_file)
    }

    fn build_rows_for_mft_file(
        info: &DriveSyncInfo,
        mft_file: &MftFile,
    ) -> eyre::Result<Vec<SearchIndexPathRow>> {
        let drive_name = info.drive_letter.to_string();

        let files =
            convert_mft_file_to_path_collection(&drive_name, mft_file).wrap_err_with(|| {
                format!(
                    "Failed processing MFT data for drive {} from {}",
                    info.drive_letter,
                    info.mft_output_path.display()
                )
            })?;

        Ok(files
            .0
            .into_iter()
            .flatten()
            .map(|path| SearchIndexPathRow {
                path: path.path.to_string_lossy().into_owned(),
                has_deleted_entries: path.has_deleted_entries(),
            })
            .collect())
    }

    fn write_index_output(
        info: &DriveSyncInfo,
        mft_file: &MftFile,
        rows: &[SearchIndexPathRow],
    ) -> eyre::Result<()> {
        SearchIndexBytesMut::from_rows(
            SearchIndexHeader::new(
                info.drive_letter,
                mft_file.size().get::<byte>() as u64,
                rows.len() as u64,
            ),
            rows,
        )?
        .write_to_path(&info.index_output_path)
        .wrap_err_with(|| {
            format!(
                "Failed writing index output for drive {} to {}",
                info.drive_letter,
                info.index_output_path.display()
            )
        })
    }
}