mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-04-20 20:48:43 +00:00
128 lines
3.1 KiB
Rust
128 lines
3.1 KiB
Rust
use std::{
|
|
borrow::Borrow,
|
|
collections::HashSet,
|
|
hash::{Hash, Hasher},
|
|
io,
|
|
time::Duration,
|
|
};
|
|
|
|
use compact_str::CompactString;
|
|
use serde::Deserialize;
|
|
use thiserror::Error as ThisError;
|
|
use url::Url;
|
|
|
|
use super::{remote, GhRelease};
|
|
|
|
#[derive(ThisError, Debug)]
|
|
#[non_exhaustive]
|
|
pub enum GhApiError {
|
|
#[error("IO Error: {0}")]
|
|
Io(#[from] io::Error),
|
|
|
|
#[error("Remote Error: {0}")]
|
|
Remote(#[from] remote::Error),
|
|
|
|
#[error("Failed to parse url: {0}")]
|
|
InvalidUrl(#[from] url::ParseError),
|
|
}
|
|
|
|
// Only include fields we do care about
|
|
|
|
#[derive(Eq, Deserialize, Debug)]
|
|
struct Artifact {
|
|
name: CompactString,
|
|
}
|
|
|
|
// Manually implement PartialEq and Hash to ensure it will always produce the
|
|
// same hash as a str with the same content, and that the comparison will be
|
|
// the same to coparing a string.
|
|
|
|
impl PartialEq for Artifact {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.name.eq(&other.name)
|
|
}
|
|
}
|
|
|
|
impl Hash for Artifact {
|
|
fn hash<H>(&self, state: &mut H)
|
|
where
|
|
H: Hasher,
|
|
{
|
|
let s: &str = self.name.as_str();
|
|
s.hash(state)
|
|
}
|
|
}
|
|
|
|
// Implement Borrow so that we can use call
|
|
// `HashSet::contains::<str>`
|
|
|
|
impl Borrow<str> for Artifact {
|
|
fn borrow(&self) -> &str {
|
|
&self.name
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub(super) struct Artifacts {
|
|
assets: HashSet<Artifact>,
|
|
}
|
|
|
|
impl Artifacts {
|
|
pub(super) fn contains(&self, artifact_name: &str) -> bool {
|
|
self.assets.contains(artifact_name)
|
|
}
|
|
}
|
|
|
|
pub(super) enum FetchReleaseRet {
|
|
ReachedRateLimit { retry_after: Option<Duration> },
|
|
ReleaseNotFound,
|
|
Artifacts(Artifacts),
|
|
Unauthorized,
|
|
}
|
|
|
|
/// Returns 404 if not found
|
|
pub(super) async fn fetch_release_artifacts(
|
|
client: &remote::Client,
|
|
GhRelease { owner, repo, tag }: &GhRelease,
|
|
auth_token: Option<&str>,
|
|
) -> Result<FetchReleaseRet, GhApiError> {
|
|
let mut request_builder = client
|
|
.get(Url::parse(&format!(
|
|
"https://api.github.com/repos/{owner}/{repo}/releases/tags/{tag}"
|
|
))?)
|
|
.header("Accept", "application/vnd.github+json")
|
|
.header("X-GitHub-Api-Version", "2022-11-28");
|
|
|
|
if let Some(auth_token) = auth_token {
|
|
request_builder = request_builder.bearer_auth(&auth_token);
|
|
}
|
|
|
|
let response = request_builder.send(false).await?;
|
|
|
|
let status = response.status();
|
|
let headers = response.headers();
|
|
|
|
if status == remote::StatusCode::FORBIDDEN
|
|
&& headers
|
|
.get("x-ratelimit-remaining")
|
|
.map(|val| val == "0")
|
|
.unwrap_or(false)
|
|
{
|
|
return Ok(FetchReleaseRet::ReachedRateLimit {
|
|
retry_after: headers.get("x-ratelimit-reset").and_then(|value| {
|
|
let secs = value.to_str().ok()?.parse().ok()?;
|
|
Some(Duration::from_secs(secs))
|
|
}),
|
|
});
|
|
}
|
|
|
|
if status == remote::StatusCode::UNAUTHORIZED {
|
|
return Ok(FetchReleaseRet::Unauthorized);
|
|
}
|
|
|
|
if status == remote::StatusCode::NOT_FOUND {
|
|
return Ok(FetchReleaseRet::ReleaseNotFound);
|
|
}
|
|
|
|
Ok(FetchReleaseRet::Artifacts(response.json().await?))
|
|
}
|