Skip to main content

layouts_rs/
github.rs

1use reqwest::blocking::{Client, Response};
2use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION, USER_AGENT};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct TreeObject {
8    path: String,
9    mode: String,
10    #[serde(rename = "type")]
11    type_: String,
12    size: Option<usize>,
13    sha: String,
14    url: String,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TreeResponse {
19    sha: String,
20    url: String,
21    tree: Vec<TreeObject>,
22}
23
24impl TreeResponse {
25    pub fn files(&self) -> Vec<String> {
26        self.tree
27            .iter()
28            .filter(|x| x.type_ == "blob")
29            .map(|x| x.path.clone())
30            .collect()
31    }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct GithubFile {
36    #[serde(rename = "type")]
37    type_: String,
38    encoding: String,
39    size: usize,
40    name: String,
41    path: String,
42    content: String,
43    sha: String,
44    url: String,
45    git_url: String,
46    html_url: String,
47    download_url: String,
48    _links: HashMap<String, String>,
49}
50
51#[derive(Debug, Clone)]
52pub struct GithubClient {
53    client: Client,
54    api_url: String,
55    repo: String,
56    has_token: bool,
57}
58
59impl GithubClient {
60    pub fn new(repo: String, api_token: Option<String>) -> Self {
61        let mut headers = HeaderMap::new();
62        headers.insert(USER_AGENT, "hid-io".parse().unwrap());
63        headers.insert(ACCEPT, "application/vnd.github.v3+json".parse().unwrap());
64        if let Some(api_token) = &api_token {
65            headers.insert(
66                AUTHORIZATION,
67                format!("token {}", api_token).parse().unwrap(),
68            );
69        }
70
71        let client = reqwest::blocking::Client::builder()
72            .default_headers(headers)
73            .build()
74            .unwrap();
75
76        GithubClient {
77            client,
78            repo,
79            api_url: "https://api.github.com".to_string(),
80            has_token: api_token.is_some(),
81        }
82    }
83
84    // If this fails there is no use in trying to parse the response
85    fn _check_response(&self, response: &Response) {
86        let headers = response.headers();
87        if headers.get("X-RateLimit-Remaining").unwrap() == "0" {
88            println!(
89                "Rate limit resets at unix time: {}",
90                headers.get("X-RateLimit-Reset").unwrap().to_str().unwrap()
91            );
92            if !self.has_token {
93                println!("NOTE: Setting GITHUB_API_TOKEN will help avoid this");
94            }
95            panic!("RATE LIMIT EXCEEDED");
96        }
97    }
98
99    pub fn get_file_info(
100        &self,
101        path: &str,
102        reftag: &str,
103    ) -> Result<GithubFile, Box<dyn std::error::Error>> {
104        let resp = self
105            .client
106            .get(&format!(
107                "{}/repos/{}/contents/{}",
108                self.api_url, self.repo, path
109            ))
110            .query(&[("ref", reftag)])
111            .send()?;
112        self._check_response(&resp);
113        let file = resp.json::<GithubFile>()?;
114        Ok(file)
115    }
116
117    pub fn get_file_raw(
118        &self,
119        path: &str,
120        reftag: &str,
121    ) -> Result<String, Box<dyn std::error::Error>> {
122        let resp = self
123            .client
124            .get(&format!(
125                "{}/repos/{}/contents/{}",
126                self.api_url, self.repo, path
127            ))
128            .header(ACCEPT, "application/vnd.github.VERSION.raw+json")
129            .query(&[("ref", reftag)])
130            .send()?;
131        self._check_response(&resp);
132        Ok(resp.text()?)
133    }
134
135    pub fn list_files(&self, reftag: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
136        let resp = self
137            .client
138            .get(&format!(
139                "{}/repos/{}/git/trees/{}",
140                self.api_url, self.repo, reftag
141            ))
142            .query(&[("recursive", "1")])
143            .send()?;
144        self._check_response(&resp);
145        let tree = resp.json::<TreeResponse>()?;
146        Ok(tree.files())
147    }
148}