From 20d645a9159cf871e9c6c2ad887877e85ced625e Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Fri, 3 Nov 2023 07:08:46 +0900 Subject: [PATCH] feat: change sender to async fn - To emit progress to front - Custom Error(For i18n but not completed yet) - tauri: change args --- Cargo.lock | 15 +- dar2oar_cli/Cargo.toml | 3 +- dar2oar_cli/src/lib.rs | 24 +- dar2oar_core/Cargo.toml | 1 + dar2oar_core/benches/convert_n_thread.rs | 27 ++- .../src/conditions/namespace_config.rs | 8 +- dar2oar_core/src/error.rs | 27 +++ dar2oar_core/src/fs/async_closure.rs | 4 + dar2oar_core/src/fs/mapping_table.rs | 13 +- dar2oar_core/src/fs/mod.rs | 153 +++++++------ dar2oar_core/src/fs/parallel.rs | 84 ++++--- dar2oar_core/src/fs/sequential.rs | 206 +++++++++--------- dar2oar_core/src/lib.rs | 4 +- frontend/src/tauri_cmd/index.ts | 25 ++- src-tauri/Cargo.toml | 2 + src-tauri/src/cmd.rs | 103 +++------ src-tauri/src/convert_option.rs | 65 ++++++ src-tauri/src/main.rs | 1 + 18 files changed, 427 insertions(+), 338 deletions(-) create mode 100644 dar2oar_core/src/error.rs create mode 100644 dar2oar_core/src/fs/async_closure.rs create mode 100644 src-tauri/src/convert_option.rs diff --git a/Cargo.lock b/Cargo.lock index 2bd4c89..6508d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,17 @@ version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "async-walkdir" version = "0.2.0" @@ -783,7 +794,6 @@ dependencies = [ "anyhow", "clap", "dar2oar_core", - "log", "pretty_assertions", "tokio", "tracing", @@ -800,6 +810,7 @@ dependencies = [ "jwalk", "log", "nom", + "once_cell", "pretty_assertions", "serde", "serde_json", @@ -1213,9 +1224,11 @@ name = "g_dar2oar" version = "0.1.6" dependencies = [ "anyhow", + "async-trait", "dar2oar_core", "once_cell", "pretty_assertions", + "serde", "tauri", "tauri-build", "tracing", diff --git a/dar2oar_cli/Cargo.toml b/dar2oar_cli/Cargo.toml index c8fce51..46abde8 100644 --- a/dar2oar_cli/Cargo.toml +++ b/dar2oar_cli/Cargo.toml @@ -18,14 +18,13 @@ path = "./src/main.rs" anyhow = { version = "1.0.75", features = ["backtrace"] } clap = { version = "4.4.4", features = ["derive"] } # For CLI dar2oar_core = { path = "../dar2oar_core" } -log = "0.4.20" # Logger tokio = { version = "1.33.0", features = [ "fs", "rt", "rt-multi-thread", "macros", ] } -tracing = "0.1.40" +tracing = "0.1.40" # Logger tracing-subscriber = "0.3.17" [dev-dependencies] diff --git a/dar2oar_cli/src/lib.rs b/dar2oar_cli/src/lib.rs index 649b0c4..bf3e877 100644 --- a/dar2oar_cli/src/lib.rs +++ b/dar2oar_cli/src/lib.rs @@ -1,7 +1,7 @@ use clap::{arg, Parser}; use dar2oar_core::{ convert_dar_to_oar, - fs::{parallel, ConvertOptions}, + fs::{async_closure::AsyncClosure, parallel, ConvertOptions}, read_mapping_table, }; use std::fs::File; @@ -71,13 +71,21 @@ pub async fn run_cli(args: Args) -> anyhow::Result<()> { section_table: read_table!(args.mapping_file), section_1person_table: read_table!(args.mapping_1person_file), hide_dar: args.hide_dar, - ..Default::default() }; - let msg = match args.run_parallel { - true => parallel::convert_dar_to_oar(config).await, - false => convert_dar_to_oar(config).await, - }?; - log::debug!("{}", msg); - Ok(()) + let res = match args.run_parallel { + true => parallel::convert_dar_to_oar(config, AsyncClosure::default).await, + false => convert_dar_to_oar(config, AsyncClosure::default).await, + }; + + match res { + Ok(msg) => { + tracing::info!("{}", msg); + Ok(()) + } + Err(err) => { + tracing::error!("{}", err); + anyhow::bail!("{}", err) + } + } } diff --git a/dar2oar_core/Cargo.toml b/dar2oar_core/Cargo.toml index 4df2d7b..6181e4e 100644 --- a/dar2oar_core/Cargo.toml +++ b/dar2oar_core/Cargo.toml @@ -34,6 +34,7 @@ criterion = { version = "0.5.1", features = [ "async_tokio", "html_reports", ] } +once_cell = "1.18.0" tracing-appender = "0.2" pretty_assertions = "1.4.0" tracing-subscriber = "0.3.17" diff --git a/dar2oar_core/benches/convert_n_thread.rs b/dar2oar_core/benches/convert_n_thread.rs index 3d4de01..40d1bec 100644 --- a/dar2oar_core/benches/convert_n_thread.rs +++ b/dar2oar_core/benches/convert_n_thread.rs @@ -1,5 +1,6 @@ use criterion::async_executor::FuturesExecutor; use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use dar2oar_core::fs::async_closure::AsyncClosure; use dar2oar_core::{ convert_dar_to_oar, fs::{parallel, ConvertOptions}, @@ -24,11 +25,14 @@ fn criterion_benchmark(c: &mut Criterion) { } let mapping = read_mapping_table(TABLE_PATH).await.unwrap(); - parallel::convert_dar_to_oar(black_box(ConvertOptions { - dar_dir: TARGET, - section_table: Some(mapping), - ..Default::default() - })) + parallel::convert_dar_to_oar( + black_box(ConvertOptions { + dar_dir: TARGET, + section_table: Some(mapping), + ..Default::default() + }), + AsyncClosure::default, + ) .await }) }); @@ -40,11 +44,14 @@ fn criterion_benchmark(c: &mut Criterion) { } let mapping = read_mapping_table(TABLE_PATH).await.unwrap(); - convert_dar_to_oar(black_box(ConvertOptions { - dar_dir: TARGET, - section_table: Some(mapping), - ..Default::default() - })) + convert_dar_to_oar( + black_box(ConvertOptions { + dar_dir: TARGET, + section_table: Some(mapping), + ..Default::default() + }), + AsyncClosure::default, + ) .await }) }); diff --git a/dar2oar_core/src/conditions/namespace_config.rs b/dar2oar_core/src/conditions/namespace_config.rs index a32f57d..d4d88aa 100644 --- a/dar2oar_core/src/conditions/namespace_config.rs +++ b/dar2oar_core/src/conditions/namespace_config.rs @@ -2,11 +2,11 @@ use serde::{Deserialize, Serialize}; /// name space config.json #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub struct MainConfig { +pub struct MainConfig<'a> { #[serde(default)] - pub name: String, + pub name: &'a str, #[serde(default)] - pub description: String, + pub description: &'a str, #[serde(default)] - pub author: String, + pub author: &'a str, } diff --git a/dar2oar_core/src/error.rs b/dar2oar_core/src/error.rs new file mode 100644 index 0000000..e2bb18a --- /dev/null +++ b/dar2oar_core/src/error.rs @@ -0,0 +1,27 @@ +#[derive(Debug, thiserror::Error)] +pub enum ConvertError { + #[error("Failed to write section config target: {0}")] + FailedWriteSectionConfig(String), + #[error("Neither 1st or 3rd person \"DynamicAnimationReplacer.mohidden\" found.")] + NotFoundUnhideTarget, + #[error("Not found \"OpenAnimationReplacer\" directory")] + NotFoundOarDir, + #[error("Not found \"DynamicAnimationReplacer\" directory")] + NotFoundDarDir, + #[error("Not found file name")] + NotFoundFileName, + #[error("This is not valid utf8")] + InvalidUtf8, + #[error(transparent)] + AnyhowError(#[from] anyhow::Error), + /// Convert json error. + #[error(transparent)] + JsonError(#[from] serde_json::Error), + #[error(transparent)] + ParseIntError(#[from] core::num::ParseIntError), + /// Represents all other cases of `std::io::Error`. + #[error(transparent)] + IOError(#[from] std::io::Error), +} + +pub type Result = core::result::Result; diff --git a/dar2oar_core/src/fs/async_closure.rs b/dar2oar_core/src/fs/async_closure.rs new file mode 100644 index 0000000..1034be3 --- /dev/null +++ b/dar2oar_core/src/fs/async_closure.rs @@ -0,0 +1,4 @@ +pub struct AsyncClosure; +impl AsyncClosure { + pub async fn default(_: usize) {} +} diff --git a/dar2oar_core/src/fs/mapping_table.rs b/dar2oar_core/src/fs/mapping_table.rs index 43183a8..86d1b4b 100644 --- a/dar2oar_core/src/fs/mapping_table.rs +++ b/dar2oar_core/src/fs/mapping_table.rs @@ -1,4 +1,3 @@ -use anyhow::bail; use std::collections::HashMap; use std::path::Path; use tokio::{fs::File, io::AsyncReadExt}; @@ -7,13 +6,11 @@ pub async fn read_mapping_table( table_path: impl AsRef, ) -> anyhow::Result> { let mut file_contents = String::new(); - match File::open(table_path).await { - Ok(mut file) => match file.read_to_string(&mut file_contents).await { - Ok(_) => Ok(parse_mapping_table(&file_contents)), - Err(e) => bail!("Error reading file: {}", e), - }, - Err(e) => bail!("Error opening file: {}", e), - } + File::open(table_path) + .await? + .read_to_string(&mut file_contents) + .await?; + Ok(parse_mapping_table(&file_contents)) } fn parse_mapping_table(table: &str) -> HashMap { diff --git a/dar2oar_core/src/fs/mod.rs b/dar2oar_core/src/fs/mod.rs index 9310079..307f524 100644 --- a/dar2oar_core/src/fs/mod.rs +++ b/dar2oar_core/src/fs/mod.rs @@ -1,11 +1,12 @@ mod mapping_table; mod sequential; +pub mod async_closure; pub mod parallel; pub mod path_changer; use crate::conditions::{ConditionsConfig, MainConfig}; -use anyhow::{bail, Context as _}; +use crate::error::{ConvertError, Result}; use async_walkdir::WalkDir; use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -33,8 +34,26 @@ pub struct ConvertOptions<'a, P: AsRef> { pub section_1person_table: Option>, /// After converting to OAR, add mohidden to the DAR directory before conversion to treat it as a hidden directory. (for MO2 users) pub hide_dar: bool, +} - pub sender: Option>, +#[derive(Debug, Clone, thiserror::Error)] +pub enum ConvertedReport { + #[error("Conversion Completed.")] + Complete, + + #[error("Converted & Renamed 1st, 3rd person DAR")] + Renamed1rdAnd3rdPersonDar, + #[error("Converted & Renamed 1rd person DAR")] + Renamed1rdPersonDar, + #[error("Converted & Renamed 3rd person DAR")] + Renamed3rdPersonDar, + + #[error("Unhide 1st & 3rd person")] + Unhide1rdAnd3rdPerson, + #[error("Unhide 1rd person")] + Unhide1rdPerson, + #[error("Unhide 3rd person")] + Unhide3rdPerson, } async fn read_file

(file_path: P) -> io::Result @@ -47,29 +66,30 @@ where Ok(content) } -async fn write_section_config

(oar_dir: P, config_json: ConditionsConfig) -> anyhow::Result<()> +async fn write_json_to(target_path: impl AsRef, value: &T) -> Result<()> where - P: AsRef, + T: ?Sized + serde::Serialize, { - let target_path = oar_dir.as_ref().join("config.json"); - let mut config_file = fs::File::create(&target_path).await.with_context(|| { - let msg = format!("writing section config target: {:?}", target_path); - tracing::error!("{}", msg); - msg - })?; - let json = serde_json::to_string_pretty(&config_json)?; + let mut config_file = fs::File::create(target_path).await?; + let json = serde_json::to_string_pretty(value)?; config_file.write_all(json.as_bytes()).await?; Ok(()) } -/// If there is no name_space_config file, create one. +async fn write_section_config

(oar_dir: P, config_json: ConditionsConfig) -> Result<()> +where + P: AsRef, +{ + write_json_to(oar_dir.as_ref().join("config.json"), &config_json).await +} + /// If it exists, do nothing. (This behavior is intended to facilitate the creation of config files /// for 1st_person and 3rd_person.) async fn write_name_space_config

( oar_name_space_path: P, mod_name: &str, author: Option<&str>, -) -> anyhow::Result<()> +) -> Result<()> where P: AsRef, { @@ -79,23 +99,22 @@ where } let config_json = MainConfig { - name: mod_name.into(), - author: author.unwrap_or_default().into(), + name: mod_name, + author: author.unwrap_or_default(), ..Default::default() }; fs::create_dir_all(&oar_name_space_path).await?; - let mut config_file = fs::File::create(target_file).await?; - let json = serde_json::to_string_pretty(&config_json)?; - config_file.write_all(json.as_bytes()).await?; - Ok(()) + write_json_to(target_file, &config_json).await } + + /// # Returns -/// Report which dirs have been restored +/// Report which dirs have been shown /// /// # NOTE /// It is currently used only in GUI, but is implemented in Core as an API. -pub async fn restore_dar(dar_dir: impl AsRef) -> anyhow::Result { +pub async fn unhide_dar(dar_dir: impl AsRef) -> Result { let mut restored_dar = None; let mut restored_1st_dar = None; let mut entries = WalkDir::new(dar_dir); @@ -117,77 +136,51 @@ pub async fn restore_dar(dar_dir: impl AsRef) -> anyhow::Result { } } - let mut msg = String::new(); - if let Some(dar_root) = restored_dar.as_ref() { - let dist = dar_root - .as_os_str() - .to_string_lossy() - .replace(".mohidden", ""); - fs::rename(dar_root.clone(), dist).await?; - msg = format!("{}- Restored 3rd_person", msg); - } - if let Some(dar_root) = restored_1st_dar.as_ref() { - let dist = dar_root - .as_os_str() - .to_string_lossy() - .replace(".mohidden", ""); - fs::rename(dar_root.clone(), dist).await?; - msg = format!("{}\n- Restored 1rd_person", msg); + async fn rename_and_check(maybe_dar_root: Option<&PathBuf>) -> Result<()> { + if let Some(dar_root) = maybe_dar_root { + let dist = dar_root + .as_os_str() + .to_string_lossy() + .replace(".mohidden", ""); + fs::rename(dar_root.clone(), dist).await?; + } + Ok(()) } - if restored_dar.is_none() && restored_1st_dar.is_none() { - anyhow::bail!("Neither 1st or 3rd person DynamicAnimationReplacer.mohidden found.") - } else { - Ok(msg) + let _ = tokio::join!( + rename_and_check(restored_dar.as_ref()), + rename_and_check(restored_1st_dar.as_ref()) + ); + + match (restored_dar, restored_1st_dar) { + (Some(_), Some(_)) => Ok(ConvertedReport::Unhide1rdAnd3rdPerson), + (Some(_), None) => Ok(ConvertedReport::Unhide3rdPerson), + (None, Some(_)) => Ok(ConvertedReport::Unhide1rdPerson), + _ => Err(ConvertError::NotFoundUnhideTarget), } } /// # NOTE /// It is currently used only in GUI, but is implemented in Core as an API. -pub async fn remove_oar(dar_dir: impl AsRef) -> anyhow::Result<()> { - let mut remove_target = None; - let mut removed_target_1st = None; - let mut entries = WalkDir::new(dar_dir); +pub async fn remove_oar(search_dir: impl AsRef) -> Result<()> { + let mut removed_once = false; + let mut entries = WalkDir::new(search_dir); while let Some(entry) = entries.next().await { let path = entry?.path(); - let path = path.as_path(); - // NOTE: The OAR root obtained by parse fn is calculated and not guaranteed to exist. - let (_, oar_name_space_path, is_1st_person, _, _, _) = - match path_changer::parse_dar_path(path, Some("DynamicAnimationReplacer.mohidden")) { - Ok(data) => data, - Err(_) => { - match path_changer::parse_dar_path(path, Some("DynamicAnimationReplacer")) { - Ok(data) => data, - Err(_) => continue, // NOTE: The first search is skipped because it does not yet lead to the DAR file. - } - } // NOTE: The first search is skipped because it does not yet lead to the DAR file. - }; - - if remove_target.is_none() && path.is_dir() && !is_1st_person { - remove_target = Some(oar_name_space_path); - continue; + if path.is_dir() { + let path = path.to_str(); + if let Some(path) = path { + if path.ends_with("OpenAnimationReplacer/") { + trace!("Try to remove oar dir: {:?}", &path); + fs::remove_dir_all(path).await?; + removed_once = true; + } + } } - if removed_target_1st.is_none() && path.is_dir() && is_1st_person { - removed_target_1st = Some(oar_name_space_path); - } - } - - if remove_target.is_none() && removed_target_1st.is_none() { - bail!("Not found OAR directory.") } - if let Some(oar_root) = remove_target { - if oar_root.exists() { - trace!("Remove oar dir: {:?}", &oar_root); - fs::remove_dir_all(oar_root).await?; - } + if removed_once { + return Err(ConvertError::NotFoundOarDir); } - if let Some(oar_root) = removed_target_1st { - if oar_root.exists() { - trace!("Remove oar dir: {:?}", &oar_root); - fs::remove_dir_all(oar_root).await?; - } - } - Ok(()) } diff --git a/dar2oar_core/src/fs/parallel.rs b/dar2oar_core/src/fs/parallel.rs index b26a44d..ab678d1 100644 --- a/dar2oar_core/src/fs/parallel.rs +++ b/dar2oar_core/src/fs/parallel.rs @@ -1,16 +1,32 @@ use crate::condition_parser::parse_dar2oar; use crate::conditions::ConditionsConfig; +use crate::error::{ConvertError, Result}; use crate::fs::path_changer::parse_dar_path; -use crate::fs::{read_file, write_name_space_config, write_section_config, ConvertOptions}; -use anyhow::{bail, Context as _, Result}; +use crate::fs::{ + read_file, write_name_space_config, write_section_config, ConvertOptions, ConvertedReport, +}; +use anyhow::Context as _; use jwalk::WalkDir; +use std::future::Future; use std::path::Path; use tokio::fs; -/// multi thread converter +/// Multi thread converter +/// +/// # Parameters +/// - `options`: Convert options +/// - `async_fn`: For progress async callback(1st time: max contents count, 2nd~: index) +/// /// # Return /// Complete info -pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) -> Result { +pub async fn convert_dar_to_oar( + options: ConvertOptions<'_, impl AsRef>, + mut async_fn: impl FnMut(usize) -> Fut, +) -> Result +where + Fut: Future + Send + 'static, + O: Send + 'static, +{ let ConvertOptions { dar_dir, oar_dir, @@ -19,29 +35,21 @@ pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) - section_table, section_1person_table, hide_dar, - sender, } = options; let mut is_converted_once = false; let mut dar_namespace = None; // To need rename to hidden let mut dar_1st_namespace = None; // To need rename to hidden(For _1stperson) - let sender = sender.as_ref(); // Borrowing ownership here prevents move errors in the loop. let entires = WalkDir::new(&dar_dir).into_iter(); - let mut walk_len = 0; - if let Some(sender) = sender { - walk_len = WalkDir::new(dar_dir).into_iter().count(); // Lower performance cost when sender is None - log::debug!("Dir & File Counts: {}", walk_len); - sender.send(walk_len).await?; - } + let walk_len = WalkDir::new(&dar_dir).into_iter().count(); + log::debug!("Dir & File Counts: {}", walk_len); + async_fn(walk_len).await; for (idx, entry) in entires.enumerate() { - if let Some(sender) = sender { - log::debug!("Converted: {}/{}", idx, walk_len); - sender.send(idx).await?; - } + async_fn(idx).await; - let entry = entry?; + let entry = entry.context("Not found path")?; let path = entry.path(); // Separate this for binding let path = path.as_path(); @@ -70,9 +78,9 @@ pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) - log::debug!("File: {:?}", path); let file_name = path .file_name() - .context("Not found file name")? + .ok_or_else(|| ConvertError::NotFoundFileName)? .to_str() - .context("This file isn't valid utf8")?; + .ok_or_else(|| ConvertError::InvalidUtf8)?; // Files that do not have a priority dir, i.e., files on the same level as the priority dir, // are copied to the name space folder location. @@ -138,26 +146,32 @@ pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) - } } + async fn rename_dir(dir: Option<&std::path::PathBuf>) -> Result<()> { + if let Some(dar_namespace) = dir { + let mut dist = dar_namespace.clone(); + dist.as_mut_os_string().push(".mohidden"); + fs::rename(dar_namespace, dist).await?; + } + Ok(()) + } + match is_converted_once { true => { - let mut msg = "Conversion Completed.".to_string(); + tracing::debug!("Conversion Completed."); if hide_dar { - if let Some(dar_namespace) = dar_namespace { - let mut dist = dar_namespace.clone(); - dist.as_mut_os_string().push(".mohidden"); - fs::rename(dar_namespace, dist).await?; - msg = format!("{}\n- 3rdPerson DAR dir was renamed", msg); - }; - - if let Some(dar_1st_namespace) = dar_1st_namespace { - let mut dist = dar_1st_namespace.clone(); - dist.as_mut_os_string().push(".mohidden"); - fs::rename(dar_1st_namespace, dist).await?; - msg = format!("{}\n- 1stPerson DAR dir was renamed", msg); - }; + rename_dir(dar_namespace.as_ref()).await?; + rename_dir(dar_1st_namespace.as_ref()).await?; + + match (dar_namespace, dar_1st_namespace) { + (Some(_), Some(_)) => Ok(ConvertedReport::Renamed1rdAnd3rdPersonDar), + (Some(_), None) => Ok(ConvertedReport::Renamed3rdPersonDar), + (None, Some(_)) => Ok(ConvertedReport::Renamed1rdPersonDar), + _ => Err(ConvertError::NotFoundDarDir), + } + } else { + Ok(ConvertedReport::Complete) } - Ok(msg) } - false => bail!("DynamicAnimationReplacer dir was never found"), + false => Err(ConvertError::NotFoundDarDir), } } diff --git a/dar2oar_core/src/fs/sequential.rs b/dar2oar_core/src/fs/sequential.rs index 29ab861..3c166fe 100644 --- a/dar2oar_core/src/fs/sequential.rs +++ b/dar2oar_core/src/fs/sequential.rs @@ -1,18 +1,32 @@ use crate::condition_parser::parse_dar2oar; use crate::conditions::ConditionsConfig; +use crate::error::{ConvertError, Result}; use crate::fs::path_changer::parse_dar_path; -use crate::fs::{read_file, write_name_space_config, write_section_config, ConvertOptions}; -use anyhow::{bail, Context as _, Result}; +use crate::fs::{ + read_file, write_name_space_config, write_section_config, ConvertOptions, ConvertedReport, +}; use async_walkdir::WalkDir; +use core::future::Future; use std::path::Path; use tokio::fs; use tokio_stream::StreamExt; /// Single thread converter /// +/// # Parameters +/// - `options`: Convert options +/// - `async_fn`: For progress async callback(1st time: max contents count, 2nd~: index) +/// /// # Return /// Complete info -pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) -> Result { +pub async fn convert_dar_to_oar( + options: ConvertOptions<'_, impl AsRef>, + mut async_fn: impl FnMut(usize) -> Fut, +) -> Result +where + Fut: Future + Send + 'static, + O: Send + 'static, +{ let ConvertOptions { dar_dir, oar_dir, @@ -21,27 +35,19 @@ pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) - section_table, section_1person_table, hide_dar, - sender, } = options; let mut is_converted_once = false; let mut dar_namespace = None; // To need rename to hidden let mut dar_1st_namespace = None; // To need rename to hidden(For _1stperson) - let sender = sender.as_ref(); // Borrowing ownership here prevents move errors in the loop. - let mut walk_len = 0; - if let Some(sender) = sender { - walk_len = WalkDir::new(&dar_dir).collect::>().await.len(); // Lower performance cost when sender is None. - log::debug!("Dir & File Counts: {}", walk_len); - sender.send(walk_len).await?; - } + let walk_len = WalkDir::new(&dar_dir).collect::>().await.len(); // Lower performance cost when sender is None. + log::debug!("Dir & File Counts: {}", walk_len); + tokio::spawn(async_fn(walk_len)); let mut entries = WalkDir::new(dar_dir); let mut idx = 0usize; while let Some(entry) = entries.next().await { - if let Some(sender) = sender { - log::debug!("Converted: {}/{}", idx, walk_len); - sender.send(idx).await?; - } + tokio::spawn(async_fn(idx)); idx += 1; let path = entry?.path(); @@ -77,13 +83,13 @@ pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) - tracing::debug!("File: {:?}", path); let file_name = path .file_name() - .context("Not found file name")? + .ok_or_else(|| ConvertError::NotFoundFileName)? .to_str() - .context("This file isn't valid utf8")?; + .ok_or_else(|| ConvertError::InvalidUtf8)?; - // Files that do not have a priority dir, i.e., files on the same level as the priority dir, + // files that do not have a priority dir, i.e., files on the same level as the priority dir, // are copied to the name space folder location. - // For this reason, an empty string should be put in the name space folder. + // for this reason, an empty string should be put in the name space folder. let priority = &priority.unwrap_or_default(); let section_name = match is_1st_person { @@ -122,14 +128,7 @@ pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) - } if !is_converted_once { is_converted_once = true; - write_name_space_config(&oar_name_space_path, &parsed_mod_name, author) - .await - .with_context(|| { - format!( - "Failed to write name space config to: {:?}", - oar_name_space_path - ) - })?; + write_name_space_config(&oar_name_space_path, &parsed_mod_name, author).await?; } } else { // maybe motion files(.kkx) @@ -149,105 +148,104 @@ pub async fn convert_dar_to_oar(options: ConvertOptions<'_, impl AsRef>) - } } + async fn rename_dir(dir: Option<&std::path::PathBuf>) -> Result<()> { + if let Some(dar_namespace) = dir { + let mut dist = dar_namespace.clone(); + dist.as_mut_os_string().push(".mohidden"); + fs::rename(dar_namespace, dist).await?; + } + Ok(()) + } + match is_converted_once { true => { - let mut msg = "Conversion Completed.".to_string(); if hide_dar { - if let Some(dar_namespace) = dar_namespace { - let mut dist = dar_namespace.clone(); - dist.as_mut_os_string().push(".mohidden"); - fs::rename(dar_namespace, dist).await?; - msg = format!("{}\n- 3rdPerson DAR dir was renamed", msg); - }; - - if let Some(dar_1st_namespace) = dar_1st_namespace { - let mut dist = dar_1st_namespace.clone(); - dist.as_mut_os_string().push(".mohidden"); - fs::rename(dar_1st_namespace, dist).await?; - msg = format!("{}\n- 1stPerson DAR dir was renamed", msg); - }; + rename_dir(dar_namespace.as_ref()).await?; + rename_dir(dar_1st_namespace.as_ref()).await?; + + match (dar_namespace, dar_1st_namespace) { + (Some(_), Some(_)) => Ok(ConvertedReport::Renamed1rdAnd3rdPersonDar), + (Some(_), None) => Ok(ConvertedReport::Renamed3rdPersonDar), + (None, Some(_)) => Ok(ConvertedReport::Renamed1rdPersonDar), + _ => Err(ConvertError::NotFoundDarDir), + } + } else { + Ok(ConvertedReport::Complete) } - tracing::debug!(msg); - Ok(msg) } - false => bail!("DynamicAnimationReplacer dir was never found"), + false => Err(ConvertError::NotFoundDarDir), } } #[cfg(test)] mod test { use super::*; - use tracing::Level; + use anyhow::Result; + + const DAR_DIR: &str = "../test/data/UNDERDOG Animations"; + const TABLE_PATH: &str = "../test/settings/UnderDog Animations_v1.9.6_mapping_table.txt"; + const LOG_PATH: &str = "../convert.log"; + + /// NOTE: It is a macro because it must be called at the root of a function to function. + macro_rules! logger_init { + () => { + let (non_blocking, _guard) = + tracing_appender::non_blocking(std::fs::File::create(LOG_PATH)?); + tracing_subscriber::fmt() + .with_writer(non_blocking) + .with_ansi(false) + .with_max_level(tracing::Level::DEBUG) + .init(); + }; + } + + async fn create_options<'a>() -> Result> { + Ok(ConvertOptions { + dar_dir: DAR_DIR, + // cannot use include_str! + section_table: Some(crate::read_mapping_table(TABLE_PATH).await?), + ..Default::default() + }) + } /// 14.75s #[ignore] #[tokio::test] - async fn convert_non_mpsc() -> anyhow::Result<()> { - let (non_blocking, _guard) = - tracing_appender::non_blocking(std::fs::File::create("../convert.log")?); - tracing_subscriber::fmt() - .with_writer(non_blocking) - .with_ansi(false) - .with_max_level(Level::DEBUG) - .init(); - - // cannot use include_str! - let table = crate::read_mapping_table( - "../test/settings/UnderDog Animations_v1.9.6_mapping_table.txt", - ) - .await - .unwrap(); - - let span = tracing::info_span!("converting"); - let _guard = span.enter(); - convert_dar_to_oar(ConvertOptions { - dar_dir: "../test/data/UNDERDOG Animations", - section_table: Some(table), - // sender: Some(tx), - ..Default::default() - }) - .await?; + async fn convert_non_mpsc() -> Result<()> { + logger_init!(); + convert_dar_to_oar(create_options().await?, |_| async {}).await?; Ok(()) } #[ignore] #[tokio::test] - async fn convert_with_mpsc() -> anyhow::Result<()> { - use tokio::sync::mpsc; - - let (non_blocking, _guard) = - tracing_appender::non_blocking(std::fs::File::create("../convert.log")?); - tracing_subscriber::fmt() - .with_writer(non_blocking) - .with_ansi(false) - .with_max_level(Level::ERROR) - .init(); - - // cannot use include_str! - let table = crate::read_mapping_table( - "../test/settings/UnderDog Animations_v1.9.6_mapping_table.txt", - ) - .await - .unwrap(); - - let span = tracing::info_span!("converting"); - let _guard = span.enter(); - - let (tx, mut rx) = mpsc::channel(1500); - - tokio::spawn(convert_dar_to_oar(ConvertOptions { - dar_dir: "../test/data/UNDERDOG Animations", - section_table: Some(table), - sender: Some(tx), - ..Default::default() - })); + async fn convert_with_mpsc() -> Result<()> { + use once_cell::sync::Lazy; + use std::sync::atomic::AtomicUsize; + use std::sync::atomic::Ordering; + + logger_init!(); + let (tx, mut rx) = tokio::sync::mpsc::channel(500); + + //? NOTE: Since recv does not seem to be possible until io is finished, send is used to see the output. + let sender = move |idx: usize| { + let tx = tx.clone(); + async move { + static NUM: Lazy = Lazy::new(AtomicUsize::default); + let num = NUM.load(Ordering::Acquire); + if num != 0 { + println!("[sender] Converted: {}/{}", idx, num); + } else { + NUM.store(idx, Ordering::Release); + println!("[sender] Converted: {}", idx); + } + tx.send(idx).await.unwrap_or_default(); + } + }; - let mut end = None; + let _ = tokio::spawn(convert_dar_to_oar(create_options().await?, sender)).await?; while let Some(i) = rx.recv().await { - match end { - Some(end) => println!("completed {}/{}", i, end), - _ => end = Some(i), - } + println!("[recv] Converted: {}", i); } Ok(()) } diff --git a/dar2oar_core/src/lib.rs b/dar2oar_core/src/lib.rs index 351ca37..6d57435 100644 --- a/dar2oar_core/src/lib.rs +++ b/dar2oar_core/src/lib.rs @@ -1,7 +1,9 @@ mod condition_parser; mod conditions; mod dar_syntax; -pub mod fs; mod values; +pub mod fs; +pub mod error; + pub use crate::fs::{convert_dar_to_oar, read_mapping_table}; diff --git a/frontend/src/tauri_cmd/index.ts b/frontend/src/tauri_cmd/index.ts index 8c7d1c9..d593b1c 100644 --- a/frontend/src/tauri_cmd/index.ts +++ b/frontend/src/tauri_cmd/index.ts @@ -33,20 +33,23 @@ export async function convertDar2oar(props: ConverterArgs): Promise { const hideDar = props.hideDar ?? false; return invoke("convert_dar2oar", { - darDir, - oarDir, - modName, - modAuthor, - mappingPath, - mapping1personPath, - logLevel: props.logLevel, - runParallel, - hideDar, + options: { + darDir, + oarDir, + modName, + modAuthor, + mappingPath, + mapping1personPath, + logLevel: props.logLevel, + runParallel, + hideDar, + }, }); } /** * @param darPath + * * # Throw Error */ export async function restoreDarDir(darDir: string) { @@ -58,6 +61,7 @@ export async function restoreDarDir(darDir: string) { /** * @param darPath + * * # Throw Error */ export async function removeOarDir(path: string) { @@ -66,12 +70,13 @@ export async function removeOarDir(path: string) { /** * Open a file or Dir + * * # Throw Error */ export async function openPath( path: string, setPath: (path: string) => void, - isDir: boolean + isDir: boolean, ) { const res = await open({ defaultPath: path, diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2f7d95b..67720e2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,9 +17,11 @@ dist = false # To run CI and build separately from CLI (cargo dist) tauri-build = { version = "1.4.0", features = [] } [dependencies] +async-trait = "0.1.74" anyhow = { version = "1.0.75", features = ["backtrace"] } dar2oar_core = { path = "../dar2oar_core" } once_cell = "1.18.0" +serde = { version = "1.0", features = ["derive"] } # Implement (De)Serializer tauri = { version = "1.4.0", features = [ "devtools", "dialog-open", diff --git a/src-tauri/src/cmd.rs b/src-tauri/src/cmd.rs index 5e2930b..3f4dd8c 100644 --- a/src-tauri/src/cmd.rs +++ b/src-tauri/src/cmd.rs @@ -1,10 +1,11 @@ -use crate::logging::change_log_level; +use crate::{ + convert_option::{AsyncFrom, GuiConverterOptions}, + logging::change_log_level, +}; use dar2oar_core::{ convert_dar_to_oar, - fs::{parallel, remove_oar, restore_dar, ConvertOptions}, - read_mapping_table, + fs::{async_closure::AsyncClosure, parallel, remove_oar, unhide_dar, ConvertOptions}, }; -use std::path::Path; /// early return with Err() and write log error. macro_rules! bail { @@ -14,92 +15,44 @@ macro_rules! bail { }}; } -macro_rules! try_get_mapping_table { - ($mapping_path:ident) => { - match $mapping_path { - Some(ref table_path) => { - let mapping = match read_mapping_table(table_path).await { - Ok(table) => table, - Err(err) => bail!(err), - }; - Some(mapping) - } - None => None, +#[tauri::command] +pub(crate) async fn convert_dar2oar(options: GuiConverterOptions<'_>) -> Result { + tracing::debug!("options: {:?}", &options); + let run_parallel = options.run_parallel.unwrap_or_default(); + let log_level = options.log_level.as_deref().unwrap_or("error"); + + let log_level = match log_level { + "trace" | "debug" | "info" | "warn" | "error" => log_level, + unknown_level => { + tracing::warn!("Unknown log level {}. Fallback to error", unknown_level); + "error" } }; -} - -#[allow(clippy::too_many_arguments)] -#[tauri::command] -pub(crate) async fn convert_dar2oar( - dar_dir: &str, - oar_dir: Option<&str>, - mod_name: Option<&str>, - mod_author: Option<&str>, - mapping_path: Option, - mapping_1person_path: Option, - log_level: Option, - run_parallel: Option, - hide_dar: Option, -) -> Result { - let oar_dir = oar_dir.and_then(|dist| match dist.is_empty() { - true => None, - false => Some(Path::new(dist).to_path_buf()), - }); - - let table = try_get_mapping_table!(mapping_path); - let table_1person = try_get_mapping_table!(mapping_1person_path); - - let log_level = log_level - .as_deref() - .and_then(|level| match level { - "trace" | "debug" | "info" | "warn" | "error" => Some(level), - unknown_level => { - tracing::warn!("unknown log level {}. fallback to error", unknown_level); - None - } - }) - .unwrap_or("error"); - - tracing::debug!("src: {}", dar_dir); - tracing::debug!("dist: {:?}", oar_dir); - tracing::debug!("mod_name: {:?}", mod_name); - tracing::debug!("mod_author: {:?}", mod_author); - tracing::debug!("table path: {:?}", mapping_path.as_ref()); - tracing::debug!("1st person table path: {:?}", mapping_1person_path.as_ref()); - tracing::debug!("log level: {:?}", log_level); - tracing::debug!("run parallel: {:?}", run_parallel); - tracing::debug!("to hidden dar: {:?}", hide_dar); - change_log_level(log_level).map_err(|err| err.to_string())?; - let config = ConvertOptions { - dar_dir, - oar_dir, - mod_name, - author: mod_author, - section_table: table, - section_1person_table: table_1person, - hide_dar: hide_dar.unwrap_or(false), - ..Default::default() - }; + let config = ConvertOptions::async_from(options).await; let res = match run_parallel { - Some(true) => parallel::convert_dar_to_oar(config).await, - Some(false) | None => convert_dar_to_oar(config).await, + true => parallel::convert_dar_to_oar(config, AsyncClosure::default).await, + false => convert_dar_to_oar(config, AsyncClosure::default).await, }; - match res { - Ok(complete_msg) => Ok(complete_msg), + Ok(complete_msg) => { + tracing::info!("{}", complete_msg); + Ok(complete_msg.to_string()) + } Err(err) => bail!(err), } } #[tauri::command] pub(crate) async fn restore_dar_dir(dar_dir: &str) -> Result { - restore_dar(dar_dir).await.map_err(|err| err.to_string()) + match unhide_dar(dar_dir).await { + Ok(complete_msg) => Ok(complete_msg.to_string()), + Err(err) => bail!(err), + } } #[tauri::command] pub(crate) async fn remove_oar_dir(path: &str) -> Result<(), String> { - remove_oar(path).await.map_err(|err| err.to_string()) + remove_oar(path).await.or_else(|err| bail!(err)) } diff --git a/src-tauri/src/convert_option.rs b/src-tauri/src/convert_option.rs new file mode 100644 index 0000000..6cc55a4 --- /dev/null +++ b/src-tauri/src/convert_option.rs @@ -0,0 +1,65 @@ +use std::{collections::HashMap, path::Path}; + +use dar2oar_core::{fs::ConvertOptions, read_mapping_table}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct GuiConverterOptions<'a> { + pub(crate) dar_dir: &'a str, + pub(crate) oar_dir: Option<&'a str>, + pub(crate) mod_name: Option<&'a str>, + pub(crate) mod_author: Option<&'a str>, + pub(crate) mapping_path: Option, + pub(crate) mapping_1person_path: Option, + pub(crate) log_level: Option, + pub(crate) run_parallel: Option, + pub(crate) hide_dar: Option, +} + +async fn try_get_mapping_table(mapping_path: Option<&str>) -> Option> { + match mapping_path { + Some(table_path) => read_mapping_table(table_path).await.ok(), + None => None, + } +} + +#[async_trait::async_trait] +pub(crate) trait AsyncFrom { + async fn async_from(options: T) -> Self; +} + +#[async_trait::async_trait] +impl<'a> AsyncFrom> for ConvertOptions<'a, &'a str> { + async fn async_from(options: GuiConverterOptions<'a>) -> Self { + let GuiConverterOptions { + dar_dir, + oar_dir, + mod_name, + mod_author: author, + mapping_path, + mapping_1person_path, + log_level: _, + run_parallel: _, + hide_dar, + } = options; + + let oar_dir = oar_dir.and_then(|dist| match dist.is_empty() { + true => None, + false => Some(Path::new(dist).to_path_buf()), + }); + + let section_table = try_get_mapping_table(mapping_path.as_deref()).await; + let section_1person_table = try_get_mapping_table(mapping_1person_path.as_deref()).await; + + Self { + dar_dir, + oar_dir, + mod_name, + author, + section_table, + section_1person_table, + hide_dar: hide_dar.unwrap_or(false), + } + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f49f21c..a98754c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,6 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod cmd; +mod convert_option; mod logging; mod runner;