use super::common::*;
use super::config::*;
use super::database::*;
use super::mimetype::*;
use super::model::*;
use super::progress::*;
use anyhow::{Context, Result, anyhow, bail};
use indicatif::ProgressBar;
use num_traits::FromPrimitive;
use rayon::prelude::*;
use regex::Regex;
use sqlx::sqlite::SqliteConnection;
use std::cmp::Ordering;
use std::ffi::OsStr;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use tempfile::TempDir;
use tokio::fs;
use tokio::fs::File;
use tokio::process::Command;
use which::which;
static SYSTEM_NAME_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(Non-Redump - |Unofficial - )?([^()]+)( \(.*\))?$").unwrap());
pub async fn get_canonicalized_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
let canonicalized_path = path.as_ref().canonicalize().with_context(|| {
format!(
"Failed to get canonicalized path for \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
)
})?;
Ok(canonicalized_path)
}
#[cfg(test)]
pub async fn open_file<P: AsRef<Path>>(path: &P) -> Result<File> {
let file = File::open(path.as_ref()).await.with_context(|| {
format!(
"Failed to open \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
)
})?;
Ok(file)
}
pub fn open_file_sync<P: AsRef<Path>>(path: &P) -> Result<std::fs::File> {
let file = std::fs::File::open(path.as_ref()).with_context(|| {
format!(
"Failed to open \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
)
})?;
Ok(file)
}
pub fn get_reader_sync<P: AsRef<Path>>(path: &P) -> Result<std::io::BufReader<std::fs::File>> {
let f = open_file_sync(path)?;
Ok(std::io::BufReader::new(f))
}
pub async fn create_file<P: AsRef<Path>>(
progress_bar: &ProgressBar,
path: &P,
quiet: bool,
) -> Result<File> {
if !quiet {
print_action(
progress_bar,
&format!(
"Creating \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
),
);
}
let directory = path.as_ref().parent().unwrap();
if !directory.is_dir() {
create_directory(progress_bar, &directory, quiet).await?;
}
let file = File::create(path).await.with_context(|| {
format!(
"Failed to create \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
)
})?;
Ok(file)
}
pub async fn copy_file<P: AsRef<Path>, Q: AsRef<Path>>(
progress_bar: &ProgressBar,
old_path: &P,
new_path: &Q,
quiet: bool,
) -> Result<()> {
if old_path.as_ref() != new_path.as_ref() {
let new_directory = new_path.as_ref().parent().unwrap();
if !new_directory.is_dir() {
create_directory(progress_bar, &new_directory, quiet).await?;
}
if !quiet {
print_action(
progress_bar,
&format!(
"Copying to \"{}\"",
new_path.as_ref().as_os_str().to_str().unwrap()
),
);
}
fs::copy(old_path, new_path).await.with_context(|| {
format!(
"Failed to copy \"{}\" to \"{}\"",
old_path.as_ref().as_os_str().to_str().unwrap(),
new_path.as_ref().as_os_str().to_str().unwrap()
)
})?;
}
Ok(())
}
pub async fn rename_file<P: AsRef<Path>, Q: AsRef<Path>>(
progress_bar: &ProgressBar,
old_path: &P,
new_path: &Q,
quiet: bool,
) -> Result<()> {
if old_path.as_ref() != new_path.as_ref() {
let new_directory = new_path.as_ref().parent().unwrap();
if !new_directory.is_dir() {
create_directory(progress_bar, &new_directory, quiet).await?;
}
if !quiet {
print_action(
progress_bar,
&format!(
"Moving to \"{}\"",
new_path.as_ref().as_os_str().to_str().unwrap()
),
);
}
let result = fs::rename(old_path, new_path).await;
if result.is_err() {
copy_file(progress_bar, old_path, new_path, quiet).await?;
remove_file(progress_bar, old_path, quiet).await?;
}
}
Ok(())
}
pub async fn remove_file<P: AsRef<Path>>(
progress_bar: &ProgressBar,
path: &P,
quiet: bool,
) -> Result<()> {
if !quiet {
print_action(
progress_bar,
&format!(
"Deleting \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
),
);
}
fs::remove_file(path).await.with_context(|| {
format!(
"Failed to delete \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
)
})?;
Ok(())
}
pub async fn create_directory<P: AsRef<Path>>(
progress_bar: &ProgressBar,
path: &P,
quiet: bool,
) -> Result<()> {
if !quiet {
print_action(
progress_bar,
&format!(
"Creating \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
),
);
}
if !path.as_ref().is_dir() {
fs::create_dir_all(path).await.with_context(|| {
format!(
"Failed to create \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
)
})?;
}
Ok(())
}
pub async fn create_tmp_directory(connection: &mut SqliteConnection) -> Result<TempDir> {
let tmp_directory = TempDir::new_in(get_tmp_directory(connection).await)
.context("Failed to create temp directory")?;
Ok(tmp_directory)
}
pub async fn remove_directory<P: AsRef<Path>>(
progress_bar: &ProgressBar,
path: &P,
quiet: bool,
) -> Result<()> {
if !quiet {
print_action(
progress_bar,
&format!(
"Deleting \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
),
);
}
fs::remove_dir_all(path).await.with_context(|| {
format!(
"Failed to delete \"{}\"",
path.as_ref().as_os_str().to_str().unwrap()
)
})?;
Ok(())
}
pub async fn get_system_directory(
connection: &mut SqliteConnection,
system: &System,
) -> Result<PathBuf> {
let system_name = match &system.custom_name {
Some(custom_name) => {
if get_bool(connection, "GROUP_SUBSYSTEMS", Some(system.id)).await {
SYSTEM_NAME_REGEX
.captures(custom_name)
.unwrap()
.get(2)
.unwrap()
.as_str()
.to_owned()
} else {
custom_name.to_owned()
}
}
None => {
if get_bool(connection, "GROUP_SUBSYSTEMS", Some(system.id)).await {
SYSTEM_NAME_REGEX
.captures(&system.name)
.unwrap()
.get(2)
.unwrap()
.as_str()
.to_owned()
} else {
system.name.trim().to_owned()
}
}
};
let rom_directory = match get_directory(connection, "ROM_DIRECTORY", Some(system.id)).await {
Some(dir) => dir,
None => get_rom_directory(connection).await,
};
let system_directory = rom_directory.join(system_name);
Ok(system_directory)
}
pub async fn get_one_region_directory(
connection: &mut SqliteConnection,
system: &System,
) -> Result<PathBuf> {
let one_region_directory = get_system_directory(connection, system).await?.join("1G1R");
Ok(one_region_directory)
}
pub async fn get_trash_directory(
connection: &mut SqliteConnection,
system: Option<&System>,
) -> Result<PathBuf> {
let trash_directory = match system {
Some(system) => get_system_directory(connection, system)
.await?
.join("Trash"),
None => get_rom_directory(connection).await.join("Trash"),
};
Ok(trash_directory)
}
pub async fn run_tool(command: &mut tokio::process::Command) -> Result<std::process::Output> {
log::debug!("{:?}", command);
let program = command.as_std().get_program().to_string_lossy().to_string();
let output = command
.output()
.await
.with_context(|| format!("Failed to spawn {}", program))?;
if !output.status.success() {
bail!("{}", String::from_utf8_lossy(&output.stderr));
}
Ok(output)
}
pub async fn tool_available<F, Fut>(check: F, name: &str, progress_bar: &ProgressBar) -> bool
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<String>>,
{
match check().await {
Ok(_) => true,
Err(_) => {
print_error(progress_bar, &format!("Required tool not found: {}", name));
false
}
}
}
pub async fn tool_version(
command: impl AsRef<OsStr>,
name: &str,
args: &[&str],
stdout: bool,
line: usize,
regex: Option<&Regex>,
) -> Result<String> {
let output = Command::new(command.as_ref())
.args(args)
.output()
.await
.context(format!("Failed to spawn {}", name))?;
let text = if stdout {
String::from_utf8(output.stdout).unwrap()
} else {
String::from_utf8(output.stderr).unwrap()
};
Ok(regex
.and_then(|regex| text.lines().nth(line).and_then(|l| regex.find(l)))
.map(|version| version.as_str().to_string())
.unwrap_or(String::from("unknown")))
}
pub fn get_executable_path(executables: &[&str]) -> Result<PathBuf> {
let path = executables
.iter()
.find_map(|executable| which(executable).ok());
if let Some(path) = path {
Ok(path)
} else {
Err(anyhow!("No executable in path"))
}
}
pub fn is_update(progress_bar: &ProgressBar, old_version: &str, new_version: &str) -> bool {
match new_version.cmp(old_version) {
Ordering::Less => {
print_skip(
progress_bar,
&format!(
"Skipping older version \"{}\" (current: \"{}\")",
new_version, old_version
),
);
false
}
Ordering::Equal => {
print_skip(
progress_bar,
&format!("Already up to date (version \"{}\")", new_version),
);
false
}
Ordering::Greater => {
print_info(
progress_bar,
&format!("Updating from \"{}\" to \"{}\"", old_version, new_version),
);
true
}
}
}
async fn create_missing_empty_files(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &System,
) -> Result<()> {
let partial_games = find_partial_games_by_system_id(connection, system.id).await;
for game in partial_games {
let missing_roms = find_roms_without_romfile_by_game_ids(connection, &[game.id]).await;
for rom in missing_roms {
if rom.size == 0 && rom.crc.is_none() {
let tmp_path = create_tmp_directory(connection)
.await?
.path()
.join(&rom.name);
create_file(progress_bar, &tmp_path, false).await?;
let common_romfile = CommonRomfile::from_path(&tmp_path)?;
common_romfile
.rename(
progress_bar,
&common_romfile
.get_sorted_path(connection, system, &game, &rom, &None, &None)
.await?,
false,
)
.await?
.create(connection, progress_bar, RomfileType::Romfile)
.await?;
}
}
}
Ok(())
}
pub async fn compute_system_completion(
connection: &mut SqliteConnection,
progress_bar: &ProgressBar,
system: &System,
) -> Result<()> {
start_action(progress_bar, Some("Computing completion"));
create_missing_empty_files(connection, progress_bar, system).await?;
if system.arcade {
let merging = Merging::from_i64(system.merging).unwrap();
match merging {
Merging::Split => {
update_split_games_completion_by_system_id(connection, system.id).await;
}
Merging::NonMerged | Merging::Merged => {
update_non_merged_and_merged_games_completion_by_system_id(connection, system.id)
.await;
}
Merging::FullNonMerged | Merging::FullMerged => {
update_games_completion_by_system_id(connection, system.id).await;
}
}
} else {
update_games_completion_by_system_id(connection, system.id).await;
update_jbfolder_games_completion_by_system_id(connection, system.id).await;
}
update_system_completion(connection, system.id).await;
stop_action(progress_bar);
Ok(())
}
pub async fn find_parent_chd_romfile_by_game(
connection: &mut SqliteConnection,
game: &Game,
) -> Option<Romfile> {
let parent_game = match game.playlist_id {
Some(playlist_id) => {
let parent_game = find_first_game_by_playlist_id(connection, playlist_id).await;
if parent_game.id != game.id {
Some(parent_game)
} else {
None
}
}
None => None,
};
match parent_game {
Some(parent_game) => {
let roms = find_roms_with_romfile_by_game_ids(connection, &[parent_game.id]).await;
let mut romfile_ids = roms
.into_par_iter()
.map(|rom| rom.romfile_id.unwrap())
.collect::<Vec<i64>>();
romfile_ids.dedup();
find_romfiles_by_ids(connection, &romfile_ids)
.await
.into_par_iter()
.find_first(|romfile| romfile.path.ends_with(CHD_EXTENSION))
}
None => None,
}
}
pub fn compute_alpha_subfolder(name: &str) -> String {
let first_char = name.chars().next().unwrap();
if first_char.is_ascii_alphabetic() {
first_char.to_ascii_uppercase().to_string()
} else {
String::from("#")
}
}
#[cfg(test)]
mod test_compute_alpha_subfolder;
#[cfg(test)]
mod test_system_directory_custom_name;
#[cfg(test)]
mod test_system_directory_group_non_redump;
#[cfg(test)]
mod test_system_directory_group_subsystems;
#[cfg(test)]
mod test_system_directory_no_group_subsystems;