oxyromon 0.23.0

Rusty ROM OrgaNizer
use super::chdman;
use super::common::*;
use super::database::*;
use super::dolphin;
use super::dolphin::AsRvz;
use super::maxcso;
use super::maxcso::AsXso;
use super::mimetype::*;
use super::model::*;
use super::nsz;
use super::nsz::AsNsz;
use super::progress::*;
use super::prompt::*;
use super::sevenzip;
use super::sevenzip::AsArchive;
use super::transcode::romfile_as_chd;
use super::util::*;
use anyhow::{Result, anyhow, bail};
use clap::{Arg, ArgAction, ArgMatches, Command};
use indicatif::ProgressBar;
use sqlx::sqlite::SqliteConnection;
use std::collections::HashMap;

pub fn subcommand() -> Command {
    Command::new("check-roms")
        .about("Check ROM files' integrity")
        .arg(
            Arg::new("ALL")
                .short('a')
                .long("all")
                .help("Check all systems")
                .required(false)
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("GAME")
                .short('g')
                .long("game")
                .help("Select games by name")
                .required(false)
                .action(ArgAction::Append),
        )
        .arg(
            Arg::new("SIZE")
                .short('s')
                .long("size")
                .help("Recalculate ROM file sizes")
                .required(false)
                .action(ArgAction::SetTrue),
        )
}

pub async fn main(
    connection: &mut SqliteConnection,
    matches: &ArgMatches,
    progress_bar: &ProgressBar,
) -> Result<()> {
    let systems =
        prompt_for_systems(connection, None, false, false, matches.get_flag("ALL")).await?;
    for system in systems {
        print_header(progress_bar, &format!("Checking \"{}\"", system.name));
        let games = match matches.get_many::<String>("GAME") {
            Some(game_names) => {
                let mut games: Vec<Game> = vec![];
                for game_name in game_names {
                    games.append(
                        &mut find_full_games_by_name_and_system_id(
                            connection, game_name, system.id,
                        )
                        .await,
                    );
                }
                games.dedup_by_key(|game| game.id);
                prompt_for_games(games, cfg!(test))?
            }
            None => find_full_games_by_system_id(connection, system.id).await,
        };

        if games.is_empty() {
            if matches.index_of("GAME").is_some() {
                print_warning(progress_bar, "No matching games found");
            }
            continue;
        }
        check_system(
            connection,
            progress_bar,
            &system,
            games,
            matches.get_flag("SIZE"),
        )
        .await?;
        print_separator(progress_bar);
    }
    Ok(())
}

async fn check_system(
    connection: &mut SqliteConnection,
    progress_bar: &ProgressBar,
    system: &System,
    games: Vec<Game>,
    size: bool,
) -> Result<()> {
    let roms = find_original_roms_with_romfile_by_game_ids(
        connection,
        &games.iter().map(|game| game.id).collect::<Vec<i64>>(),
    )
    .await;
    let romfiles = find_romfiles_by_ids(
        connection,
        roms.iter()
            .map(|rom| rom.romfile_id.unwrap())
            .collect::<Vec<i64>>()
            .as_slice(),
    )
    .await;
    let mut roms_by_romfile_id: HashMap<i64, Vec<&Rom>> = HashMap::new();
    roms.iter().for_each(|rom| {
        let group = roms_by_romfile_id
            .entry(rom.romfile_id.unwrap())
            .or_default();
        group.push(rom);
    });
    let header = find_header_by_system_id(connection, system.id).await;

    let mut transaction = begin_transaction(connection).await;

    let mut errors = 0;

    for romfile in &romfiles {
        let romfile_path = romfile.as_common(&mut transaction).await?.path;
        let romfile_extension = romfile_path.extension().unwrap().to_str().unwrap();
        let romfile_roms = roms_by_romfile_id.remove(&romfile.id).unwrap();

        print_subheader(
            progress_bar,
            &format!(
                "Processing \"{}\"",
                romfile_path.file_name().unwrap().to_str().unwrap()
            ),
        );

        let result;
        if ARCHIVE_EXTENSIONS.contains(&romfile_extension) {
            if sevenzip::get_version().await.is_err() {
                print_error(progress_bar, "Required tool not found: sevenzip");
                break;
            }
            result = check_archive(
                &mut transaction,
                progress_bar,
                &header,
                romfile,
                romfile_roms,
            )
            .await;
        } else if CHD_EXTENSION == romfile_extension {
            if chdman::get_version().await.is_err() {
                print_error(progress_bar, "Required tool not found: chdman");
                break;
            }
            let chd_romfile = romfile_as_chd(&mut transaction, romfile).await?;
            result = chd_romfile
                .check(&mut transaction, progress_bar, &header, &romfile_roms)
                .await;
        } else if CSO_EXTENSION == romfile_extension {
            if maxcso::get_version().await.is_err() {
                print_error(progress_bar, "Required tool not found: maxcso");
                break;
            }
            result = romfile
                .as_common(&mut transaction)
                .await?
                .as_xso()
                .await?
                .check(&mut transaction, progress_bar, &header, &romfile_roms)
                .await;
        } else if NSZ_EXTENSION == romfile_extension {
            if nsz::get_version().await.is_err() {
                print_error(progress_bar, "Required tool not found: nsz");
                break;
            }
            result = romfile
                .as_common(&mut transaction)
                .await?
                .as_nsz()?
                .check(&mut transaction, progress_bar, &header, &romfile_roms)
                .await;
        } else if RVZ_EXTENSION == romfile_extension {
            if dolphin::get_version().await.is_err() {
                print_error(progress_bar, "Required tool not found: dolphin-tool");
                break;
            }
            result = romfile
                .as_common(&mut transaction)
                .await?
                .as_rvz()?
                .check(&mut transaction, progress_bar, &header, &romfile_roms)
                .await;
        } else if ZSO_EXTENSION == romfile_extension {
            if maxcso::get_version().await.is_err() {
                print_error(progress_bar, "Required tool not found: maxcso");
                break;
            }
            result = romfile
                .as_common(&mut transaction)
                .await?
                .as_xso()
                .await?
                .check(&mut transaction, progress_bar, &header, &romfile_roms)
                .await;
        } else {
            result = romfile
                .as_common(&mut transaction)
                .await?
                .check(&mut transaction, progress_bar, &header, &romfile_roms)
                .await;
        }

        if result.is_err() {
            errors += 1;
            print_error(
                progress_bar,
                &format!(
                    "Integrity check failed for \"{}\"",
                    romfile_path.file_name().unwrap().to_str().unwrap()
                ),
            );
            move_to_trash(&mut transaction, progress_bar, system, romfile).await?;
        } else {
            print_success(
                progress_bar,
                &format!(
                    "\"{}\" OK",
                    romfile_path.file_name().unwrap().to_str().unwrap()
                ),
            );
            if size {
                romfile
                    .as_common(&mut transaction)
                    .await?
                    .update(&mut transaction, progress_bar, romfile.id)
                    .await?;
            }
        }
    }

    let checked = romfiles.len();
    let passed = checked - errors;
    if errors > 0 {
        print_warning(
            progress_bar,
            &format!(
                "{}/{} files passed, {} moved to trash",
                passed, checked, errors
            ),
        );
        compute_system_completion(&mut transaction, progress_bar, system).await?;
    } else {
        print_success(
            progress_bar,
            &format!("All {} files passed integrity check", checked),
        );
    }

    commit_transaction(transaction).await;

    Ok(())
}

async fn check_archive(
    connection: &mut SqliteConnection,
    progress_bar: &ProgressBar,
    header: &Option<Header>,
    romfile: &Romfile,
    roms: Vec<&Rom>,
) -> Result<()> {
    let archive_romfiles = romfile
        .as_common(connection)
        .await?
        .as_archive(progress_bar, None)
        .await?;
    if archive_romfiles.len() != roms.len() {
        bail!("Archive contains a different number of ROM files");
    }
    for archive_romfile in archive_romfiles {
        let rom = roms
            .iter()
            .find(|rom| rom.name == archive_romfile.path)
            .ok_or_else(|| anyhow!("file not found in DB {}", archive_romfile.path))?;
        archive_romfile
            .check(connection, progress_bar, header, &[rom])
            .await?;
    }
    Ok(())
}

async fn move_to_trash(
    connection: &mut SqliteConnection,
    progress_bar: &ProgressBar,
    system: &System,
    romfile: &Romfile,
) -> Result<()> {
    let new_path = get_trash_directory(connection, Some(system)).await?.join(
        romfile
            .as_common(connection)
            .await?
            .path
            .file_name()
            .unwrap(),
    );
    romfile
        .as_common(connection)
        .await?
        .rename(progress_bar, &new_path, false)
        .await?
        .update(connection, progress_bar, romfile.id)
        .await?;
    Ok(())
}

#[cfg(test)]
mod test_cso;
#[cfg(test)]
mod test_iso_chd;
#[cfg(test)]
mod test_multiple_tracks_chd;
#[cfg(test)]
mod test_original;
#[cfg(test)]
mod test_original_crc_mismatch;
#[cfg(test)]
mod test_original_size_mismatch;
#[cfg(test)]
mod test_original_with_header;
#[cfg(test)]
mod test_rvz;
#[cfg(test)]
mod test_sevenzip;
#[cfg(test)]
mod test_sevenzip_with_header;
#[cfg(test)]
mod test_zip;
#[cfg(test)]
mod test_zso;