use anyhow::{anyhow, Context as _};
use super::version::Version;
pub(super) const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
pub(super) const MAX_ASSET_BYTES: u64 = 128 * 1024 * 1024;
pub(super) fn repository_parts() -> anyhow::Result<(String, String)> {
crate_plugin_kit::install::prebuilt::parse_github_repo(REPOSITORY).ok_or_else(|| {
anyhow!("`pmpx self update` only knows how to read GitHub releases, and {REPOSITORY} is not a GitHub repository")
})
}
pub(super) fn asset_stem() -> String {
format!("pmpx-{}", crate_plugin_kit::TARGET_TRIPLE)
}
pub(super) fn archive_ext(os: &str) -> &'static str {
if os == "windows" {
"zip"
} else {
"tar.gz"
}
}
pub(super) fn binary_name(os: &str) -> &'static str {
if os == "windows" {
"pmpx.exe"
} else {
"pmpx"
}
}
pub(super) fn latest_version() -> anyhow::Result<Version> {
let (owner, repo) = repository_parts()?;
let url = format!("https://api.github.com/repos/{owner}/{repo}/releases/latest");
let body = match download(&url) {
Ok(Some(body)) => body,
Ok(None) => return Err(anyhow!("no release has been published yet")),
Err(error) => {
let mut error = error;
if error.to_string().contains("403") {
error = error.context(
"GitHub rate-limits requests that are not authenticated; trying again later usually works",
);
}
return Err(error.context(format!("cannot ask {url}")));
}
};
let json: serde_json::Value =
serde_json::from_slice(&body).context("the releases API did not return JSON")?;
let tag = json
.get("tag_name")
.and_then(|value| value.as_str())
.ok_or_else(|| anyhow!("the releases API returned no tag_name"))?;
Version::parse(tag)
.ok_or_else(|| anyhow!("the latest release is tagged {tag}, which is not a version"))
}
pub(super) fn download(url: &str) -> anyhow::Result<Option<Vec<u8>>> {
let request = ureq::get(url).header("User-Agent", &user_agent());
match request.call() {
Ok(mut response) => {
let body = response
.body_mut()
.read_to_vec()
.with_context(|| format!("cannot read the response from {url}"))?;
if body.len() as u64 > MAX_ASSET_BYTES {
return Err(anyhow!(
"{url} is larger than the {MAX_ASSET_BYTES} byte limit ({} bytes)",
body.len()
));
}
Ok(Some(body))
}
Err(ureq::Error::StatusCode(404)) => Ok(None),
Err(error) => Err(anyhow!("{error} ({url})")),
}
}
pub(super) fn user_agent() -> String {
format!("pmpx/{} (+{REPOSITORY})", env!("CARGO_PKG_VERSION"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_archive_matches_the_platform() {
assert_eq!(archive_ext("windows"), "zip");
assert_eq!(archive_ext("linux"), "tar.gz");
assert_eq!(archive_ext("macos"), "tar.gz");
assert_eq!(binary_name("windows"), "pmpx.exe");
assert_eq!(binary_name("linux"), "pmpx");
assert_eq!(binary_name("macos"), "pmpx");
}
#[test]
fn the_asset_stem_carries_the_target() {
assert_eq!(
asset_stem(),
format!("pmpx-{}", crate_plugin_kit::TARGET_TRIPLE)
);
}
}