Skip to content
Merged
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
116 changes: 116 additions & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use crate::cli::Cli;
use crate::models::Result;
use jlogger_tracing::{jdebug, jinfo};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use std::fs;

/// Add authentication header to request headers
pub fn add_auth_header(cli: &Cli, header: &mut HeaderMap) -> Result<()> {
let mut success = false;

// Try direct token first
if let Some(token) = &cli.token {
jinfo!("Using token from command line");
let auth_value = format!("Bearer {}", token.trim());
header.insert(
AUTHORIZATION,
HeaderValue::from_str(&auth_value).map_err(|e| e.to_string())?,
);
success = true;
} else if let Some(token_file) = &cli.token_file {
// Try token file
jinfo!("Using token from file: {}", token_file);
match fs::read_to_string(token_file) {
Ok(token) => {
let auth_value = format!("Bearer {}", token.trim());
header.insert(
AUTHORIZATION,
HeaderValue::from_str(&auth_value).map_err(|e| e.to_string())?,
);
success = true;
}
Err(e) => {
return Err(format!("Failed to read token file: {}", e));
}
}
} else {
// Try .netrc as fallback
if let Ok(home) = std::env::var("HOME") {
let netrc_path = std::path::Path::new(&home).join(".netrc");
jdebug!("Trying .netrc at {:?}", netrc_path);

if let Ok(content) = fs::read_to_string(&netrc_path) {
jinfo!("Using .netrc for authentication");
let lines: Vec<&str> = content.lines().collect();
let mut in_github = false;

for line in lines {
let trimmed = line.trim();

if trimmed.starts_with("machine") {
if trimmed.contains("github.com") {
jinfo!("Found machine github.com in .netrc");
in_github = true;
} else {
in_github = false;
}
} else if in_github && trimmed.starts_with("password") {
if let Some(password) = trimmed.split_whitespace().nth(1) {
let auth_value = format!("Bearer {}", password);
header.insert(
AUTHORIZATION,
HeaderValue::from_str(&auth_value).map_err(|e| e.to_string())?,
);
success = true;
break;
}
}
}
}
}
}

if success {
Ok(())
} else {
Err("No authentication method provided".to_string())
}
}

/// Extract token from CLI arguments
pub fn extract_token_from_cli(cli: &Cli) -> Option<String> {
// Try direct token first
if let Some(token) = &cli.token {
return Some(token.clone());
}

// Try token file
if let Some(token_file) = &cli.token_file {
if let Ok(token) = std::fs::read_to_string(token_file) {
return Some(token.trim().to_string());
}
}

// Try .netrc
if let Ok(home) = std::env::var("HOME") {
let netrc_path = std::path::Path::new(&home).join(".netrc");
if let Ok(content) = std::fs::read_to_string(&netrc_path) {
let lines: Vec<&str> = content.lines().collect();
let mut in_github = false;
for line in lines {
let trimmed = line.trim();
if trimmed.starts_with("machine") && trimmed.contains("github.com") {
in_github = true;
} else if in_github && trimmed.starts_with("password") {
if let Some(password) = trimmed.split_whitespace().nth(1) {
return Some(password.to_string());
}
} else if trimmed.starts_with("machine") {
in_github = false;
}
}
}
}

None
}
70 changes: 70 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use clap::{ArgAction, Parser};
use std::path::PathBuf;

/// CLI arguments
#[derive(Parser)]
#[command(
name = "Github release fetcher",
version,
about = "A tool to retrieve and download github release package."
)]
pub struct Cli {
/// GitHub Repository in the format "owner/repo" (required for release operations)
#[arg(long, short = 'r')]
pub repo: Option<String>,

/// Token for GitHub API authentication
#[arg(short = 't', long = "token")]
pub token: Option<String>,

/// File containing GitHub API token
#[arg(short = 'T', long = "token-file")]
pub token_file: Option<String>,

/// Specific version to download (or "latest" for the most recent release)
#[arg(short = 'd', long = "download")]
pub download: Option<String>,

/// String used to filter the name of assets to download, multiple filters can be separated by
/// commas.
#[arg(short = 'f', long = "filter")]
pub filter: Option<String>,

/// Search for repositories using pattern:
/// - "username/keyword": Search repos owned by username containing keyword
/// - "username/": List all repos owned by username
/// - "/keyword": Search top N repos globally containing keyword
#[arg(short = 's', long = "search")]
pub search: Option<String>,

/// Directory to save downloaded assets (defaults to current directory)
#[arg(short = 'o', long = "output-dir")]
pub output_dir: Option<PathBuf>,

/// Show information about a specific version, multiple versions can be separated by commas.
#[arg(short = 'i', long = "info")]
pub info: Option<String>,

/// Number of packages to fetch
#[arg(short = 'n', long = "num", default_value_t = 10)]
pub num: usize,

/// Maximum number of concurrent downloads
#[arg(short = 'j', long = "concurrency", default_value_t = 5)]
pub concurrency: usize,

/// Clone a repository with optional ref (branch/tag/sha1)
/// Format: <url>[:<ref>] where url can be:
/// - https://github.com/owner/repo
/// - git@github.com:owner/repo.git
/// - owner/repo (short format)
#[arg(short = 'c', long = "clone", value_name = "URL[:REF]")]
pub clone: Option<String>,

/// Local directory for cloned repository (defaults to repository name)
#[arg(value_name = "DIRECTORY", requires = "clone")]
pub directory: Option<String>,

#[arg(short = 'v', long = "verbose", action = ArgAction::Count)]
pub verbose: u8,
}
Loading
Loading