use std::path::Path; use crate::{ cli::ImportArgs, error::{PakkerError, Result}, model::{Config, LockFile, Target}, ui_utils::prompt_yes_no, }; pub async fn execute( args: ImportArgs, global_yes: bool, lockfile_path: &Path, config_path: &Path, ) -> Result<()> { let skip_prompts = global_yes; log::info!("Importing modpack from {}", args.file); log::info!( "Dependency resolution: {}", if args.deps { "enabled" } else { "disabled" } ); let path = Path::new(&args.file); if !path.exists() { return Err(PakkerError::FileNotFound( path.to_string_lossy().to_string(), )); } // Check if lockfile or config already exist if (lockfile_path.exists() || config_path.exists()) && !skip_prompts { let msg = if lockfile_path.exists() && config_path.exists() { "Both pakku-lock.json and pakku.json exist. Importing will overwrite \ them. Continue?" } else if lockfile_path.exists() { "pakku-lock.json exists. Importing will overwrite it. Continue?" } else { "pakku.json exists. Importing will overwrite it. Continue?" }; if !prompt_yes_no(msg, false, skip_prompts)? { log::info!("Import cancelled by user"); return Ok(()); } } // Detect format by checking file contents let file = std::fs::File::open(path)?; let mut archive = zip::ZipArchive::new(file)?; let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new(".")); let config_dir = config_path.parent().unwrap_or(Path::new(".")); if archive.by_name("modrinth.index.json").is_ok() { drop(archive); import_modrinth(path, lockfile_dir, config_dir).await } else if archive.by_name("manifest.json").is_ok() { drop(archive); import_curseforge(path, lockfile_dir, config_dir).await } else { Err(PakkerError::InvalidImportFile( "Unknown pack format".to_string(), )) } } async fn import_modrinth( path: &Path, lockfile_dir: &Path, config_dir: &Path, ) -> Result<()> { use std::{fs::File, io::Read}; use zip::ZipArchive; use crate::platform::create_platform; let file = File::open(path)?; let mut archive = ZipArchive::new(file)?; let index_content = { let mut index_file = archive.by_name("modrinth.index.json")?; let mut content = String::new(); index_file.read_to_string(&mut content)?; content }; let index: serde_json::Value = serde_json::from_str(&index_content)?; // Create lockfile let mc_version = index["dependencies"]["minecraft"] .as_str() .unwrap_or("1.20.1") .to_string(); let loader = if let Some(fabric) = index["dependencies"]["fabric-loader"].as_str() { ("fabric".to_string(), fabric.to_string()) } else if let Some(forge) = index["dependencies"]["forge"].as_str() { ("forge".to_string(), forge.to_string()) } else { ("fabric".to_string(), "latest".to_string()) }; let mut loaders = std::collections::HashMap::new(); loaders.insert(loader.0.clone(), loader.1); let mut lockfile = LockFile { target: Some(Target::Modrinth), mc_versions: vec![mc_version.clone()], loaders: loaders.clone(), projects: Vec::new(), lockfile_version: 1, }; // Import projects from files list if let Some(files) = index["files"].as_array() { log::info!("Importing {} projects from modpack", files.len()); // Create platform client let creds = crate::model::credentials::ResolvedCredentials::load().ok(); let platform = create_platform( "modrinth", creds .as_ref() .and_then(|c| c.modrinth_token().map(std::string::ToString::to_string)), )?; for file_entry in files { if let Some(project_id) = file_entry["downloads"] .as_array() .and_then(|downloads| downloads.first()) .and_then(|url| url.as_str()) .and_then(|url| url.split('/').rev().nth(1)) { log::info!("Fetching project: {project_id}"); match platform .request_project_with_files( project_id, &lockfile.mc_versions, std::slice::from_ref(&loader.0), ) .await { Ok(mut project) => { // Select best file if let Err(e) = project.select_file( &lockfile.mc_versions, std::slice::from_ref(&loader.0), None, // Use default (1 file) during import ) { log::warn!( "Failed to select file for {}: {}", project.get_name(), e ); continue; } lockfile.add_project(project); }, Err(e) => { log::warn!("Failed to fetch project {project_id}: {e}"); }, } } } } // Create config let config = Config { name: index["name"] .as_str() .unwrap_or("Imported Pack") .to_string(), version: index["versionId"] .as_str() .unwrap_or("1.0.0") .to_string(), description: index["summary"] .as_str() .map(std::string::ToString::to_string), author: None, overrides: vec!["overrides".to_string()], server_overrides: None, client_overrides: None, paths: Default::default(), projects: None, export_profiles: None, export_server_side_projects_to_client: None, file_count_preference: None, }; // Save files using provided paths lockfile.save(lockfile_dir)?; config.save(config_dir)?; log::info!("Imported {} projects", lockfile.projects.len()); // Extract overrides for i in 0..archive.len() { let mut file = archive.by_index(i)?; let outpath = file.enclosed_name().ok_or_else(|| { PakkerError::InternalError("Invalid file path in archive".to_string()) })?; if outpath.starts_with("overrides/") { let target = outpath.strip_prefix("overrides/").unwrap(); if file.is_dir() { std::fs::create_dir_all(target)?; } else { if let Some(parent) = target.parent() { std::fs::create_dir_all(parent)?; } let mut outfile = File::create(target)?; std::io::copy(&mut file, &mut outfile)?; } } } Ok(()) } async fn import_curseforge( path: &Path, lockfile_dir: &Path, config_dir: &Path, ) -> Result<()> { use std::{fs::File, io::Read}; use zip::ZipArchive; let file = File::open(path)?; let mut archive = ZipArchive::new(file)?; let manifest_content = { let mut manifest_file = archive.by_name("manifest.json")?; let mut content = String::new(); manifest_file.read_to_string(&mut content)?; content }; let manifest: serde_json::Value = serde_json::from_str(&manifest_content)?; // Create lockfile let mc_version = manifest["minecraft"]["version"] .as_str() .unwrap_or("1.20.1") .to_string(); let mod_loaders = manifest["minecraft"]["modLoaders"] .as_array() .ok_or_else(|| { PakkerError::InvalidImportFile("Missing modLoaders".to_string()) })?; let loader_info = mod_loaders .first() .and_then(|l| l["id"].as_str()) .ok_or_else(|| { PakkerError::InvalidImportFile("Missing loader id".to_string()) })?; let parts: Vec<&str> = loader_info.split('-').collect(); let loader_name = (*parts.first().unwrap_or(&"fabric")).to_string(); let loader_version = (*parts.get(1).unwrap_or(&"latest")).to_string(); let mut loaders = std::collections::HashMap::new(); loaders.insert(loader_name, loader_version); let mut lockfile = LockFile { target: Some(Target::CurseForge), mc_versions: vec![mc_version.clone()], loaders: loaders.clone(), projects: Vec::new(), lockfile_version: 1, }; // Import projects from files list if let Some(files) = manifest["files"].as_array() { log::info!("Importing {} projects from modpack", files.len()); // Create platform client use crate::platform::create_platform; let curseforge_token = std::env::var("CURSEFORGE_TOKEN").ok(); let platform = create_platform("curseforge", curseforge_token)?; for file_entry in files { if let Some(project_id) = file_entry["projectID"].as_u64() { let project_id_str = project_id.to_string(); log::info!("Fetching project: {project_id_str}"); match platform .request_project_with_files( &project_id_str, &lockfile.mc_versions, &loaders.keys().cloned().collect::>(), ) .await { Ok(mut project) => { // Try to select the specific file if fileID is provided if let Some(file_id) = file_entry["fileID"].as_u64() { let file_id_str = file_id.to_string(); // Try to find the file with matching ID if let Some(file) = project.files.iter().find(|f| f.id == file_id_str).cloned() { project.files = vec![file]; } else { log::warn!( "Could not find file {} for project {}, selecting best match", file_id, project.get_name() ); if let Err(e) = project.select_file( &lockfile.mc_versions, &loaders.keys().cloned().collect::>(), None, // Use default (1 file) during import ) { log::warn!( "Failed to select file for {}: {}", project.get_name(), e ); continue; } } } else { // No specific file ID, select best match if let Err(e) = project.select_file( &lockfile.mc_versions, &loaders.keys().cloned().collect::>(), None, // Use default (1 file) during import ) { log::warn!( "Failed to select file for {}: {}", project.get_name(), e ); continue; } } lockfile.add_project(project); }, Err(e) => { log::warn!("Failed to fetch project {project_id_str}: {e}"); }, } } } } // Create config let config = Config { name: manifest["name"] .as_str() .unwrap_or("Imported Pack") .to_string(), version: manifest["version"] .as_str() .unwrap_or("1.0.0") .to_string(), description: None, author: manifest["author"] .as_str() .map(std::string::ToString::to_string), overrides: vec!["overrides".to_string()], server_overrides: None, client_overrides: None, paths: Default::default(), projects: None, export_profiles: None, export_server_side_projects_to_client: None, file_count_preference: None, }; // Save files using provided paths lockfile.save(lockfile_dir)?; config.save(config_dir)?; log::info!("Imported {} projects", lockfile.projects.len()); // Extract overrides let overrides_prefix = manifest["overrides"].as_str().unwrap_or("overrides"); for i in 0..archive.len() { let mut file = archive.by_index(i)?; let outpath = file.enclosed_name().ok_or_else(|| { PakkerError::InternalError("Invalid file path in archive".to_string()) })?; if outpath.starts_with(overrides_prefix) { let target = outpath.strip_prefix(overrides_prefix).unwrap(); if file.is_dir() { std::fs::create_dir_all(target)?; } else { if let Some(parent) = target.parent() { std::fs::create_dir_all(parent)?; } let mut outfile = File::create(target)?; std::io::copy(&mut file, &mut outfile)?; } } } Ok(()) }