initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ife1391ed23a1e7f388b1b5eca90b9ea76a6a6964
This commit is contained in:
commit
ef28bdaeb4
63 changed files with 17292 additions and 0 deletions
395
src/cli/commands/import.rs
Normal file
395
src/cli/commands/import.rs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
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);
|
||||
|
||||
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, &[
|
||||
loader.0.clone(),
|
||||
])
|
||||
.await
|
||||
{
|
||||
Ok(mut project) => {
|
||||
// Select best file
|
||||
if let Err(e) =
|
||||
project.select_file(&lockfile.mc_versions, &[loader.0.clone()])
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
// 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(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue