Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ampup/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pub async fn run(
None,
None,
None,
None,
)
.await?;
} else {
Expand Down
10 changes: 8 additions & 2 deletions ampup/src/commands/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
github::GitHubClient,
install::Installer,
platform::{Architecture, Platform},
ui,
token, ui,
version_manager::VersionManager,
};

Expand All @@ -16,9 +16,15 @@ pub async fn run(
version: Option<String>,
arch_override: Option<String>,
platform_override: Option<String>,
jobs: Option<usize>,
) -> Result<()> {
let config = Config::new(install_dir)?;
let github = GitHubClient::new(repo, github_token)?;
let _max_concurrent = jobs.unwrap_or(4);

// Resolve token with fallback chain: explicit → gh auth token → unauthenticated
let resolved_token = token::resolve_github_token(github_token);

let github = GitHubClient::new(repo, resolved_token)?;
let version_manager = VersionManager::new(config);

// Determine version to install
Expand Down
102 changes: 86 additions & 16 deletions ampup/src/github.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use std::sync::Arc;

use anyhow::{Context, Result};
use futures::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use serde::Deserialize;

use crate::rate_limiter::GitHubRateLimiter;

const AMPUP_API_URL: &str = "https://ampup.sh/api";
const GITHUB_API_URL: &str = "https://api.github.com";

Expand Down Expand Up @@ -37,6 +41,10 @@ pub enum GitHubError {
url: String,
body: String,
},
RateLimited {
retry_after_secs: u64,
has_token: bool,
},
}

impl std::fmt::Display for GitHubError {
Expand Down Expand Up @@ -144,6 +152,18 @@ impl std::fmt::Display for GitHubError {
writeln!(f, " Response: {}", body)?;
}
}
Self::RateLimited {
retry_after_secs,
has_token,
} => {
writeln!(f, "GitHub API rate limit exceeded")?;
writeln!(f, " Retry after: {} seconds", retry_after_secs)?;
writeln!(f)?;
if !*has_token {
writeln!(f, " Unauthenticated requests have lower rate limits.")?;
writeln!(f, " Try: export GITHUB_TOKEN=$(gh auth token)")?;
}
}
}
Ok(())
}
Expand All @@ -166,12 +186,16 @@ struct Asset {
url: String,
}

/// Clone is cheap: `reqwest::Client` and `rate_limiter` are both `Arc`-backed.
/// Needed so `DownloadManager` can move a handle into each spawned download task.
#[derive(Clone)]
pub struct GitHubClient {
client: reqwest::Client,
repo: String,
token: Option<String>,
/// Base URL for API requests (either custom API or GitHub API)
api: String,
rate_limiter: Arc<GitHubRateLimiter>,
}

impl GitHubClient {
Expand Down Expand Up @@ -203,11 +227,14 @@ impl GitHubClient {
format!("{}/repos/{}/releases", GITHUB_API_URL, repo)
};

let rate_limiter = Arc::new(GitHubRateLimiter::new(github_token.is_some()));

Ok(Self {
client,
repo,
token: github_token,
api,
rate_limiter,
})
}

Expand All @@ -227,16 +254,59 @@ impl GitHubClient {
self.get_release(&format!("tags/{}", version)).await
}

/// Send a request with rate-limit awareness and one retry on 429.
async fn send_with_rate_limit(
&self,
build_request: impl Fn() -> reqwest::RequestBuilder,
context_msg: &str,
) -> Result<reqwest::Response> {
self.rate_limiter.wait_if_paused().await;

let response = build_request()
.send()
.await
.context(context_msg.to_string())?;

if let Some(retry_after) = self.rate_limiter.update_from_response(&response).await {
crate::ui::warn!(
"Rate limited by GitHub API, retrying in {} seconds...",
retry_after
);
self.rate_limiter.wait_if_paused().await;

let response = build_request()
.send()
.await
.context(context_msg.to_string())?;

if let Some(retry_after) = self.rate_limiter.update_from_response(&response).await {
return Err(GitHubError::RateLimited {
retry_after_secs: retry_after,
has_token: self.token.is_some(),
}
.into());
}

return Ok(response);
}

// Warn if rate limit is exhausted (preemptive pause applies to next request)
if self.rate_limiter.remaining().await == Some(0) {
crate::ui::warn!(
"GitHub API rate limit exhausted, subsequent requests will be paused until reset"
);
}

Ok(response)
}

/// Fetch release from GitHub API
async fn get_release(&self, path: &str) -> Result<Release> {
let url = format!("{}/{}", self.api, path);

let response = self
.client
.get(&url)
.send()
.await
.context("Failed to fetch release")?;
.send_with_rate_limit(|| self.client.get(&url), "Failed to fetch release")
.await?;

if !response.status().is_success() {
let status = response.status();
Expand Down Expand Up @@ -312,12 +382,15 @@ impl GitHubClient {
);

let response = self
.client
.get(&url)
.header(reqwest::header::ACCEPT, "application/octet-stream")
.send()
.await
.context("Failed to download asset")?;
.send_with_rate_limit(
|| {
self.client
.get(&url)
.header(reqwest::header::ACCEPT, "application/octet-stream")
},
"Failed to download asset",
)
.await?;

self.download_with_progress(response, &url, asset_name)
.await
Expand All @@ -326,11 +399,8 @@ impl GitHubClient {
/// Download asset directly (for public repos)
async fn download_asset_direct(&self, url: &str, asset_name: &str) -> Result<Vec<u8>> {
let response = self
.client
.get(url)
.send()
.await
.context("Failed to download asset")?;
.send_with_rate_limit(|| self.client.get(url), "Failed to download asset")
.await?;

self.download_with_progress(response, url, asset_name).await
}
Expand Down
2 changes: 2 additions & 0 deletions ampup/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ pub mod config;
pub mod github;
pub mod install;
pub mod platform;
pub mod rate_limiter;
pub mod shell;
pub mod token;
pub mod updater;
pub mod version_manager;

Expand Down
26 changes: 23 additions & 3 deletions ampup/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ enum Commands {
/// Override platform detection (linux, darwin)
#[arg(long)]
platform: Option<String>,

/// Number of concurrent downloads (default: 4)
#[arg(short = 'j', long = "jobs")]
jobs: Option<usize>,
},

/// List installed versions
Expand Down Expand Up @@ -142,6 +146,10 @@ enum Commands {
/// Override platform detection (linux, darwin)
#[arg(long)]
platform: Option<String>,

/// Number of concurrent downloads (default: 4)
#[arg(short = 'j', long = "jobs")]
jobs: Option<usize>,
},

/// Manage the ampup executable
Expand Down Expand Up @@ -198,9 +206,18 @@ async fn run() -> anyhow::Result<()> {
github_token,
arch,
platform,
jobs,
}) => {
commands::install::run(install_dir, repo, github_token, version, arch, platform)
.await?;
commands::install::run(
install_dir,
repo,
github_token,
version,
arch,
platform,
jobs,
)
.await?;
}
Some(Commands::List { install_dir }) => {
commands::list::run(install_dir)?;
Expand Down Expand Up @@ -235,9 +252,11 @@ async fn run() -> anyhow::Result<()> {
github_token,
arch,
platform,
jobs,
}) => {
// Install latest version (same as default behavior)
commands::install::run(install_dir, repo, github_token, None, arch, platform).await?;
commands::install::run(install_dir, repo, github_token, None, arch, platform, jobs)
.await?;
}
Some(Commands::SelfCmd { command }) => match command {
SelfCommands::Update { repo, github_token } => {
Expand All @@ -256,6 +275,7 @@ async fn run() -> anyhow::Result<()> {
None,
None,
None,
None,
)
.await?;
}
Expand Down
Loading