use std::{
fs::{self, File},
io::{Read, Write},
path::Path,
sync::Arc,
};
use anyhow::{bail, Result};
use bzip2::read::BzDecoder;
use camino::Utf8Path;
use flate2::read::GzDecoder;
use indicatif::ProgressBar;
use reqwest::Client;
use rustls::crypto::aws_lc_rs;
use rustls_platform_verifier::BuilderVerifierExt;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256 as Sha256Hasher};
use tar::Archive;
use xz2::read::XzDecoder;
#[derive(Debug, Deserialize, Serialize)]
struct Config {
sources: Vec<Source>,
}
#[derive(Debug, Deserialize, Serialize)]
struct Source {
name: String,
url: String,
target: String,
version: String,
hash: String,
}
#[tokio::main]
async fn main() -> Result<()> {
let file = fs::read_to_string("sugo.toml")?;
let config: Config = toml::from_str(&file)?;
let args: Vec<String> = std::env::args().collect();
if let Some(source) = args.get(1) {
print!(
"{}",
config
.sources
.iter()
.find_map(|src| {
if &src.name == source {
Some(src.version.as_str())
} else {
None
}
})
.unwrap_or("unknown")
);
return Ok(());
}
fs::create_dir_all("sources/tar")?;
let client = Client::builder()
.use_preconfigured_tls(
rustls::ClientConfig::builder_with_provider(Arc::new(aws_lc_rs::default_provider()))
.with_safe_default_protocol_versions()?
.with_platform_verifier()
.with_no_client_auth(),
)
.build()?;
for source in &config.sources {
download_source(&client, source).await?;
extract_source(source).await?;
}
println!("Sources OK");
Ok(())
}
async fn download_source(client: &Client, source: &Source) -> Result<()> {
let target_path: String = format!("sources/tar/{}", source.target);
if Path::new(&target_path).exists() && check_hash(&target_path, &source.hash)? {
return Ok(());
}
let mut target = File::create(target_path)?;
println!("Downloading {}...", source.name);
let mut res = client.get(&source.url).send().await?;
let len = res.content_length().unwrap_or(0);
let progress_bar = ProgressBar::new(len);
while let Some(chunk) = res.chunk().await? {
progress_bar.inc(chunk.len() as u64);
target.write_all(&chunk)?;
}
progress_bar.finish();
Ok(())
}
fn check_hash(path: &str, hash: &str) -> Result<bool> {
let file = fs::read(path)?;
let (hash_type, hash) = hash.split_once(':').unwrap_or(("blake3", hash));
let computed_hash = match hash_type {
"blake3" => blake3::hash(&file).to_hex().to_string(),
"sha256" => base16ct::lower::encode_string(Sha256Hasher::digest(&file).as_slice()),
_ => bail!("Unsupported hash"),
};
Ok(hash == computed_hash)
}
async fn extract_source(source: &Source) -> Result<()> {
let target_path = format!("sources/tar/{}", source.target);
let target_path = Utf8Path::new(&target_path);
let archive_path = format!("sources/{}", source.name);
let archive_path = Utf8Path::new(&archive_path);
if archive_path.join(".ok").exists() {
return Ok(());
}
if archive_path.exists() {
fs::remove_dir_all(archive_path)?;
}
let target = File::open(target_path)?;
match target_path.extension().unwrap() {
"xz" => {
unpack_archive(XzDecoder::new(target), &source.name)?;
}
"gz" => {
unpack_archive(GzDecoder::new(target), &source.name)?;
}
"bz2" => {
unpack_archive(BzDecoder::new(target), &source.name)?;
}
_ => bail!("Something went wrong extracting"),
}
Ok(())
}
fn unpack_archive<R: Read>(decoder: R, name: &str) -> Result<()> {
println!("Unpacking {name}");
let mut archive = Archive::new(decoder);
archive.unpack(&format!("sources/{name}"))?;
File::create(&format!("sources/{name}/.ok"))?;
Ok(())
}