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)
        }
    }
}