Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I694da71afe93bcb33687ff7d8e75f04f6a6a6964
404 lines
12 KiB
Rust
404 lines
12 KiB
Rust
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,
|
|
lockfile_path: &Path,
|
|
config_path: &Path,
|
|
) -> Result<()> {
|
|
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()) && !args.yes {
|
|
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)? {
|
|
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),
|
|
) {
|
|
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,
|
|
};
|
|
|
|
// 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::<Vec<_>>(),
|
|
)
|
|
.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::<Vec<_>>(),
|
|
) {
|
|
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::<Vec<_>>(),
|
|
) {
|
|
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,
|
|
};
|
|
|
|
// 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(())
|
|
}
|