Skip to content

Commit fc4daae

Browse files
Mark Gentrymarkg-github
authored andcommitted
fix: Allow asset download from private repos
Use --public to download in a way (the old way) that only works for public repos. Resolves: #18
1 parent 4bf12d6 commit fc4daae

File tree

3 files changed

+119
-5
lines changed

3 files changed

+119
-5
lines changed

src/github.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ impl<K, U> Knowable<K, U> {
5555

5656
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
5757
pub struct Asset<T = Type> {
58+
/// (Asset) ID needed to download assets from private repositories
59+
pub id: u64,
5860
pub name: String,
5961
pub size: u64,
6062

@@ -68,6 +70,7 @@ pub struct Asset<T = Type> {
6870
impl Asset<Knowable<Type, String>> {
6971
fn known(self) -> Option<Asset> {
7072
self.mime.known().map(|mime| Asset {
73+
id: self.id,
7174
name: self.name,
7275
size: self.size,
7376
url: self.url,
@@ -116,6 +119,11 @@ pub struct GitHubArgs {
116119
#[arg(short = 't', long)]
117120
pub tag: String,
118121

122+
/// Download Release Assets w/o supplying token, which doesn't work for
123+
/// private repos.
124+
#[arg(long)]
125+
pub public: bool,
126+
119127
/// Filter asset names
120128
#[arg(trailing_var_arg = true)]
121129
pub filter: Vec<String>,
@@ -243,13 +251,34 @@ impl GitHub {
243251
})
244252
}
245253

254+
/// Get the GitHub token if available
255+
pub fn token(&self) -> Option<&str> {
256+
self.args.token.as_deref()
257+
}
258+
259+
/// Returns true if the repository is not public (i.e., `public` flag is false).
260+
pub const fn is_private(&self) -> bool {
261+
!self.args.public
262+
}
263+
264+
/// Get the owner name
265+
pub fn owner(&self) -> &str {
266+
&self.args.owner
267+
}
268+
269+
/// Get the repo name
270+
pub fn repo(&self) -> &str {
271+
&self.args.repo
272+
}
273+
246274
pub async fn assets(&self) -> Result<BTreeSet<Asset>> {
247275
let url = format!(
248276
"https://api.github.com/repos/{}/{}/releases/tags/{}",
249277
self.args.owner, self.args.repo, self.args.tag
250278
);
251279

252280
let response = self.client.get(&url).send().await?;
281+
253282
let release: Release = response.json().await?;
254283

255284
let assets = release

src/http/server.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,31 @@ impl Server {
4949
attempt.stop()
5050
});
5151

52+
let mut client_builder = Client::builder().redirect(policy);
53+
if github.is_private() {
54+
// Build client with GitHub authentication if token is available
55+
if let Some(token) = github.token() {
56+
let mut headers = reqwest::header::HeaderMap::new();
57+
headers.insert("Authorization", format!("token {token}").parse().unwrap());
58+
headers.insert(
59+
"User-Agent",
60+
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))
61+
.parse()
62+
.unwrap(),
63+
);
64+
headers.insert(
65+
"Accept",
66+
"Accept: application/octet-stream".parse().unwrap(),
67+
);
68+
client_builder = client_builder.default_headers(headers);
69+
}
70+
}
71+
5272
Ok(Self {
5373
listener,
5474
status,
5575
github,
56-
client: Client::builder().redirect(policy).build()?,
76+
client: client_builder.build()?,
5777
path,
5878
})
5979
}

src/http/service.rs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,46 @@ impl Service {
4545
path,
4646
}
4747
}
48+
49+
/// Build a reqwest request for an asset, handling private/public repos and method.
50+
fn build_request(
51+
gh: &GitHub,
52+
asset: &Asset,
53+
client: &Client,
54+
method: &Method,
55+
) -> reqwest::RequestBuilder {
56+
let url = if gh.is_private() {
57+
format!(
58+
"https://api.github.com/repos/{}/{}/releases/assets/{}",
59+
gh.owner(),
60+
gh.repo(),
61+
asset.id
62+
)
63+
} else {
64+
asset.url.clone()
65+
};
66+
67+
let rb = match method {
68+
&Method::HEAD => client.head(url),
69+
_ => client.get(url), // fallback; you only use HEAD/GET here
70+
};
71+
72+
if gh.is_private() {
73+
// Required by GitHub asset API for private repos
74+
rb.header("Accept", "application/octet-stream")
75+
} else {
76+
rb
77+
}
78+
}
79+
80+
fn inspect_response(response: &reqwest::Response) -> (reqwest::StatusCode, Option<&str>) {
81+
let status_code = response.status();
82+
let content_length = response
83+
.headers()
84+
.get("content-length")
85+
.and_then(|v| v.to_str().ok());
86+
(status_code, content_length)
87+
}
4888
}
4989

5090
impl hyper::service::Service<Request<Incoming>> for Service {
@@ -110,7 +150,21 @@ impl hyper::service::Service<Request<Incoming>> for Service {
110150
None => return Ok(POWEROFF_EFI.reply(None, Type::Efi, EMPTY)),
111151

112152
// Send the request (possibly redirecting...)
113-
Some(asset) => (client.head(asset.url).send().await?, asset.mime),
153+
Some(asset) => {
154+
let request =
155+
Self::build_request(&github, &asset, &client, &Method::HEAD);
156+
157+
match request.send().await {
158+
Ok(resp) => {
159+
let (_status_code, _content_length) =
160+
Self::inspect_response(&resp);
161+
(resp, asset.mime)
162+
}
163+
Err(_e) => {
164+
return Ok(EMPTY.reply(Code::BAD_GATEWAY, None, None));
165+
}
166+
}
167+
}
114168
}
115169
}
116170

@@ -122,9 +176,20 @@ impl hyper::service::Service<Request<Incoming>> for Service {
122176

123177
// Send the request (possibly redirecting...)
124178
Some(asset) => {
125-
let response = client.get(asset.url).send().await?;
126-
status.lock().await.update().downloading(remote);
127-
(response, asset.mime)
179+
let request =
180+
Self::build_request(&github, &asset, &client, &Method::GET);
181+
182+
match request.send().await {
183+
Ok(resp) => {
184+
let (_status_code, _content_length) =
185+
Self::inspect_response(&resp);
186+
status.lock().await.update().downloading(remote);
187+
(resp, asset.mime)
188+
}
189+
Err(_e) => {
190+
return Ok(EMPTY.reply(Code::BAD_GATEWAY, None, None));
191+
}
192+
}
128193
}
129194
}
130195
}

0 commit comments

Comments
 (0)