You've already forked AstralRinth
forked from didirus/AstralRinth
381ea51cce
* fix: files.vue bugs before styling changes * feat: move files tab to shared layout structure * fix: qa * fix: qa * fix: bugs * fix: lint * fix: admonition cleanup with progress + actions * fix: cleanup * fix: modals * fix: admon title * fix: i18n standard * fix: lint + i18n pass * fix: remove transition * fix: type errors * feat: files tab in app * fix: qa * fix: backup item minmax * fix: use ContentPageHeader for server panel * fix: lint * fix: lint * fix: lint * feat: page leave safety * fix: lint * fix: cargo fmt fix * fix: blank in prod * fix: content card table stuff * Revert "fix: blank in prod" This reverts commit 74758fe185cf85a4a20355857f889cb091b97ace. * fix: import * feat: browse worlds/servers flow * fix: worlds tab parity with content tab * fix: perf bug + shader filter pill copy * feat: singleplayer filter * fix: ordering * fix: breadcrumbs * fix: lint * fix: qa * feat: store server proj id when adding to a non-linked instance * fix: lint * fix: i18n + qa * fix: conflict * qa: already installed modal + placeholders not server-specific * fix: qa * fix: add + edit server modals * fix: qa * fix: security * fix: devin flags * fix: lint * chore: change file to break build cache * fix: admon * fix: import path stuff * feat: qa * fix: fmt fmt idiot --------- Signed-off-by: Calum H. <calum@modrinth.com>
423 lines
13 KiB
Rust
423 lines
13 KiB
Rust
use std::{
|
|
fmt,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use io::IOError;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{
|
|
event::{
|
|
LoadingBarId,
|
|
emit::{emit_loading, init_or_edit_loading},
|
|
},
|
|
util::{
|
|
fetch::{self, IoSemaphore},
|
|
io,
|
|
},
|
|
};
|
|
|
|
pub mod atlauncher;
|
|
pub mod curseforge;
|
|
pub mod gdlauncher;
|
|
pub mod mmc;
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub enum ImportLauncherType {
|
|
MultiMC,
|
|
PrismLauncher,
|
|
ATLauncher,
|
|
GDLauncher,
|
|
Curseforge,
|
|
#[serde(other)]
|
|
Unknown,
|
|
}
|
|
// impl display
|
|
impl fmt::Display for ImportLauncherType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
ImportLauncherType::MultiMC => write!(f, "MultiMC"),
|
|
ImportLauncherType::PrismLauncher => write!(f, "PrismLauncher"),
|
|
ImportLauncherType::ATLauncher => write!(f, "ATLauncher"),
|
|
ImportLauncherType::GDLauncher => write!(f, "GDLauncher"),
|
|
ImportLauncherType::Curseforge => write!(f, "Curseforge"),
|
|
ImportLauncherType::Unknown => write!(f, "Unknown"),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return a list of importable instances from a launcher type and base path, by iterating through the folder and checking
|
|
pub async fn get_importable_instances(
|
|
launcher_type: ImportLauncherType,
|
|
base_path: PathBuf,
|
|
) -> crate::Result<Vec<String>> {
|
|
// Some launchers have a different folder structure for instances
|
|
let instances_subfolder = match launcher_type {
|
|
ImportLauncherType::GDLauncher | ImportLauncherType::ATLauncher => {
|
|
"instances".to_string()
|
|
}
|
|
ImportLauncherType::Curseforge => "Instances".to_string(),
|
|
ImportLauncherType::MultiMC => {
|
|
mmc::get_instances_subpath(base_path.clone().join("multimc.cfg"))
|
|
.await
|
|
.unwrap_or_else(|| "instances".to_string())
|
|
}
|
|
ImportLauncherType::PrismLauncher => mmc::get_instances_subpath(
|
|
base_path.clone().join("prismlauncher.cfg"),
|
|
)
|
|
.await
|
|
.unwrap_or_else(|| "instances".to_string()),
|
|
ImportLauncherType::Unknown => {
|
|
let types = [
|
|
ImportLauncherType::MultiMC,
|
|
ImportLauncherType::PrismLauncher,
|
|
ImportLauncherType::ATLauncher,
|
|
ImportLauncherType::GDLauncher,
|
|
ImportLauncherType::Curseforge,
|
|
];
|
|
for lt in types {
|
|
if let Ok(instances) =
|
|
Box::pin(get_importable_instances(lt, base_path.clone()))
|
|
.await
|
|
&& !instances.is_empty()
|
|
{
|
|
return Ok(instances);
|
|
}
|
|
}
|
|
return Ok(Vec::new());
|
|
}
|
|
};
|
|
|
|
let instances_folder = base_path.join(&instances_subfolder);
|
|
let mut instances = Vec::new();
|
|
let mut dir = io::read_dir(&instances_folder).await.map_err(| _ | {
|
|
crate::ErrorKind::InputError(format!(
|
|
"Invalid {launcher_type} launcher path, could not find '{instances_subfolder}' subfolder."
|
|
))
|
|
})?;
|
|
while let Some(entry) = dir
|
|
.next_entry()
|
|
.await
|
|
.map_err(|e| IOError::with_path(e, &instances_folder))?
|
|
{
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
// Check instance is valid of this launcher type
|
|
if is_valid_importable_instance(path.clone(), launcher_type).await {
|
|
let name = path.file_name();
|
|
if let Some(name) = name {
|
|
instances.push(name.to_string_lossy().to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(instances)
|
|
}
|
|
|
|
// Import an instance from a launcher type and base path
|
|
// Note: this *deletes* the submitted empty profile
|
|
|
|
// #[tracing::instrument]
|
|
pub async fn import_instance(
|
|
profile_path: &str, // This should be a blank profile
|
|
launcher_type: ImportLauncherType,
|
|
base_path: PathBuf,
|
|
instance_folder: String,
|
|
) -> crate::Result<()> {
|
|
tracing::debug!("Importing instance from {instance_folder}");
|
|
let res = match launcher_type {
|
|
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
|
|
mmc::import_mmc(
|
|
base_path, // path to base mmc folder
|
|
instance_folder, // instance folder in mmc_base_path
|
|
profile_path, // path to profile
|
|
)
|
|
.await
|
|
}
|
|
ImportLauncherType::ATLauncher => {
|
|
atlauncher::import_atlauncher(
|
|
base_path, // path to atlauncher folder
|
|
instance_folder, // instance folder in atlauncher
|
|
profile_path, // path to profile
|
|
)
|
|
.await
|
|
}
|
|
ImportLauncherType::GDLauncher => {
|
|
gdlauncher::import_gdlauncher(
|
|
base_path.join("instances").join(instance_folder), // path to gdlauncher folder
|
|
profile_path, // path to profile
|
|
)
|
|
.await
|
|
}
|
|
ImportLauncherType::Curseforge => {
|
|
curseforge::import_curseforge(
|
|
base_path.join("Instances").join(instance_folder), // path to curseforge folder
|
|
profile_path, // path to profile
|
|
)
|
|
.await
|
|
}
|
|
ImportLauncherType::Unknown => {
|
|
let types = [
|
|
ImportLauncherType::MultiMC,
|
|
ImportLauncherType::PrismLauncher,
|
|
ImportLauncherType::ATLauncher,
|
|
ImportLauncherType::GDLauncher,
|
|
ImportLauncherType::Curseforge,
|
|
];
|
|
let mut matched = false;
|
|
for lt in types {
|
|
if let Ok(instances) =
|
|
Box::pin(get_importable_instances(lt, base_path.clone()))
|
|
.await
|
|
&& instances.contains(&instance_folder)
|
|
{
|
|
matched = true;
|
|
Box::pin(import_instance(
|
|
profile_path,
|
|
lt,
|
|
base_path,
|
|
instance_folder,
|
|
))
|
|
.await?;
|
|
break;
|
|
}
|
|
}
|
|
if !matched {
|
|
return Err(crate::ErrorKind::InputError(
|
|
"Could not determine launcher type for the given path"
|
|
.to_string(),
|
|
)
|
|
.into());
|
|
}
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// If import failed, delete the profile
|
|
match res {
|
|
Ok(_) => {}
|
|
Err(e) => {
|
|
tracing::warn!("Import failed: {:?}", e);
|
|
let _ = crate::api::profile::remove(profile_path).await;
|
|
return Err(e);
|
|
}
|
|
}
|
|
|
|
tracing::debug!("Completed import.");
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the default path for the given launcher type
|
|
/// None if it can't be found or doesn't exist
|
|
pub fn get_default_launcher_path(
|
|
r#type: ImportLauncherType,
|
|
) -> Option<PathBuf> {
|
|
let path = match r#type {
|
|
ImportLauncherType::MultiMC => {
|
|
return find_multimc_path();
|
|
}
|
|
ImportLauncherType::PrismLauncher => {
|
|
Some(dirs::data_dir()?.join("PrismLauncher"))
|
|
}
|
|
ImportLauncherType::ATLauncher => {
|
|
Some(dirs::data_dir()?.join("ATLauncher"))
|
|
}
|
|
ImportLauncherType::GDLauncher => {
|
|
Some(dirs::data_dir()?.join("gdlauncher_next"))
|
|
}
|
|
ImportLauncherType::Curseforge => {
|
|
let home = dirs::home_dir()?;
|
|
let primary = home.join("curseforge").join("minecraft");
|
|
if primary.exists() {
|
|
return Some(primary);
|
|
}
|
|
Some(dirs::document_dir()?.join("curseforge").join("minecraft"))
|
|
}
|
|
ImportLauncherType::Unknown => None,
|
|
};
|
|
let path = path?;
|
|
if path.exists() { Some(path) } else { None }
|
|
}
|
|
|
|
/// Searches common locations for a MultiMC installation.
|
|
/// MultiMC stores data in its own application directory (not a standard data dir)
|
|
fn find_multimc_path() -> Option<PathBuf> {
|
|
let mut candidates: Vec<PathBuf> = Vec::new();
|
|
|
|
// Linux/macOS: ~/.local/share/multimc is the typical location
|
|
if let Some(data_dir) = dirs::data_dir() {
|
|
candidates.push(data_dir.join("multimc"));
|
|
candidates.push(data_dir.join("MultiMC"));
|
|
}
|
|
|
|
// Windows: check common extraction locations
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if let Some(home) = dirs::home_dir() {
|
|
candidates.push(home.join("MultiMC"));
|
|
candidates.push(home.join("Desktop").join("MultiMC"));
|
|
candidates.push(home.join("Downloads").join("MultiMC"));
|
|
}
|
|
candidates.push(PathBuf::from("C:\\MultiMC"));
|
|
if let Some(program_files) =
|
|
std::env::var_os("ProgramFiles").map(PathBuf::from)
|
|
{
|
|
candidates.push(program_files.join("MultiMC"));
|
|
}
|
|
if let Some(program_files_x86) =
|
|
std::env::var_os("ProgramFiles(x86)").map(PathBuf::from)
|
|
{
|
|
candidates.push(program_files_x86.join("MultiMC"));
|
|
}
|
|
}
|
|
|
|
// macOS: MultiMC is a .app bundle with data inside MultiMC.app/Data/
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
candidates.push(PathBuf::from("/Applications/MultiMC.app/Data"));
|
|
if let Some(home) = dirs::home_dir() {
|
|
candidates.push(
|
|
home.join("Applications").join("MultiMC.app").join("Data"),
|
|
);
|
|
}
|
|
}
|
|
|
|
candidates
|
|
.into_iter()
|
|
.find(|p| p.join("multimc.cfg").exists())
|
|
}
|
|
|
|
/// Checks if this PathBuf is a valid instance for the given launcher type
|
|
|
|
#[tracing::instrument]
|
|
pub async fn is_valid_importable_instance(
|
|
instance_path: PathBuf,
|
|
r#type: ImportLauncherType,
|
|
) -> bool {
|
|
match r#type {
|
|
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
|
|
mmc::is_valid_mmc(instance_path).await
|
|
}
|
|
ImportLauncherType::ATLauncher => {
|
|
atlauncher::is_valid_atlauncher(instance_path).await
|
|
}
|
|
ImportLauncherType::GDLauncher => {
|
|
gdlauncher::is_valid_gdlauncher(instance_path).await
|
|
}
|
|
ImportLauncherType::Curseforge => {
|
|
curseforge::is_valid_curseforge(instance_path).await
|
|
}
|
|
ImportLauncherType::Unknown => false,
|
|
}
|
|
}
|
|
|
|
/// Caches an image file in the filesystem into the cache directory, and returns the path to the cached file.
|
|
|
|
#[tracing::instrument]
|
|
pub async fn recache_icon(
|
|
icon_path: PathBuf,
|
|
) -> crate::Result<Option<PathBuf>> {
|
|
let state = crate::State::get().await?;
|
|
|
|
let bytes = tokio::fs::read(&icon_path).await;
|
|
if let Ok(bytes) = bytes {
|
|
let bytes = bytes::Bytes::from(bytes);
|
|
let cache_dir = &state.directories.caches_dir();
|
|
let semaphore = &state.io_semaphore;
|
|
Ok(Some(
|
|
fetch::write_cached_icon(
|
|
&icon_path.to_string_lossy(),
|
|
cache_dir,
|
|
bytes,
|
|
semaphore,
|
|
)
|
|
.await?,
|
|
))
|
|
} else {
|
|
// could not find icon (for instance, prism default icon, etc)
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub async fn copy_dotminecraft(
|
|
profile_path_id: &str,
|
|
dotminecraft: PathBuf,
|
|
io_semaphore: &IoSemaphore,
|
|
existing_loading_bar: Option<LoadingBarId>,
|
|
) -> crate::Result<LoadingBarId> {
|
|
// Get full path to profile
|
|
let profile_path =
|
|
crate::api::profile::get_full_path(profile_path_id).await?;
|
|
|
|
// Gets all subfiles recursively in src
|
|
let subfiles = get_all_subfiles(&dotminecraft, false).await?;
|
|
let total_subfiles = subfiles.len() as u64;
|
|
|
|
let loading_bar = init_or_edit_loading(
|
|
existing_loading_bar,
|
|
crate::LoadingBarType::CopyProfile {
|
|
import_location: dotminecraft.clone(),
|
|
profile_name: profile_path_id.to_string(),
|
|
},
|
|
total_subfiles as f64,
|
|
"Copying files in profile",
|
|
)
|
|
.await?;
|
|
|
|
// Copy each file
|
|
for src_child in subfiles {
|
|
let dst_child =
|
|
src_child.strip_prefix(&dotminecraft).map_err(|_| {
|
|
crate::ErrorKind::InputError(format!(
|
|
"Invalid file: {}",
|
|
&src_child.display()
|
|
))
|
|
})?;
|
|
let dst_child = profile_path.join(dst_child);
|
|
|
|
// sleep for cpu for 1 millisecond
|
|
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
|
|
|
fetch::copy(&src_child, &dst_child, io_semaphore).await?;
|
|
|
|
emit_loading(&loading_bar, 1.0, None)?;
|
|
}
|
|
Ok(loading_bar)
|
|
}
|
|
|
|
/// Recursively get a list of all subfiles in src
|
|
/// uses async recursion
|
|
|
|
#[async_recursion::async_recursion]
|
|
#[tracing::instrument]
|
|
pub async fn get_all_subfiles(
|
|
src: &Path,
|
|
include_empty_dirs: bool,
|
|
) -> crate::Result<Vec<PathBuf>> {
|
|
if !src.is_dir() {
|
|
return Ok(vec![src.to_path_buf()]);
|
|
}
|
|
|
|
let mut files = Vec::new();
|
|
let mut dir = io::read_dir(&src).await?;
|
|
|
|
let mut has_files = false;
|
|
while let Some(child) = dir
|
|
.next_entry()
|
|
.await
|
|
.map_err(|e| IOError::with_path(e, src))?
|
|
{
|
|
has_files = true;
|
|
let src_child = child.path();
|
|
files.append(
|
|
&mut get_all_subfiles(&src_child, include_empty_dirs).await?,
|
|
);
|
|
}
|
|
|
|
if !has_files && include_empty_dirs {
|
|
files.push(src.to_path_buf());
|
|
}
|
|
|
|
Ok(files)
|
|
}
|