pakker/src/cli/commands/import.rs
NotAShelf e01313066d
cli: wire shelve flag; more clippy fixes
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I694da71afe93bcb33687ff7d8e75f04f6a6a6964
2026-03-03 23:34:53 +03:00

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(())
}