mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-06-17 08:06:38 +00:00
Refactor: Move HasReleaseArtifacts
failure variants into GhApiError
Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
This commit is contained in:
parent
5417475542
commit
7bd8468c35
8 changed files with 129 additions and 257 deletions
|
@ -8,7 +8,7 @@ pub(super) use binstalk_downloader::{
|
||||||
remote::{Client, Url},
|
remote::{Client, Url},
|
||||||
};
|
};
|
||||||
pub(super) use binstalk_git_repo_api::gh_api_client::GhApiClient;
|
pub(super) use binstalk_git_repo_api::gh_api_client::GhApiClient;
|
||||||
use binstalk_git_repo_api::gh_api_client::{GhReleaseArtifact, HasReleaseArtifact};
|
use binstalk_git_repo_api::gh_api_client::{GhApiError, GhReleaseArtifact};
|
||||||
pub(super) use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta};
|
pub(super) use binstalk_types::cargo_toml_binstall::{PkgFmt, PkgMeta};
|
||||||
pub(super) use compact_str::CompactString;
|
pub(super) use compact_str::CompactString;
|
||||||
pub(super) use tokio::task::JoinHandle;
|
pub(super) use tokio::task::JoinHandle;
|
||||||
|
@ -34,22 +34,24 @@ pub(super) async fn does_url_exist(
|
||||||
debug!("Using GitHub API to check for existence of artifact, which will also cache the API response");
|
debug!("Using GitHub API to check for existence of artifact, which will also cache the API response");
|
||||||
|
|
||||||
// The future returned has the same size as a pointer
|
// The future returned has the same size as a pointer
|
||||||
match gh_api_client.has_release_artifact(artifact).await? {
|
match gh_api_client.has_release_artifact(artifact).await {
|
||||||
HasReleaseArtifact::Yes { .. } => return Ok(true),
|
Ok(Some(_)) => return Ok(true),
|
||||||
HasReleaseArtifact::No | HasReleaseArtifact::NoSuchRelease => return Ok(false),
|
Ok(None) | Err(GhApiError::NotFound) => return Ok(false),
|
||||||
|
|
||||||
HasReleaseArtifact::RateLimit { retry_after } => {
|
Err(GhApiError::RateLimit { retry_after }) => {
|
||||||
WARN_RATE_LIMIT_ONCE.call_once(|| {
|
WARN_RATE_LIMIT_ONCE.call_once(|| {
|
||||||
warn!("Your GitHub API token (if any) has reached its rate limit and cannot be used again until {retry_after:?}, so we will fallback to HEAD/GET on the url.");
|
warn!("Your GitHub API token (if any) has reached its rate limit and cannot be used again until {retry_after:?}, so we will fallback to HEAD/GET on the url.");
|
||||||
warn!("If you did not supply a github token, consider doing so: GitHub limits unauthorized users to 60 requests per hour per origin IP address.");
|
warn!("If you did not supply a github token, consider doing so: GitHub limits unauthorized users to 60 requests per hour per origin IP address.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HasReleaseArtifact::Unauthorized => {
|
Err(GhApiError::Unauthorized) => {
|
||||||
WARN_UNAUTHORIZED_ONCE.call_once(|| {
|
WARN_UNAUTHORIZED_ONCE.call_once(|| {
|
||||||
warn!("GitHub API somehow requires a token for the API access, so we will fallback to HEAD/GET on the url.");
|
warn!("GitHub API somehow requires a token for the API access, so we will fallback to HEAD/GET on the url.");
|
||||||
warn!("Please consider supplying a token to cargo-binstall to speedup resolution.");
|
warn!("Please consider supplying a token to cargo-binstall to speedup resolution.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
GH_API_CLIENT_FAILED.store(true, Relaxed);
|
GH_API_CLIENT_FAILED.store(true, Relaxed);
|
||||||
|
|
|
@ -130,40 +130,22 @@ impl GhApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FetchReleaseArtifactError {
|
|
||||||
Error(GhApiError),
|
|
||||||
RateLimit { retry_after: Instant },
|
|
||||||
Unauthorized,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GhApiClient {
|
impl GhApiClient {
|
||||||
async fn do_fetch_release_artifacts(
|
async fn do_fetch_release_artifacts(
|
||||||
&self,
|
&self,
|
||||||
release: &GhRelease,
|
release: &GhRelease,
|
||||||
auth_token: Option<&str>,
|
auth_token: Option<&str>,
|
||||||
) -> Result<Option<release_artifacts::Artifacts>, FetchReleaseArtifactError> {
|
) -> Result<Option<release_artifacts::Artifacts>, GhApiError> {
|
||||||
use common::GhApiRet::*;
|
|
||||||
use FetchReleaseArtifactError as Error;
|
|
||||||
|
|
||||||
match release_artifacts::fetch_release_artifacts(&self.0.client, release, auth_token).await
|
match release_artifacts::fetch_release_artifacts(&self.0.client, release, auth_token).await
|
||||||
{
|
{
|
||||||
Ok(NotFound) => Ok(None),
|
Ok(artifacts) => Ok(Some(artifacts)),
|
||||||
Ok(Success(artifacts)) => Ok(Some(artifacts)),
|
Err(GhApiError::NotFound) => Ok(None),
|
||||||
Ok(ReachedRateLimit { retry_after }) => {
|
Err(err) => Err(err),
|
||||||
let retry_after = retry_after.unwrap_or(DEFAULT_RETRY_DURATION);
|
|
||||||
|
|
||||||
let now = Instant::now();
|
|
||||||
let retry_after = now
|
|
||||||
.checked_add(retry_after)
|
|
||||||
.unwrap_or_else(|| now + DEFAULT_RETRY_DURATION);
|
|
||||||
|
|
||||||
Err(Error::RateLimit { retry_after })
|
|
||||||
}
|
|
||||||
Ok(Unauthorized) => Err(Error::Unauthorized),
|
|
||||||
Err(err) => Err(Error::Error(err)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `Ok(Some(api_artifact_url))` if exists.
|
||||||
|
///
|
||||||
/// The returned future is guaranteed to be pointer size.
|
/// The returned future is guaranteed to be pointer size.
|
||||||
pub async fn has_release_artifact(
|
pub async fn has_release_artifact(
|
||||||
&self,
|
&self,
|
||||||
|
@ -171,9 +153,7 @@ impl GhApiClient {
|
||||||
release,
|
release,
|
||||||
artifact_name,
|
artifact_name,
|
||||||
}: GhReleaseArtifact,
|
}: GhReleaseArtifact,
|
||||||
) -> Result<HasReleaseArtifact, GhApiError> {
|
) -> Result<Option<CompactString>, GhApiError> {
|
||||||
use FetchReleaseArtifactError as Error;
|
|
||||||
|
|
||||||
let once_cell = self.0.release_artifacts.get(release.clone());
|
let once_cell = self.0.release_artifacts.get(release.clone());
|
||||||
let res = once_cell
|
let res = once_cell
|
||||||
.get_or_try_init(|| {
|
.get_or_try_init(|| {
|
||||||
|
@ -183,7 +163,9 @@ impl GhApiClient {
|
||||||
|
|
||||||
if let Some(retry_after) = *guard {
|
if let Some(retry_after) = *guard {
|
||||||
if retry_after.elapsed().is_zero() {
|
if retry_after.elapsed().is_zero() {
|
||||||
return Err(Error::RateLimit { retry_after });
|
return Err(GhApiError::RateLimit {
|
||||||
|
retry_after: Some(retry_after - Instant::now()),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Instant retry_after is already reached.
|
// Instant retry_after is already reached.
|
||||||
*guard = None;
|
*guard = None;
|
||||||
|
@ -196,7 +178,7 @@ impl GhApiClient {
|
||||||
.do_fetch_release_artifacts(&release, self.0.auth_token.as_deref())
|
.do_fetch_release_artifacts(&release, self.0.auth_token.as_deref())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(Error::Unauthorized) => {
|
Err(GhApiError::Unauthorized) => {
|
||||||
self.0.is_auth_token_valid.store(false, Relaxed);
|
self.0.is_auth_token_valid.store(false, Relaxed);
|
||||||
}
|
}
|
||||||
res => return res,
|
res => return res,
|
||||||
|
@ -209,52 +191,19 @@ impl GhApiClient {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(Some(artifacts)) => Ok(artifacts
|
Ok(Some(artifacts)) => Ok(artifacts.get_artifact_url(&artifact_name)),
|
||||||
.get_artifact_url(&artifact_name)
|
Ok(None) => Ok(None),
|
||||||
.map(|url| HasReleaseArtifact::Yes { url })
|
Err(GhApiError::RateLimit { retry_after }) => {
|
||||||
.unwrap_or(HasReleaseArtifact::No)),
|
*self.0.retry_after.lock().unwrap() =
|
||||||
Ok(None) => Ok(HasReleaseArtifact::NoSuchRelease),
|
Some(Instant::now() + retry_after.unwrap_or(DEFAULT_RETRY_DURATION));
|
||||||
Err(Error::Unauthorized) => Ok(HasReleaseArtifact::Unauthorized),
|
|
||||||
Err(Error::RateLimit { retry_after }) => {
|
|
||||||
*self.0.retry_after.lock().unwrap() = Some(retry_after);
|
|
||||||
|
|
||||||
Ok(HasReleaseArtifact::RateLimit { retry_after })
|
Err(GhApiError::RateLimit { retry_after })
|
||||||
}
|
}
|
||||||
Err(Error::Error(err)) => Err(err),
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Clone, Debug)]
|
|
||||||
pub enum HasReleaseArtifact {
|
|
||||||
Yes {
|
|
||||||
/// get url for downloading the artifact using GitHub API (for private repository).
|
|
||||||
url: CompactString,
|
|
||||||
},
|
|
||||||
No,
|
|
||||||
NoSuchRelease,
|
|
||||||
/// GitHub returns 401 requiring a token.
|
|
||||||
/// In this case, it makes sense to fallback to HEAD/GET.
|
|
||||||
Unauthorized,
|
|
||||||
|
|
||||||
/// GitHub rate limit is applied per hour, so in case of reaching the rate
|
|
||||||
/// limit, [`GhApiClient`] will return this variant and let the user decide
|
|
||||||
/// what to do.
|
|
||||||
///
|
|
||||||
/// Usually it is more sensible to fallback to directly HEAD/GET the
|
|
||||||
/// artifact url than waiting until `retry_after`.
|
|
||||||
///
|
|
||||||
/// If you encounter this frequently, then you should consider getting an
|
|
||||||
/// authentication token (can be personal access or oath access token),
|
|
||||||
/// which should give you 5000 requests per hour per user.
|
|
||||||
///
|
|
||||||
/// Rate limit for unauthorized user is 60 requests per hour per originating
|
|
||||||
/// IP address, so it is very easy to be rate limited.
|
|
||||||
RateLimit {
|
|
||||||
retry_after: Instant,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -373,39 +322,31 @@ mod test {
|
||||||
eprintln!("In client {client:?}");
|
eprintln!("In client {client:?}");
|
||||||
|
|
||||||
for artifact_name in artifacts {
|
for artifact_name in artifacts {
|
||||||
let ret = client
|
let res = client
|
||||||
.has_release_artifact(GhReleaseArtifact {
|
.has_release_artifact(GhReleaseArtifact {
|
||||||
release: release.clone(),
|
release: release.clone(),
|
||||||
artifact_name: artifact_name.to_compact_string(),
|
artifact_name: artifact_name.to_compact_string(),
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(res, Ok(Some(_)) | Err(GhApiError::RateLimit { .. })),
|
||||||
ret,
|
|
||||||
HasReleaseArtifact::Yes { .. } | HasReleaseArtifact::RateLimit { .. }
|
|
||||||
),
|
|
||||||
"for '{artifact_name}': answer is {:#?}",
|
"for '{artifact_name}': answer is {:#?}",
|
||||||
ret
|
res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ret = client
|
let res = client
|
||||||
.has_release_artifact(GhReleaseArtifact {
|
.has_release_artifact(GhReleaseArtifact {
|
||||||
release: release.clone(),
|
release: release.clone(),
|
||||||
artifact_name: "123z".to_compact_string(),
|
artifact_name: "123z".to_compact_string(),
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(res, Ok(None) | Err(GhApiError::RateLimit { .. })),
|
||||||
ret,
|
"res = {:#?}",
|
||||||
HasReleaseArtifact::No | HasReleaseArtifact::RateLimit { .. }
|
res
|
||||||
),
|
|
||||||
"ret = {:#?}",
|
|
||||||
ret
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -430,21 +371,18 @@ mod test {
|
||||||
tag: "v0.18.2".to_compact_string(),
|
tag: "v0.18.2".to_compact_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let ret = client
|
let err = client
|
||||||
.has_release_artifact(GhReleaseArtifact {
|
.has_release_artifact(GhReleaseArtifact {
|
||||||
release,
|
release,
|
||||||
artifact_name: "1234".to_compact_string(),
|
artifact_name: "1234".to_compact_string(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(err, GhApiError::NotFound | GhApiError::RateLimit { .. }),
|
||||||
ret,
|
"err = {:#?}",
|
||||||
HasReleaseArtifact::NoSuchRelease | HasReleaseArtifact::RateLimit { .. }
|
err
|
||||||
),
|
|
||||||
"ret = {:#?}",
|
|
||||||
ret
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,14 +38,7 @@ pub(super) fn percent_decode_http_url_path(input: &str) -> CompactString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) enum GhApiRet<T> {
|
fn check_http_status_and_header(status: StatusCode, headers: &HeaderMap) -> Result<(), GhApiError> {
|
||||||
ReachedRateLimit { retry_after: Option<Duration> },
|
|
||||||
NotFound,
|
|
||||||
Success(T),
|
|
||||||
Unauthorized,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn check_for_status<T>(status: StatusCode, headers: &HeaderMap) -> Option<GhApiRet<T>> {
|
|
||||||
match status {
|
match status {
|
||||||
remote::StatusCode::FORBIDDEN
|
remote::StatusCode::FORBIDDEN
|
||||||
if headers
|
if headers
|
||||||
|
@ -53,7 +46,7 @@ pub(super) fn check_for_status<T>(status: StatusCode, headers: &HeaderMap) -> Op
|
||||||
.map(|val| val == "0")
|
.map(|val| val == "0")
|
||||||
.unwrap_or(false) =>
|
.unwrap_or(false) =>
|
||||||
{
|
{
|
||||||
Some(GhApiRet::ReachedRateLimit {
|
Err(GhApiError::RateLimit {
|
||||||
retry_after: headers.get("x-ratelimit-reset").and_then(|value| {
|
retry_after: headers.get("x-ratelimit-reset").and_then(|value| {
|
||||||
let secs = value.to_str().ok()?.parse().ok()?;
|
let secs = value.to_str().ok()?.parse().ok()?;
|
||||||
Some(Duration::from_secs(secs))
|
Some(Duration::from_secs(secs))
|
||||||
|
@ -61,13 +54,37 @@ pub(super) fn check_for_status<T>(status: StatusCode, headers: &HeaderMap) -> Op
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
remote::StatusCode::UNAUTHORIZED => Some(GhApiRet::Unauthorized),
|
remote::StatusCode::UNAUTHORIZED => Err(GhApiError::Unauthorized),
|
||||||
remote::StatusCode::NOT_FOUND => Some(GhApiRet::NotFound),
|
remote::StatusCode::NOT_FOUND => Err(GhApiError::NotFound),
|
||||||
|
|
||||||
_ => None,
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn issue_restful_api<T>(
|
||||||
|
client: &remote::Client,
|
||||||
|
path: String,
|
||||||
|
auth_token: Option<&str>,
|
||||||
|
) -> Result<T, GhApiError>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let mut request_builder = client
|
||||||
|
.get(Url::parse(&format!("https://api.github.com/{path}"))?)
|
||||||
|
.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?;
|
||||||
|
|
||||||
|
check_http_status_and_header(response.status(), response.headers())?;
|
||||||
|
|
||||||
|
Ok(response.json().await?)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
enum GraphQLResponse<T> {
|
enum GraphQLResponse<T> {
|
||||||
#[serde(rename = "data")]
|
#[serde(rename = "data")]
|
||||||
|
@ -90,16 +107,11 @@ fn get_graphql_endpoint() -> &'static Url {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) enum GraphQLResult<T, U> {
|
pub(super) async fn issue_graphql_query<T>(
|
||||||
Data(T),
|
|
||||||
Else(GhApiRet<U>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn issue_graphql_query<T, U>(
|
|
||||||
client: &remote::Client,
|
client: &remote::Client,
|
||||||
query: String,
|
query: String,
|
||||||
auth_token: &str,
|
auth_token: &str,
|
||||||
) -> Result<GraphQLResult<T, U>, GhApiError>
|
) -> Result<T, GhApiError>
|
||||||
where
|
where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
{
|
{
|
||||||
|
@ -115,19 +127,14 @@ where
|
||||||
.bearer_auth(&auth_token);
|
.bearer_auth(&auth_token);
|
||||||
|
|
||||||
let response = request_builder.send(false).await?;
|
let response = request_builder.send(false).await?;
|
||||||
|
check_http_status_and_header(response.status(), response.headers())?;
|
||||||
if let Some(ret) = check_for_status(response.status(), response.headers()) {
|
|
||||||
return Ok(GraphQLResult::Else(ret));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: GraphQLResponse<T> = response.json().await?;
|
let response: GraphQLResponse<T> = response.json().await?;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
GraphQLResponse::Data(data) => Ok(GraphQLResult::Data(data)),
|
GraphQLResponse::Data(data) => Ok(data),
|
||||||
GraphQLResponse::Errors(errors) if errors.is_rate_limited() => {
|
GraphQLResponse::Errors(errors) if errors.is_rate_limited() => {
|
||||||
Ok(GraphQLResult::Else(GhApiRet::ReachedRateLimit {
|
Err(GhApiError::RateLimit { retry_after: None })
|
||||||
retry_after: None,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
GraphQLResponse::Errors(errors) => Err(errors.into()),
|
GraphQLResponse::Errors(errors) => Err(errors.into()),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{error, fmt, io};
|
use std::{error, fmt, io, time::Duration};
|
||||||
|
|
||||||
use binstalk_downloader::remote;
|
use binstalk_downloader::remote;
|
||||||
use compact_str::{CompactString, ToCompactString};
|
use compact_str::{CompactString, ToCompactString};
|
||||||
|
@ -31,15 +31,30 @@ pub enum GhApiError {
|
||||||
|
|
||||||
#[error("Remote failed to process GraphQL query: {0}")]
|
#[error("Remote failed to process GraphQL query: {0}")]
|
||||||
GraphQLErrors(#[from] GhGraphQLErrors),
|
GraphQLErrors(#[from] GhGraphQLErrors),
|
||||||
|
|
||||||
|
#[error("Hit rate-limit, retry after {retry_after:?}")]
|
||||||
|
RateLimit { retry_after: Option<Duration> },
|
||||||
|
|
||||||
|
#[error("Corresponding resource is not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("Does not have permission to access the API")]
|
||||||
|
Unauthorized,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GhApiError {
|
impl GhApiError {
|
||||||
/// Attach context to [`GhApiError`]
|
/// Attach context to [`GhApiError`]
|
||||||
pub fn context(self, context: impl fmt::Display) -> Self {
|
pub fn context(self, context: impl fmt::Display) -> Self {
|
||||||
Self::Context(Box::new(GhApiContextError {
|
use GhApiError::*;
|
||||||
context: context.to_compact_string(),
|
|
||||||
err: self,
|
if matches!(self, RateLimit { .. } | NotFound | Unauthorized) {
|
||||||
}))
|
self
|
||||||
|
} else {
|
||||||
|
Self::Context(Box::new(GhApiContextError {
|
||||||
|
context: context.to_compact_string(),
|
||||||
|
err: self,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@ use std::{
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
};
|
};
|
||||||
|
|
||||||
use binstalk_downloader::remote::{self, header::HeaderMap, StatusCode, Url};
|
use binstalk_downloader::remote::{self};
|
||||||
use compact_str::CompactString;
|
use compact_str::CompactString;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
common::{self, issue_graphql_query, percent_encode_http_url_path, GraphQLResult},
|
common::{issue_graphql_query, issue_restful_api, percent_encode_http_url_path},
|
||||||
GhApiError, GhRelease,
|
GhApiError, GhRelease,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,38 +65,22 @@ impl Artifacts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) type FetchReleaseRet = common::GhApiRet<Artifacts>;
|
|
||||||
|
|
||||||
fn check_for_status(status: StatusCode, headers: &HeaderMap) -> Option<FetchReleaseRet> {
|
|
||||||
common::check_for_status(status, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_release_artifacts_restful_api(
|
async fn fetch_release_artifacts_restful_api(
|
||||||
client: &remote::Client,
|
client: &remote::Client,
|
||||||
GhRelease { owner, repo, tag }: &GhRelease,
|
GhRelease { owner, repo, tag }: &GhRelease,
|
||||||
auth_token: Option<&str>,
|
auth_token: Option<&str>,
|
||||||
) -> Result<FetchReleaseRet, GhApiError> {
|
) -> Result<Artifacts, GhApiError> {
|
||||||
let mut request_builder = client
|
issue_restful_api(
|
||||||
.get(Url::parse(&format!(
|
client,
|
||||||
"https://api.github.com/repos/{owner}/{repo}/releases/tags/{tag}",
|
format!(
|
||||||
|
"repos/{owner}/{repo}/releases/tags/{tag}",
|
||||||
owner = percent_encode_http_url_path(owner),
|
owner = percent_encode_http_url_path(owner),
|
||||||
repo = percent_encode_http_url_path(repo),
|
repo = percent_encode_http_url_path(repo),
|
||||||
tag = percent_encode_http_url_path(tag),
|
tag = percent_encode_http_url_path(tag),
|
||||||
))?)
|
),
|
||||||
.header("Accept", "application/vnd.github+json")
|
auth_token,
|
||||||
.header("X-GitHub-Api-Version", "2022-11-28");
|
)
|
||||||
|
.await
|
||||||
if let Some(auth_token) = auth_token {
|
|
||||||
request_builder = request_builder.bearer_auth(&auth_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = request_builder.send(false).await?;
|
|
||||||
|
|
||||||
if let Some(ret) = check_for_status(response.status(), response.headers()) {
|
|
||||||
Ok(ret)
|
|
||||||
} else {
|
|
||||||
Ok(FetchReleaseRet::Success(response.json().await?))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -149,7 +133,7 @@ async fn fetch_release_artifacts_graphql_api(
|
||||||
client: &remote::Client,
|
client: &remote::Client,
|
||||||
GhRelease { owner, repo, tag }: &GhRelease,
|
GhRelease { owner, repo, tag }: &GhRelease,
|
||||||
auth_token: &str,
|
auth_token: &str,
|
||||||
) -> Result<FetchReleaseRet, GhApiError> {
|
) -> Result<Artifacts, GhApiError> {
|
||||||
let mut artifacts = Artifacts::default();
|
let mut artifacts = Artifacts::default();
|
||||||
let mut cond = FilterCondition::Init;
|
let mut cond = FilterCondition::Init;
|
||||||
|
|
||||||
|
@ -171,10 +155,7 @@ query {{
|
||||||
}}"#
|
}}"#
|
||||||
);
|
);
|
||||||
|
|
||||||
let data: GraphQLData = match issue_graphql_query(client, query, auth_token).await? {
|
let data: GraphQLData = issue_graphql_query(client, query, auth_token).await?;
|
||||||
GraphQLResult::Data(data) => data,
|
|
||||||
GraphQLResult::Else(ret) => return Ok(ret),
|
|
||||||
};
|
|
||||||
|
|
||||||
let assets = data
|
let assets = data
|
||||||
.repository
|
.repository
|
||||||
|
@ -191,10 +172,10 @@ query {{
|
||||||
} => {
|
} => {
|
||||||
cond = FilterCondition::After(end_cursor);
|
cond = FilterCondition::After(end_cursor);
|
||||||
}
|
}
|
||||||
_ => break Ok(FetchReleaseRet::Success(artifacts)),
|
_ => break Ok(artifacts),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break Ok(FetchReleaseRet::NotFound);
|
break Err(GhApiError::NotFound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,7 +184,7 @@ pub(super) async fn fetch_release_artifacts(
|
||||||
client: &remote::Client,
|
client: &remote::Client,
|
||||||
release: &GhRelease,
|
release: &GhRelease,
|
||||||
auth_token: Option<&str>,
|
auth_token: Option<&str>,
|
||||||
) -> Result<FetchReleaseRet, GhApiError> {
|
) -> Result<Artifacts, GhApiError> {
|
||||||
if let Some(auth_token) = auth_token {
|
if let Some(auth_token) = auth_token {
|
||||||
let res = fetch_release_artifacts_graphql_api(client, release, auth_token)
|
let res = fetch_release_artifacts_graphql_api(client, release, auth_token)
|
||||||
.await
|
.await
|
||||||
|
@ -211,7 +192,7 @@ pub(super) async fn fetch_release_artifacts(
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
// Fallback to Restful API
|
// Fallback to Restful API
|
||||||
Ok(FetchReleaseRet::Unauthorized) => (),
|
Err(GhApiError::Unauthorized) => (),
|
||||||
res => return res,
|
res => return res,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use binstalk_downloader::remote::{header::HeaderMap, StatusCode, Url};
|
|
||||||
use compact_str::CompactString;
|
use compact_str::CompactString;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
common::{self, issue_graphql_query, percent_encode_http_url_path, GraphQLResult},
|
common::{issue_graphql_query, issue_restful_api, percent_encode_http_url_path},
|
||||||
remote, GhApiError, GhRepo,
|
remote, GhApiError, GhRepo,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,37 +31,21 @@ impl RepoInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) type FetchRepoInfoRet = common::GhApiRet<RepoInfo>;
|
|
||||||
|
|
||||||
fn check_for_status(status: StatusCode, headers: &HeaderMap) -> Option<FetchRepoInfoRet> {
|
|
||||||
common::check_for_status(status, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_repo_info_restful_api(
|
async fn fetch_repo_info_restful_api(
|
||||||
client: &remote::Client,
|
client: &remote::Client,
|
||||||
GhRepo { owner, repo }: &GhRepo,
|
GhRepo { owner, repo }: &GhRepo,
|
||||||
auth_token: Option<&str>,
|
auth_token: Option<&str>,
|
||||||
) -> Result<FetchRepoInfoRet, GhApiError> {
|
) -> Result<RepoInfo, GhApiError> {
|
||||||
let mut request_builder = client
|
issue_restful_api(
|
||||||
.get(Url::parse(&format!(
|
client,
|
||||||
"https://api.github.com/repos/{owner}/{repo}",
|
format!(
|
||||||
|
"repos/{owner}/{repo}",
|
||||||
owner = percent_encode_http_url_path(owner),
|
owner = percent_encode_http_url_path(owner),
|
||||||
repo = percent_encode_http_url_path(repo),
|
repo = percent_encode_http_url_path(repo),
|
||||||
))?)
|
),
|
||||||
.header("Accept", "application/vnd.github+json")
|
auth_token,
|
||||||
.header("X-GitHub-Api-Version", "2022-11-28");
|
)
|
||||||
|
.await
|
||||||
if let Some(auth_token) = auth_token {
|
|
||||||
request_builder = request_builder.bearer_auth(&auth_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = request_builder.send(false).await?;
|
|
||||||
|
|
||||||
if let Some(ret) = check_for_status(response.status(), response.headers()) {
|
|
||||||
Ok(ret)
|
|
||||||
} else {
|
|
||||||
Ok(FetchRepoInfoRet::Success(response.json().await?))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -74,7 +57,7 @@ async fn fetch_repo_info_graphql_api(
|
||||||
client: &remote::Client,
|
client: &remote::Client,
|
||||||
GhRepo { owner, repo }: &GhRepo,
|
GhRepo { owner, repo }: &GhRepo,
|
||||||
auth_token: &str,
|
auth_token: &str,
|
||||||
) -> Result<FetchRepoInfoRet, GhApiError> {
|
) -> Result<RepoInfo, GhApiError> {
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"
|
r#"
|
||||||
query {{
|
query {{
|
||||||
|
@ -88,17 +71,14 @@ query {{
|
||||||
}}"#
|
}}"#
|
||||||
);
|
);
|
||||||
|
|
||||||
match issue_graphql_query(client, query, auth_token).await? {
|
issue_graphql_query(client, query, auth_token).await
|
||||||
GraphQLResult::Data(repo_info) => Ok(common::GhApiRet::Success(repo_info)),
|
|
||||||
GraphQLResult::Else(ret) => Ok(ret),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn fetch_repo_info(
|
pub(super) async fn fetch_repo_info(
|
||||||
client: &remote::Client,
|
client: &remote::Client,
|
||||||
repo: &GhRepo,
|
repo: &GhRepo,
|
||||||
auth_token: Option<&str>,
|
auth_token: Option<&str>,
|
||||||
) -> Result<FetchRepoInfoRet, GhApiError> {
|
) -> Result<RepoInfo, GhApiError> {
|
||||||
if let Some(auth_token) = auth_token {
|
if let Some(auth_token) = auth_token {
|
||||||
let res = fetch_repo_info_graphql_api(client, repo, auth_token)
|
let res = fetch_repo_info_graphql_api(client, repo, auth_token)
|
||||||
.await
|
.await
|
||||||
|
@ -106,7 +86,7 @@ pub(super) async fn fetch_repo_info(
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
// Fallback to Restful API
|
// Fallback to Restful API
|
||||||
Ok(FetchRepoInfoRet::Unauthorized) => (),
|
Err(GhApiError::Unauthorized) => (),
|
||||||
res => return res,
|
res => return res,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
pub mod jobserver_client;
|
pub mod jobserver_client;
|
||||||
pub mod remote;
|
pub mod remote {
|
||||||
|
pub use binstalk_downloader::remote::*;
|
||||||
|
pub use url::ParseError as UrlParseError;
|
||||||
|
}
|
||||||
pub(crate) mod target_triple;
|
pub(crate) mod target_triple;
|
||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
pub use binstalk_downloader::remote::*;
|
|
||||||
pub use url::ParseError as UrlParseError;
|
|
||||||
|
|
||||||
use std::sync::{
|
|
||||||
atomic::{AtomicBool, Ordering::Relaxed},
|
|
||||||
Once,
|
|
||||||
};
|
|
||||||
use tracing::{debug, warn};
|
|
||||||
|
|
||||||
use super::gh_api_client::{GhApiClient, GhReleaseArtifact, HasReleaseArtifact};
|
|
||||||
use crate::errors::BinstallError;
|
|
||||||
|
|
||||||
/// This function returns a future where its size should be at most size of
|
|
||||||
/// 2 pointers.
|
|
||||||
pub async fn does_url_exist(
|
|
||||||
client: Client,
|
|
||||||
gh_api_client: GhApiClient,
|
|
||||||
url: &Url,
|
|
||||||
) -> Result<bool, BinstallError> {
|
|
||||||
static GH_API_CLIENT_FAILED: AtomicBool = AtomicBool::new(false);
|
|
||||||
static WARN_RATE_LIMIT_ONCE: Once = Once::new();
|
|
||||||
static WARN_UNAUTHORIZED_ONCE: Once = Once::new();
|
|
||||||
|
|
||||||
debug!("Checking for package at: '{url}'");
|
|
||||||
|
|
||||||
if !GH_API_CLIENT_FAILED.load(Relaxed) {
|
|
||||||
if let Some(artifact) = GhReleaseArtifact::try_extract_from_url(url) {
|
|
||||||
debug!("Using GitHub API to check for existence of artifact, which will also cache the API response");
|
|
||||||
|
|
||||||
// The future returned has the same size as a pointer
|
|
||||||
match gh_api_client.has_release_artifact(artifact).await? {
|
|
||||||
HasReleaseArtifact::Yes { .. } => return Ok(true),
|
|
||||||
HasReleaseArtifact::No | HasReleaseArtifact::NoSuchRelease => return Ok(false),
|
|
||||||
|
|
||||||
HasReleaseArtifact::RateLimit { retry_after } => {
|
|
||||||
WARN_RATE_LIMIT_ONCE.call_once(|| {
|
|
||||||
warn!("Your GitHub API token (if any) has reached its rate limit and cannot be used again until {retry_after:?}, so we will fallback to HEAD/GET on the url.");
|
|
||||||
warn!("If you did not supply a github token, consider doing so: GitHub limits unauthorized users to 60 requests per hour per origin IP address.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
HasReleaseArtifact::Unauthorized => {
|
|
||||||
WARN_UNAUTHORIZED_ONCE.call_once(|| {
|
|
||||||
warn!("GitHub API somehow requires a token for the API access, so we will fallback to HEAD/GET on the url.");
|
|
||||||
warn!("Please consider supplying a token to cargo-binstall to speedup resolution.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GH_API_CLIENT_FAILED.store(true, Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Box::pin(client.remote_gettable(url.clone())).await?)
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue