oxyromon 0.22.0

Rusty ROM OrgaNizer
use super::config::{add_to_list, remove_from_list, set_bool, set_directory, set_string};
use super::database::*;
use super::download_dats::download_redump_system;
use super::progress::*;
use super::purge_systems::purge_system;
use super::server::SseMessage;
use super::validator::*;
use async_graphql::{Context, Object, Result};
use serde_json::json;
use sqlx::SqlitePool;
use tokio::sync::broadcast;

pub struct Mutation;

#[Object]
impl Mutation {
    async fn add_to_list(
        &self,
        ctx: &Context<'_>,
        key: String,
        value: String,
        system_id: Option<i64>,
    ) -> Result<bool> {
        log::debug!("mutation::add_to_list({}, {})", key, value);
        let pool = ctx.data_unchecked::<SqlitePool>();
        let progress_bar = get_progress_bar(0, get_none_progress_style());
        add_to_list(
            &mut pool.acquire().await.unwrap(),
            &progress_bar,
            &key,
            &value,
            system_id,
        )
        .await;
        Ok(true)
    }

    async fn remove_from_list(
        &self,
        ctx: &Context<'_>,
        key: String,
        value: String,
        system_id: Option<i64>,
    ) -> Result<bool> {
        log::debug!("mutation::remove_to_list({}, {})", key, value);
        let pool = ctx.data_unchecked::<SqlitePool>();
        let progress_bar = get_progress_bar(0, get_none_progress_style());
        remove_from_list(
            &mut pool.acquire().await.unwrap(),
            &progress_bar,
            &key,
            &value,
            system_id,
        )
        .await;
        Ok(true)
    }

    async fn set_bool(
        &self,
        ctx: &Context<'_>,
        key: String,
        value: bool,
        system_id: Option<i64>,
    ) -> Result<bool> {
        log::debug!("mutation::set_bool({}, {})", key, value);
        let pool = ctx.data_unchecked::<SqlitePool>();
        set_bool(&mut pool.acquire().await.unwrap(), &key, value, system_id).await;
        Ok(true)
    }

    async fn set_prefer_regions(
        &self,
        ctx: &Context<'_>,
        #[graphql(validator(custom = "PreferRegionValidator::new()"))] value: String,
        system_id: Option<i64>,
    ) -> Result<bool> {
        log::debug!("mutation::set_prefer_regions({})", value);
        let pool = ctx.data_unchecked::<SqlitePool>();
        set_string(
            &mut pool.acquire().await.unwrap(),
            "PREFER_REGIONS",
            &value,
            system_id,
        )
        .await;
        Ok(true)
    }

    async fn set_prefer_versions(
        &self,
        ctx: &Context<'_>,
        #[graphql(validator(custom = "PreferVersionValidator::new()"))] value: String,
        system_id: Option<i64>,
    ) -> Result<bool> {
        log::debug!("mutation::set_prefer_versions({})", value);
        let pool = ctx.data_unchecked::<SqlitePool>();
        set_string(
            &mut pool.acquire().await.unwrap(),
            "PREFER_VERSIONS",
            &value,
            system_id,
        )
        .await;
        Ok(true)
    }

    async fn set_subfolder_scheme(
        &self,
        ctx: &Context<'_>,
        key: String,
        #[graphql(validator(custom = "SubfolderSchemeValidator::new()"))] value: String,
        system_id: Option<i64>,
    ) -> Result<bool> {
        log::debug!("mutation::set_subfolder_scheme({}, {})", key, value);
        let pool = ctx.data_unchecked::<SqlitePool>();
        set_string(&mut pool.acquire().await.unwrap(), &key, &value, system_id).await;
        Ok(true)
    }

    async fn set_directory(
        &self,
        ctx: &Context<'_>,
        key: String,
        #[graphql(validator(custom = "DirectoryValidator::new()"))] value: String,
        system_id: Option<i64>,
    ) -> Result<bool> {
        log::debug!("mutation::set_directory({}, {})", key, value);
        let pool = ctx.data_unchecked::<SqlitePool>();
        set_directory(&mut pool.acquire().await.unwrap(), &key, &value, system_id).await;
        Ok(true)
    }

    /// Fetch the named Redump systems' DAT files and import them.
    ///
    /// Returns as soon as the work is handed to a background task; progress and
    /// the outcome arrive over SSE, the same way importing an uploaded DAT does.
    async fn download_dats(&self, ctx: &Context<'_>, systems: Vec<String>) -> Result<bool> {
        log::debug!("mutation::download_dats({:?})", systems);
        let pool = ctx.data_unchecked::<SqlitePool>().clone();
        let sse_tx = ctx
            .data_unchecked::<broadcast::Sender<SseMessage>>()
            .clone();

        tokio::spawn(async move {
            let mut connection = pool.acquire().await.unwrap();
            let progress_bar = ProgressBar::hidden();
            let total = systems.len();

            let _ = sse_tx.send(SseMessage {
                event: "download_dats_started".to_string(),
                data: json!({
                    "total": total,
                    "message": format!("Downloading {} DAT file(s)", total),
                })
                .to_string(),
            });

            // Deliberately quiet between the two: a per-system event would mean
            // a toast each, and this list can run to the whole catalogue.
            let mut failed: Vec<String> = Vec::new();
            for system_name in systems.iter() {
                if let Err(e) =
                    download_redump_system(&mut connection, &progress_bar, system_name, false).await
                {
                    log::error!("Failed to download {}: {:#}", system_name, e);
                    failed.push(system_name.clone());
                }
            }

            if failed.is_empty() {
                let _ = sse_tx.send(SseMessage {
                    event: "download_dats_complete".to_string(),
                    data: json!({
                        "total": total,
                        "success": true,
                        "message": format!("Downloaded {} DAT file(s)", total),
                    })
                    .to_string(),
                });
            } else {
                let _ = sse_tx.send(SseMessage {
                    event: "download_dats_error".to_string(),
                    data: json!({
                        "success": false,
                        "failed": failed,
                        "message": format!("Failed to download: {}", failed.join(", ")),
                    })
                    .to_string(),
                });
            }
        });

        Ok(true)
    }

    async fn purge_system(&self, ctx: &Context<'_>, system_id: i64) -> Result<bool> {
        log::debug!("mutation::purge_system({})", system_id);
        let pool = ctx.data_unchecked::<SqlitePool>().clone();
        let sse_tx = ctx
            .data_unchecked::<broadcast::Sender<SseMessage>>()
            .clone();
        let mut connection = pool.acquire().await.unwrap();

        let system = find_system_by_id(&mut connection, system_id).await;
        let system_name = system.name.clone();

        // Spawn background task for deletion
        tokio::spawn(async move {
            let mut connection = pool.acquire().await.unwrap();
            let progress_bar = ProgressBar::hidden();

            // Send start notification
            let _ = sse_tx.send(SseMessage {
                event: "purge_started".to_string(),
                data: json!({
                    "system_id": system_id,
                    "system_name": system_name,
                    "message": format!("Starting deletion of system '{}'", system_name)
                })
                .to_string(),
            });

            // Perform the actual deletion
            match purge_system(&mut connection, &progress_bar, &system).await {
                Ok(_) => {
                    let _ = sse_tx.send(SseMessage {
                        event: "purge_complete".to_string(),
                        data: json!({
                            "system_id": system_id,
                            "system_name": system_name,
                            "success": true,
                            "message": format!("System '{}' has been successfully deleted", system_name)
                        }).to_string(),
                    });
                    log::info!("Successfully purged system: {}", system_name);
                }
                Err(e) => {
                    let _ = sse_tx.send(SseMessage {
                        event: "purge_error".to_string(),
                        data: json!({
                            "system_id": system_id,
                            "system_name": system_name,
                            "success": false,
                            "error": format!("{:#}", e),
                            "message": format!("Failed to delete system '{}': {:#}", system_name, e)
                        })
                        .to_string(),
                    });
                    log::error!("Failed to purge system {}: {:#}", system_name, e);
                }
            }
        });

        Ok(true)
    }
}