rootle_github/client/
blobs.rs1use super::transport::{classify_send, classify_status};
4use super::{API, GitHubClient, urlencoding};
5use rootle_provider::{ProviderError, ProviderResult};
6
7impl GitHubClient {
8 pub fn blob_at(
11 &self,
12 owner: &str,
13 repo: &str,
14 path: &str,
15 ref_: Option<&str>,
16 ) -> ProviderResult<(Vec<u8>, String)> {
17 #[derive(serde::Deserialize)]
18 struct Contents {
19 content: String, sha: String,
21 }
22 let mut url = format!("{API}/repos/{owner}/{repo}/contents/{path}");
23 if let Some(r) = ref_ {
24 url.push_str(&format!("?ref={}", urlencoding(r)));
25 }
26 let item: Contents = self.get(&url)?;
27 use base64::Engine;
28 let clean: String = item
29 .content
30 .chars()
31 .filter(|c| !c.is_whitespace())
32 .collect();
33 let bytes = base64::engine::general_purpose::STANDARD
34 .decode(clean)
35 .map_err(|e| ProviderError::other(format!("contents base64: {e}")))?;
36 Ok((bytes, item.sha))
37 }
38
39 pub fn source_tarball(&self, repo: &str) -> ProviderResult<Vec<u8>> {
43 const CAP: u64 = 64 * 1024 * 1024;
44 let url = format!("{API}/repos/{repo}/tarball");
45 let trace = super::transport::HttpTrace::begin(&url, self.auth_source, false);
46 let mut req = self.http.get(&url);
47 if let Some(token) = &self.token {
48 req = req.bearer_auth(token);
49 }
50 let mut resp = match req.send() {
51 Ok(resp) => resp,
52 Err(error) => {
53 if let Some(trace) = &trace {
54 trace.fail(if error.is_timeout() {
55 "timeout"
56 } else {
57 "network"
58 });
59 }
60 return Err(classify_send(error));
61 }
62 };
63 if !resp.status().is_success() {
64 if let Some(trace) = &trace {
65 trace.finish(resp.status().as_u16(), 0);
66 }
67 return Err(classify_status(resp));
68 }
69 if let Some(len) = resp.content_length()
70 && len > CAP
71 {
72 if let Some(trace) = &trace {
73 trace.finish(resp.status().as_u16(), 0);
74 trace.rejected("content_length_over_cap");
75 }
76 return Err(ProviderError::other(format!(
77 "tarball too large for local grep ({len} bytes)"
78 )));
79 }
80 let status = resp.status().as_u16();
83 let mut bytes = Vec::new();
84 let mut capped = std::io::Read::take(&mut resp, CAP);
85 std::io::Read::read_to_end(&mut capped, &mut bytes).map_err(|error| {
86 if let Some(trace) = &trace {
87 trace.fail("body_read");
88 }
89 ProviderError::other(error.to_string())
90 })?;
91 if let Some(trace) = &trace {
92 trace.finish(status, bytes.len() as u64);
93 }
94 Ok(bytes)
95 }
96
97 pub fn fetch_blob(&self, owner: &str, repo: &str, sha: &str) -> ProviderResult<Vec<u8>> {
100 if let Some(bytes) = crate::cache::read_blob(sha) {
101 return Ok(bytes);
102 }
103 #[derive(serde::Deserialize)]
104 struct BlobResponse {
105 content: String,
106 size: u64,
107 }
108 let url = format!("{API}/repos/{owner}/{repo}/git/blobs/{sha}");
109 let blob: BlobResponse = self.get(&url)?;
110 if blob.size > 1024 * 1024 {
111 return Err(ProviderError::other(format!(
112 "file too large to preview ({} bytes)",
113 blob.size
114 )));
115 }
116 use base64::Engine;
117 let clean: String = blob
118 .content
119 .chars()
120 .filter(|c| !c.is_whitespace())
121 .collect();
122 let bytes = base64::engine::general_purpose::STANDARD
123 .decode(clean)
124 .map_err(|e| ProviderError::other(e.to_string()))?;
125 let _ = crate::cache::write_blob(sha, &bytes);
126 Ok(bytes)
127 }
128}