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