mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-06-17 08:06:38 +00:00
216 lines
5.4 KiB
Rust
216 lines
5.4 KiB
Rust
use std::{
|
|
borrow::Borrow,
|
|
collections::HashSet,
|
|
fmt,
|
|
future::Future,
|
|
hash::{Hash, Hasher},
|
|
};
|
|
|
|
use binstalk_downloader::remote::{self};
|
|
use compact_str::{CompactString, ToCompactString};
|
|
use serde::Deserialize;
|
|
|
|
use super::{
|
|
common::{issue_graphql_query, issue_restful_api, percent_encode_http_url_path},
|
|
GhApiError, GhRelease, GhRepo,
|
|
};
|
|
|
|
// Only include fields we do care about
|
|
|
|
#[derive(Eq, Deserialize, Debug)]
|
|
struct Artifact {
|
|
name: CompactString,
|
|
url: 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, Default, Deserialize)]
|
|
pub(super) struct Artifacts {
|
|
assets: HashSet<Artifact>,
|
|
}
|
|
|
|
impl Artifacts {
|
|
/// get url for downloading the artifact using GitHub API (for private repository).
|
|
pub(super) fn get_artifact_url(&self, artifact_name: &str) -> Option<CompactString> {
|
|
self.assets
|
|
.get(artifact_name)
|
|
.map(|artifact| artifact.url.clone())
|
|
}
|
|
}
|
|
|
|
fn fetch_release_artifacts_restful_api(
|
|
client: &remote::Client,
|
|
GhRelease {
|
|
repo: GhRepo { owner, repo },
|
|
tag,
|
|
}: &GhRelease,
|
|
auth_token: Option<&str>,
|
|
) -> impl Future<Output = Result<Artifacts, GhApiError>> + Send + Sync + 'static {
|
|
issue_restful_api(
|
|
client,
|
|
&[
|
|
"repos", owner, repo, "releases", "tags",
|
|
tag, //&percent_encode_http_url_path(tag).to_compact_string(),
|
|
],
|
|
auth_token,
|
|
)
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GraphQLData {
|
|
repository: Option<GraphQLRepo>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GraphQLRepo {
|
|
release: Option<GraphQLRelease>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GraphQLRelease {
|
|
#[serde(rename = "releaseAssets")]
|
|
assets: GraphQLReleaseAssets,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GraphQLReleaseAssets {
|
|
nodes: Vec<Artifact>,
|
|
#[serde(rename = "pageInfo")]
|
|
page_info: GraphQLPageInfo,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GraphQLPageInfo {
|
|
#[serde(rename = "endCursor")]
|
|
end_cursor: Option<CompactString>,
|
|
#[serde(rename = "hasNextPage")]
|
|
has_next_page: bool,
|
|
}
|
|
|
|
enum FilterCondition {
|
|
Init,
|
|
After(CompactString),
|
|
}
|
|
|
|
impl fmt::Display for FilterCondition {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
// GitHub imposes a limit of 100 for the value passed to param "first"
|
|
FilterCondition::Init => f.write_str("first:100"),
|
|
FilterCondition::After(end_cursor) => write!(f, r#"first:100,after:"{end_cursor}""#),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fetch_release_artifacts_graphql_api(
|
|
client: &remote::Client,
|
|
GhRelease {
|
|
repo: GhRepo { owner, repo },
|
|
tag,
|
|
}: &GhRelease,
|
|
auth_token: &str,
|
|
) -> impl Future<Output = Result<Artifacts, GhApiError>> + Send + Sync + 'static {
|
|
let client = client.clone();
|
|
let auth_token = auth_token.to_compact_string();
|
|
|
|
let base_query_prefix = format!(
|
|
r#"
|
|
query {{
|
|
repository(owner:"{owner}",name:"{repo}") {{
|
|
release(tagName:"{tag}") {{"#
|
|
);
|
|
|
|
let base_query_suffix = r#"
|
|
nodes { name url }
|
|
pageInfo { endCursor hasNextPage }
|
|
}}}}"#
|
|
.trim();
|
|
|
|
async move {
|
|
let mut artifacts = Artifacts::default();
|
|
let mut cond = FilterCondition::Init;
|
|
let base_query_prefix = base_query_prefix.trim();
|
|
|
|
loop {
|
|
let query = format!(
|
|
r#"
|
|
{base_query_prefix}
|
|
releaseAssets({cond}) {{
|
|
{base_query_suffix}"#
|
|
);
|
|
|
|
let data: GraphQLData = issue_graphql_query(&client, query, &auth_token).await?;
|
|
|
|
let assets = data
|
|
.repository
|
|
.and_then(|repository| repository.release)
|
|
.map(|release| release.assets);
|
|
|
|
if let Some(assets) = assets {
|
|
artifacts.assets.extend(assets.nodes);
|
|
|
|
match assets.page_info {
|
|
GraphQLPageInfo {
|
|
end_cursor: Some(end_cursor),
|
|
has_next_page: true,
|
|
} => {
|
|
cond = FilterCondition::After(end_cursor);
|
|
}
|
|
_ => break Ok(artifacts),
|
|
}
|
|
} else {
|
|
break Err(GhApiError::NotFound);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) async fn fetch_release_artifacts(
|
|
client: &remote::Client,
|
|
release: &GhRelease,
|
|
auth_token: Option<&str>,
|
|
) -> Result<Artifacts, GhApiError> {
|
|
if let Some(auth_token) = auth_token {
|
|
let res = fetch_release_artifacts_graphql_api(client, release, auth_token)
|
|
.await
|
|
.map_err(|err| err.context("GraphQL API"));
|
|
|
|
match res {
|
|
// Fallback to Restful API
|
|
Err(GhApiError::Unauthorized) => (),
|
|
res => return res,
|
|
}
|
|
}
|
|
|
|
fetch_release_artifacts_restful_api(client, release, auth_token)
|
|
.await
|
|
.map_err(|err| err.context("Restful API"))
|
|
}
|