1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
//! While gird is primarily developed as a command-line tool, it can
//! also be used as a library by other programs. Using this library,
//! it is simple to retrieve release artifacts from supported sources.
use ureq::Response;
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Regex error: {0}")]
Regex(#[from] regex::Error),
#[error("I/O error: {0}")]
IO(#[from] std::io::Error),
#[error("ureq error: {0}")]
UReq(#[from] ureq::Error),
#[error("No such release found")]
ReleaseNotFound,
#[error("No matching artifacts found")]
NoMatches,
#[error("Artifact download URL not found")]
UrlNotFound,
#[error("{0}")]
Misc(String),
}
pub mod github {
//! GitHub-specific functions and utilities.
/// GitHub-specific downloader for the latest release from
/// repository user/repo for the given platform.
///
/// The exclude iterator, if non-empty, provides a list of
/// substrings which will invalidate a particular artifact from
/// being an acceptable match for the download. For example, if
/// exclude is set to `vec!["musl"].iter()` then a release
/// artifact such as **foobar-v12.6.1-linux-amd64-musl.tar.gz**
/// would not be downloaded, even if the platform was set to
/// `"linux-amd64"`
///
/// `std::iter::empty::<&str>()` can be used as a value for the
/// exclude parameter if you do not wish to exclude any file name
/// patterns.
pub fn download_release<'e, E, S>(
artifact: impl AsRef<str>,
exclude: E,
user: impl AsRef<str>,
repo: impl AsRef<str>,
) -> Result<super::Response, super::Error>
where
E: IntoIterator<Item = S>,
S: AsRef<str>,
{
let user = user.as_ref();
let repo = repo.as_ref();
let artifact = artifact.as_ref();
let exclude: Vec<_> = exclude
.into_iter()
.map(|x| x.as_ref().to_string())
.collect();
let agent = ureq::AgentBuilder::new().redirects(0).build();
let artifact_pattern =
regex::RegexBuilder::new(r#" href="(.*?/releases/download/.*?)""#).build()?;
let Some(tag) = agent
.get(format!("https://github.com/{}/{}/releases/latest", user, repo).as_str())
.call()?
.header("Location")
.and_then(|loc| loc.rsplit_once("/tag/").map(|parts| parts.1.to_string()))
else {
return Err(super::Error::ReleaseNotFound);
};
let artifacts_page = agent
.get(
format!(
"https://github.com/{}/{}/releases/expanded_assets/{}",
user, repo, &tag
)
.as_str(),
)
.call()?
.into_string()?;
let artifact_urls: Vec<_> = artifact_pattern
.captures_iter(artifacts_page.as_str())
.flat_map(|c| {
c.get(1).and_then(|m| {
let m = m.as_str();
if m.contains(artifact) {
if exclude.iter().any(|e| m.contains(e.as_str())) {
None
} else {
Some(String::from(m))
}
} else {
None
}
})
})
.collect();
if let Some(path) = artifact_urls.into_iter().next() {
let Some(redir) = agent
.get(format!("https://github.com{}", &path).as_str())
.call()?
.header("Location")
.map(|loc| loc.to_string())
else {
return Err(super::Error::UrlNotFound);
};
Ok(agent.get(&redir).call()?)
} else {
Err(super::Error::NoMatches)
}
}
}