cari 0.1.0

Simple package manager for minecraft servers
use color_eyre::eyre::{self, Result};
use futures::stream::{self, StreamExt, TryStreamExt};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use sha2::{Digest, Sha512};
use std::path::PathBuf;
use std::time::Duration;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use yansi::Paint;

pub const USER_AGENT: &str = "bedrocko (+https://bedrocko.com)";

pub async fn sha512sum(path: &PathBuf) -> Result<String> {
    let mut file = tokio::fs::File::open(path).await?;
    let mut hasher = <Sha512 as Digest>::new();
    let mut buffer = [0u8; 8192];

    loop {
        let n = file.read(&mut buffer).await?;
        if n == 0 {
            break;
        }
        hasher.update(&buffer[..n]);
    }

    Ok(hex::encode(hasher.finalize()))
}

pub struct Downloader(Vec<Download>);

impl Downloader {
    pub fn new() -> Self {
        Self(vec![])
    }

    pub fn add(&mut self, url: String, dest: PathBuf, sha512: String) {
        self.0.push(Download { url, dest, sha512 });
    }

    pub fn add_from(&mut self, downloads: &mut Vec<Download>) {
        self.0.append(downloads);
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }

    async fn download_item(
        &self,
        client: &reqwest::Client,
        download: &Download,
        multi: &MultiProgress,
    ) -> Result<()> {
        let filename = download
            .dest
            .file_name()
            .map(|name| name.to_string_lossy().to_string())
            .unwrap_or_else(|| download.url.clone());

        let pb = multi.add(ProgressBar::new_spinner());
        pb.set_style(
            ProgressStyle::with_template(" {spinner:.blue} {msg}")
                .unwrap()
                .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⠋"),
        );
        pb.set_message(filename.clone());
        pb.enable_steady_tick(Duration::from_millis(80));

        let result = self.download_file(client, download).await;

        pb.set_style(ProgressStyle::with_template(" {msg}").unwrap());
        match &result {
            Ok(()) => pb.finish_with_message(format!("{} {}", "".green(), filename)),
            Err(_) => pb.finish_with_message(format!("{} {}", "".red(), filename)),
        }

        result
    }

    async fn download_file(&self, client: &reqwest::Client, download: &Download) -> Result<()> {
        let mut stream = client
            .get(&download.url)
            .send()
            .await?
            .error_for_status()?
            .bytes_stream();

        let mut file = File::create(&download.dest).await?;
        let mut hasher = <Sha512 as Digest>::new();

        while let Some(chunk) = stream.next().await {
            let chunk = chunk?;
            hasher.update(&chunk);
            file.write_all(&chunk).await?;
        }
        file.flush().await?;

        if hex::encode(hasher.finalize()) != download.sha512 {
            eyre::bail!("sha512 mismatch");
        }

        Ok(())
    }

    pub async fn download(self) -> Result<()> {
        if self.0.is_empty() {
            return Ok(());
        }

        println!();
        println!("   {}", "Downloading".blue().bold().underline());

        let client = reqwest::Client::builder().user_agent(USER_AGENT).build()?;
        let multi = MultiProgress::new();

        stream::iter(&self.0)
            .map(|item| self.download_item(&client, item, &multi))
            .buffer_unordered(8)
            .try_collect::<Vec<_>>()
            .await?;

        Ok(())
    }
}

pub struct Download {
    pub url: String,
    pub dest: PathBuf,
    pub sha512: String,
}