pub mod archive;
pub mod events;
pub mod fetch;
pub mod github_api;
pub mod install;
pub mod probe;
pub mod resolve;
pub mod token;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender;
use crate::downloader::events::{DownloaderError, ProgressEvent, UnsupportedReason};
use crate::downloader::resolve::CandidateUrl;
pub struct InstallSpec {
pub name: String,
pub version: String,
pub repo_url: Option<String>,
pub bins: Vec<String>,
}
pub struct InstallOutcome {
pub name: String,
pub old_version: Option<String>,
pub new_version: String,
}
async fn try_api_winning_url(
client: &reqwest::Client,
spec: &InstallSpec,
repo_url: &str,
targets: &[String],
name_candidates: &[String],
) -> Option<CandidateUrl> {
let (owner, repo) = github_api::parse_owner_repo(repo_url)?;
let token = token::discover_token();
let expected = resolve::expected_filenames(name_candidates, &spec.version, targets);
let pkg = name_candidates.first()?;
let tags = [
format!("v{}", spec.version),
spec.version.clone(),
format!("{pkg}-v{}", spec.version),
format!("{pkg}-{}", spec.version),
format!("{pkg}/v{}", spec.version),
format!("{pkg}/{}", spec.version),
];
for tag in &tags {
match github_api::fetch_release_assets(
client,
"https://api.github.com",
&owner,
&repo,
tag,
token,
)
.await
{
Ok(assets) => {
if let Some(asset) = github_api::match_winning_asset(&assets, &expected) {
let archive_fmt = if asset.name.ends_with(".zip") {
resolve::ArchiveFmt::Zip
} else if asset.name.ends_with(".tar.gz") || asset.name.ends_with(".tgz") {
resolve::ArchiveFmt::TarGz
} else {
resolve::ArchiveFmt::Bin
};
return Some(CandidateUrl {
url: asset.browser_download_url.clone(),
archive_fmt,
});
}
}
Err(github_api::GithubApiError::NotFound) => continue,
Err(_) => return None, }
}
None
}
pub async fn download_and_install(
client: &reqwest::Client,
spec: InstallSpec,
old_version: Option<String>,
events: UnboundedSender<ProgressEvent>,
cancel: Arc<AtomicBool>,
) -> Result<InstallOutcome, DownloaderError> {
let _ = events.send(ProgressEvent::Resolving {
name: spec.name.clone(),
});
if cancel.load(Ordering::SeqCst) {
return Err(DownloaderError::Cancelled);
}
let targets = resolve::current_targets();
if targets.is_empty() {
return Err(DownloaderError::Unsupported(UnsupportedReason::UnsupportedPlatform));
}
let repo_url = spec
.repo_url
.as_deref()
.ok_or(DownloaderError::Unsupported(UnsupportedReason::NoMetadataAndNoConvention))?;
let mut name_candidates: Vec<String> = vec![spec.name.clone()];
for b in &spec.bins {
if !name_candidates.contains(b) {
name_candidates.push(b.clone());
}
}
let candidates =
match try_api_winning_url(client, &spec, repo_url, &targets, &name_candidates).await {
Some(winner) => vec![winner],
None => resolve::candidate_urls(&name_candidates, &spec.version, repo_url, &targets)?,
};
let fetched = fetch::fetch(client, &spec.name, &candidates, &events, cancel.clone()).await?;
if cancel.load(Ordering::SeqCst) {
return Err(DownloaderError::Cancelled);
}
let _ = events.send(ProgressEvent::Extracting {
name: spec.name.clone(),
});
let fmt = if fetched.winning_url.ends_with(".zip") {
resolve::ArchiveFmt::Zip
} else {
resolve::ArchiveFmt::TarGz
};
let mut bin_candidates: Vec<String> = spec.bins.clone();
if bin_candidates.is_empty() {
bin_candidates.push(spec.name.clone());
}
let extracted = archive::extract(&fetched.archive_path, fmt, &bin_candidates)?;
if cancel.load(Ordering::SeqCst) {
return Err(DownloaderError::Cancelled);
}
let _ = events.send(ProgressEvent::Installing {
name: spec.name.clone(),
});
let _installed_path = install::install_binary(
&extracted.binary_path,
&extracted.binary_name,
&spec.version,
)?;
let _ = events.send(ProgressEvent::Done {
name: spec.name.clone(),
version: spec.version.clone(),
});
Ok(InstallOutcome {
name: spec.name,
old_version,
new_version: spec.version,
})
}