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
216
src/cli/commands/sync.rs
Normal file
216
src/cli/commands/sync.rs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
use crate::{
|
||||
cli::SyncArgs,
|
||||
error::{PakkerError, Result},
|
||||
fetch::Fetcher,
|
||||
model::{Config, LockFile},
|
||||
platform::{CurseForgePlatform, ModrinthPlatform, PlatformClient},
|
||||
};
|
||||
|
||||
enum SyncChange {
|
||||
Addition(PathBuf, String), // (file_path, project_name)
|
||||
Removal(String), // project_pakku_id
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
args: SyncArgs,
|
||||
lockfile_path: &Path,
|
||||
config_path: &Path,
|
||||
) -> Result<()> {
|
||||
log::info!("Synchronizing with lockfile");
|
||||
|
||||
let lockfile_dir = lockfile_path.parent().unwrap_or(Path::new("."));
|
||||
let config_dir = config_path.parent().unwrap_or(Path::new("."));
|
||||
|
||||
let mut lockfile = LockFile::load(lockfile_dir)?;
|
||||
let config = Config::load(config_dir)?;
|
||||
|
||||
// Detect changes
|
||||
let changes = detect_changes(&lockfile, &config)?;
|
||||
|
||||
if changes.is_empty() {
|
||||
println!("✓ Everything is in sync");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Filter changes based on flags
|
||||
let mut additions = Vec::new();
|
||||
let mut removals = Vec::new();
|
||||
|
||||
for change in changes {
|
||||
match change {
|
||||
SyncChange::Addition(path, name) => additions.push((path, name)),
|
||||
SyncChange::Removal(id) => removals.push(id),
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
let no_filter = !args.additions && !args.removals;
|
||||
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
spinner.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.green} {msg}")
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
if no_filter || args.additions {
|
||||
for (file_path, _) in &additions {
|
||||
spinner
|
||||
.set_message(format!("Processing addition: {}", file_path.display()));
|
||||
if prompt_user(&format!("Add {} to lockfile?", file_path.display()))? {
|
||||
add_file_to_lockfile(&mut lockfile, file_path, &config).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if no_filter || args.removals {
|
||||
for pakku_id in &removals {
|
||||
if let Some(project) = lockfile
|
||||
.projects
|
||||
.iter()
|
||||
.find(|p| p.pakku_id.as_ref() == Some(pakku_id))
|
||||
{
|
||||
let name = project
|
||||
.name
|
||||
.values()
|
||||
.next()
|
||||
.map(std::string::String::as_str)
|
||||
.or(project.pakku_id.as_deref())
|
||||
.unwrap_or("unknown");
|
||||
spinner.set_message(format!("Processing removal: {name}"));
|
||||
if prompt_user(&format!("Remove {name} from lockfile?"))? {
|
||||
lockfile
|
||||
.remove_project(pakku_id)
|
||||
.ok_or_else(|| PakkerError::ProjectNotFound(pakku_id.clone()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spinner.finish_and_clear();
|
||||
|
||||
// Save changes
|
||||
lockfile.save(lockfile_dir)?;
|
||||
|
||||
// Fetch missing files
|
||||
let fetcher = Fetcher::new(".");
|
||||
fetcher.sync(&lockfile, &config).await?;
|
||||
|
||||
println!("✓ Sync complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn detect_changes(
|
||||
lockfile: &LockFile,
|
||||
config: &Config,
|
||||
) -> Result<Vec<SyncChange>> {
|
||||
let mut changes = Vec::new();
|
||||
|
||||
// Get paths for each project type
|
||||
let paths = config.paths.clone();
|
||||
let mods_path = paths
|
||||
.get("mods")
|
||||
.map_or("mods", std::string::String::as_str);
|
||||
|
||||
// Build map of lockfile projects by file path
|
||||
let mut lockfile_files: HashMap<PathBuf, String> = HashMap::new();
|
||||
for project in &lockfile.projects {
|
||||
for file in &project.files {
|
||||
let file_path = PathBuf::from(mods_path).join(&file.file_name);
|
||||
if let Some(ref pakku_id) = project.pakku_id {
|
||||
lockfile_files.insert(file_path, pakku_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan filesystem for additions
|
||||
if let Ok(entries) = fs::read_dir(mods_path) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file()
|
||||
&& let Some(ext) = path.extension()
|
||||
&& ext == "jar"
|
||||
&& !lockfile_files.contains_key(&path)
|
||||
{
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
changes.push(SyncChange::Addition(path, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removals (projects in lockfile but files missing)
|
||||
let filesystem_files: HashSet<_> =
|
||||
if let Ok(entries) = fs::read_dir(mods_path) {
|
||||
entries
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.is_file())
|
||||
.collect()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
for (lockfile_path, pakku_id) in &lockfile_files {
|
||||
if !filesystem_files.contains(lockfile_path) {
|
||||
changes.push(SyncChange::Removal(pakku_id.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
async fn add_file_to_lockfile(
|
||||
lockfile: &mut LockFile,
|
||||
file_path: &Path,
|
||||
_config: &Config,
|
||||
) -> Result<()> {
|
||||
// Try to identify the file by hash lookup
|
||||
let _modrinth = ModrinthPlatform::new();
|
||||
let curseforge = CurseForgePlatform::new(None);
|
||||
|
||||
// Compute file hash
|
||||
let file_data = fs::read(file_path)?;
|
||||
// Compute SHA-1 hash from file bytes
|
||||
use sha1::Digest;
|
||||
let mut hasher = sha1::Sha1::new();
|
||||
hasher.update(&file_data);
|
||||
let hash = format!("{:x}", hasher.finalize());
|
||||
|
||||
// Try Modrinth first (SHA-1 hash)
|
||||
if let Ok(Some(project)) = _modrinth.lookup_by_hash(&hash).await {
|
||||
lockfile.add_project(project);
|
||||
println!("✓ Added {} (from Modrinth)", file_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try CurseForge (Murmur2 hash computed from file)
|
||||
if let Ok(Some(project)) = curseforge.lookup_by_hash(&hash).await {
|
||||
lockfile.add_project(project);
|
||||
println!("✓ Added {} (from CurseForge)", file_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("⚠ Could not identify {}, skipping", file_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_user(message: &str) -> Result<bool> {
|
||||
print!("{message} [y/N] ");
|
||||
io::stdout().flush().map_err(PakkerError::IoError)?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.map_err(PakkerError::IoError)?;
|
||||
|
||||
Ok(input.trim().eq_ignore_ascii_case("y"))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue