vendor 0.2.0

Easy to use package manager library
Documentation
use crate::{project, MESSAGES};

use anyhow::Context;
use brown;
use colored::Colorize;
use flate2::read::GzDecoder;
use futures_util::StreamExt;
use global_placeholders::global;
use indicatif::{HumanDuration, ProgressBar, ProgressStyle};
use macros_rs::{fmtstr, string, ternary};
use std::cmp::min;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use std::{fs, fs::File, io::Write};
use tar::Archive;

#[derive(Debug, serde::Deserialize)]
struct Dist {
    version: String,
    tarball: String,
}

#[derive(Debug, serde::Deserialize)]
struct Response {
    dist: Dist,
}

fn remove_file(file: &str) {
    if let Err(_) = fs::remove_file(file) {
        eprintln!("{} {}", "".red(), MESSAGES.get("file_error").unwrap().bright_red());
        std::process::exit(1);
    }
}
fn move_package(file: &str, name: &str, version: &str) {
    let current_dir = std::env::current_dir().expect(MESSAGES.get("cwd_error").unwrap());
    let mut package = project::package::read();
    let dependencies = package.dependencies.clone();

    if !std::path::Path::new(fmtstr!("{}/packages", current_dir.display())).is_dir() {
        std::fs::create_dir_all(format!("{}/packages", current_dir.display())).unwrap();
        log::debug!("created {}/packages", current_dir.display())
    }

    match File::open(file) {
        Ok(tarball) => {
            let tar = GzDecoder::new(tarball);
            let mut archive = Archive::new(tar);

            archive
                .unpack(format!("{}/packages/{name}@{version}", current_dir.display()))
                .expect(MESSAGES.get("unpack_error").unwrap());
            remove_file(file);

            if package.dependencies.get(name) == None {
                package.dependencies.insert(name.to_string(), version.to_string());
            } else {
                let mut versions = dependencies.get(name).unwrap().split(",").collect::<Vec<&str>>();

                if versions.last().unwrap() != &version {
                    versions.push(&version);
                    package.dependencies.insert(name.to_string(), versions.join(",").trim_matches(' ').to_string());
                }
            }

            if let Err(err) = File::create("package.yml").unwrap().write_all(serde_yaml::to_string(&package).unwrap().as_bytes()) {
                eprintln!("{} {}", "".red(), format!("{}{name}@{version}, {err}", MESSAGES.get("pkg_add_error").unwrap()).bright_red());
                std::process::exit(1);
            };
        }
        Err(_) => {
            eprintln!("{} {}", "".red(), MESSAGES.get("pkg_fs_error").unwrap().bright_red());
            remove_file(file);
            std::process::exit(1);
        }
    }
}

pub async fn download(client: &reqwest::Client, url: &str, path: &str, package_info: String) -> Result<(), String> {
    let res = client
        .get(url)
        .send()
        .await
        .or(Err(format!("\r{} {}\n", "".red(), format!("{}{}", MESSAGES.get("pkg_fetch_error").unwrap(), &url).bright_red())))?;

    let total_size = res
        .content_length()
        .ok_or(format!("\r{} {}\n", "".red(), format!("{}{}", MESSAGES.get("pkg_content_error").unwrap(), &url).bright_red()))?;

    let pb = ProgressBar::new(total_size);
    pb.set_style(ProgressStyle::with_template("{msg}: [{bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap());
    pb.set_message(format!("{}", format!("+ {package_info}").bright_cyan()));

    let mut file = File::create(path).or(Err(format!("{}'{}'", MESSAGES.get("pkg_create_error").unwrap(), path)))?;
    let mut downloaded: u64 = 0;
    let mut stream = res.bytes_stream();

    while let Some(item) = stream.next().await {
        let chunk = item.or(Err(string!(MESSAGES.get("download_error").unwrap())))?;
        file.write_all(&chunk).or(Err(string!(MESSAGES.get("write_error").unwrap())))?;
        let new = min(downloaded + (chunk.len() as u64), total_size);
        downloaded = new;
        pb.set_position(new);
    }

    pb.finish_with_message(format!("{}", format!("+ {package_info}").bright_cyan()));
    return Ok(());
}

pub fn install() {
    let started = Instant::now();
    let packages = project::package::read().dependencies;
    for (name, versions) in &packages {
        for ver in versions.split(",").collect::<Vec<&str>>() {
            add(&format!("{}@{}", name, ver.trim_matches(' ')), false)
        }
    }
    println!("{}", format!("{}{}", MESSAGES.get("install_done").unwrap(), HumanDuration(started.elapsed())).yellow());
}

pub fn add(input: &str, timer: bool) {
    let app_name = global!("vendor.name");
    let registry = global!("vendor.registry");

    let version;
    let started = Instant::now();
    let name = input.split("@").collect::<Vec<&str>>()[0];
    let current_dir = std::env::current_dir().expect(MESSAGES.get("cwd_error").unwrap());
    let client = reqwest::blocking::Client::builder().user_agent(format!("{app_name}/{}", env!("CARGO_PKG_VERSION"))).build().unwrap();
    let style = ProgressStyle::with_template("{spinner:.yellow} {msg}").unwrap().tick_strings(&[
        "[    ]", "[=   ]", "[==  ]", "[=== ]", "[ ===]", "[  ==]", "[   =]", "[    ]", "[   =]", "[  ==]", "[ ===]", "[====]", "[=== ]", "[==  ]", "[=   ]", "",
    ]);

    let package_info = ternary!(
        input.split("@").collect::<Vec<&str>>().len() > 1,
        format!("{}@{}", input.split("@").collect::<Vec<&str>>()[0], input.split("@").collect::<Vec<&str>>()[1]),
        input.split("@").collect::<Vec<&str>>()[0].to_string()
    );

    let pb = ProgressBar::new_spinner();
    pb.enable_steady_tick(Duration::from_millis(80));
    pb.set_style(style.clone());
    pb.set_message(string!(MESSAGES.get("pkg_find_msg").unwrap()));

    match client.get(format!("{registry}/{package_info}")).send() {
        Ok(res) => {
            match serde_json::from_str::<Response>(&res.text().unwrap()) {
                Ok(json) => {
                    version = json.dist.version.clone();
                    if !std::path::Path::new(fmtstr!("{}/packages/{name}@{version}", current_dir.display())).is_dir() {
                        pb.finish_with_message(format!("\x08{} {}", "".green(), format!("{}{name}@{}", MESSAGES.get("pkg_found").unwrap(), json.dist.version).green()));

                        let runtime = tokio::runtime::Runtime::new().unwrap();
                        match runtime.block_on(download(&reqwest::Client::new(), &json.dist.tarball, &format!("{name}.tgz"), format!("{name}@{}", &json.dist.version))) {
                            Ok(_) => move_package(&format!("{name}.tgz"), &name, &json.dist.version),
                            Err(err) => {
                                eprint!(
                                    "\r{} {}\n",
                                    "".red(),
                                    format!("{}{}: {}", MESSAGES.get("pkg_error").unwrap(), package_info, err.to_string()).bright_red()
                                );
                                std::process::exit(1);
                            }
                        };
                    } else {
                        pb.finish_with_message(format!(
                            "\x08{} {}",
                            "".magenta(),
                            format!("{}{name}@{}", MESSAGES.get("pkg_skip").unwrap(), json.dist.version).bright_magenta()
                        ));
                    }
                }
                Err(_) => {
                    pb.finish_with_message(format!("\x08{} {}", "".red(), format!("{}{}", MESSAGES.get("find_error").unwrap(), package_info).bright_red()));
                    std::process::exit(1);
                }
            };
        }
        Err(err) => {
            eprint!(
                "\r{} {}\n",
                "".red(),
                format!("{}{}: {}", MESSAGES.get("pkg_error").unwrap(), package_info, err.to_string()).bright_red()
            );
            std::process::exit(1);
        }
    };

    match reqwest::blocking::get(format!(
        "{registry}/api/v{}/dependencies/{}",
        env!("CARGO_PKG_VERSION").split(".").collect::<Vec<&str>>().join(""),
        input.split("@").collect::<Vec<&str>>()[0].to_string()
    )) {
        Ok(res) => {
            match serde_json::from_str::<HashMap<String, Vec<String>>>(&res.text().unwrap()) {
                Ok(json) => {
                    for link in &json[&version] {
                        let pb_dep = ProgressBar::new_spinner();
                        let name = link.split("/").collect::<Vec<&str>>()[3];
                        let version = link.split("/").collect::<Vec<&str>>()[5];

                        pb_dep.enable_steady_tick(Duration::from_millis(80));
                        pb_dep.set_style(style.clone());
                        pb_dep.set_message("locating...");

                        if !std::path::Path::new(fmtstr!("{}/packages/{name}@{version}", current_dir.display())).is_dir() {
                            pb_dep.finish_with_message(format!("\x08{} {}", "".green(), format!("{}{name}@{}", MESSAGES.get("dep_found").unwrap(), &version).bright_green()));
                        } else {
                            pb_dep.finish_with_message(format!(
                                "\x08{} {}",
                                "".magenta(),
                                format!("{}{name}@{}", MESSAGES.get("dep_skip").unwrap(), &version).bright_magenta()
                            ));
                        }
                    }

                    for link in &json[&version] {
                        let name = link.split("/").collect::<Vec<&str>>()[3];
                        let version = link.split("/").collect::<Vec<&str>>()[5];
                        let runtime = tokio::runtime::Runtime::new().unwrap();
                        if !std::path::Path::new(fmtstr!("{}/packages/{name}@{version}", current_dir.display())).is_dir() {
                            match runtime.block_on(download(&reqwest::Client::new(), link, &format!("{name}.tgz"), format!("{name}@{}", version))) {
                                Ok(_) => move_package(&format!("{name}.tgz"), &name, &version),
                                Err(err) => {
                                    eprint!(
                                        "\r{} {}\n",
                                        "".red(),
                                        format!("{}{}: {}", MESSAGES.get("pkg_error").unwrap(), package_info, err.to_string()).bright_red()
                                    );
                                    std::process::exit(1);
                                }
                            };
                        }
                    }
                }
                Err(_) => {}
            };
        }
        Err(err) => {
            eprint!(
                "\r{} {}\n",
                "".red(),
                format!("{}{}: {}", MESSAGES.get("pkg_error").unwrap(), package_info, err.to_string()).bright_red()
            );
            std::process::exit(1);
        }
    };
    if timer {
        println!("{}", format!("{}{}", MESSAGES.get("install_done").unwrap(), HumanDuration(started.elapsed())).yellow());
    }
}

pub fn remove(name: &String) {
    let started = Instant::now();
    let current_dir = std::env::current_dir().expect(MESSAGES.get("cwd_error").unwrap());
    let mut package = project::package::read();
    let dependencies = package.dependencies.clone();
    let key = name.split("@").collect::<Vec<&str>>()[0];
    let generic_error = |err: String| -> String { format!("{} {}", "".red(), format!("{}{name}, {err}", MESSAGES.get("remove_error").unwrap()).bright_red()) };

    let mut versions = match dependencies.get(key).with_context(|| generic_error(string!(MESSAGES.get("is_question_install").unwrap()))) {
        Ok(content) => content.split(",").collect::<Vec<&str>>(),
        Err(err) => {
            eprintln!("{err}");
            std::process::exit(1);
        }
    };

    let package_dir = ternary!(
        name.split("@").collect::<Vec<&str>>().len() > 1,
        format!("{}/{}", name.split("@").collect::<Vec<&str>>()[0], name.split("@").collect::<Vec<&str>>()[1]),
        name.split("@").collect::<Vec<&str>>()[0].to_string()
    );

    if let Err(_) = std::fs::remove_dir_all(format!("{}/packages/{package_dir}", current_dir.display())) {
        eprintln!("{}", generic_error(string!(MESSAGES.get("is_question_install").unwrap())));
        std::process::exit(1);
    } else {
        if name.split("@").collect::<Vec<&str>>().len() > 1 {
            if versions.len() > 1 {
                versions.remove(versions.iter().position(|x| &*x.trim_matches(' ') == name.split("@").collect::<Vec<&str>>()[1]).unwrap());
                package.dependencies.remove(key);
                package.dependencies.insert(key.to_string(), String::from(versions.join(",").trim_matches(' ')));
            } else {
                package.dependencies.remove(key);
            }
        } else {
            package.dependencies.remove(name);
        }

        if let Err(err) = File::create("package.yml").unwrap().write_all(serde_yaml::to_string(&package).unwrap().as_bytes()) {
            eprintln!("{}", generic_error(err.to_string()));
            std::process::exit(1);
        }
        println!("\x08{} {}", "".green(), format!("{}{name}", MESSAGES.get("pkg_remove").unwrap()).green());
    }

    println!("{}", format!("{}{}", MESSAGES.get("install_done").unwrap(), HumanDuration(started.elapsed())).yellow());
}

pub fn clean() {
    let package = project::package::read();
    let dependencies = package.dependencies.clone();
    let generic_error = |name: &str, err: &str| -> String { format!("{} {}", "".red(), format!("{}{name}, {err}", MESSAGES.get("remove_error").unwrap()).bright_red()) };

    match brown::get_dirs("packages") {
        Ok(paths) => {
            for path in paths {
                let package_dir = brown::direntry_to_path(&path).unwrap();
                let package_name = package_dir.split('/').last().unwrap();

                if dependencies.get(package_name).is_none() {
                    if let Err(_) = brown::remove_dir_brute(&package_dir) {
                        eprintln!("{}", generic_error(package_name, MESSAGES.get("is_question_install").unwrap()));
                        std::process::exit(1);
                    } else {
                        println!("\x08{} {}", "".blue(), format!("{}{package_name}", MESSAGES.get("pkg_unused").unwrap()).bright_blue());
                    }
                }
            }
        }
        Err(_) => eprintln!("{} {}", "".red(), MESSAGES.get("clean_error").unwrap().bright_red()),
    };
}