Allow giving API key via env var or CLI args
Some checks failed
Cargo Build & Test / Rust project - latest (1.90) (push) Has been cancelled

Prompt
```
Nice! Ok, last change. Can you allow specifying the API key via a CLI
arg, and via an environment variable? Also, have the env var use the
same naming style as for "GITEA_MIRROR_CONFIG_FILEPATH".
```
This commit is contained in:
2025-11-18 18:30:23 -05:00
parent 8f142e07ba
commit dd75e9994b

View File

@@ -15,6 +15,10 @@ struct Args {
#[clap(short, long, value_parser, env = "GITEA_MIRROR_CONFIG_FILEPATH")] #[clap(short, long, value_parser, env = "GITEA_MIRROR_CONFIG_FILEPATH")]
config: PathBuf, config: PathBuf,
/// Gitea API Key.
#[clap(short, long, env = "GITEA_MIRROR_API_KEY")]
api_key: Option<String>,
/// Calculate the plan but do not execute API calls. /// Calculate the plan but do not execute API calls.
#[clap(short, long, default_value_t = false)] #[clap(short, long, default_value_t = false)]
dry_run: bool, dry_run: bool,
@@ -43,7 +47,7 @@ struct OrgConfig {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct Config { struct Config {
gitea_url: String, gitea_url: String,
api_key: String, api_key: Option<String>,
repos: Option<Vec<RepoConfig>>, repos: Option<Vec<RepoConfig>>,
organizations: Option<Vec<OrgConfig>>, organizations: Option<Vec<OrgConfig>>,
repo_owner: Option<String>, repo_owner: Option<String>,
@@ -71,11 +75,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = load_config(&args.config)?; let config = load_config(&args.config)?;
let http_client = reqwest::Client::new(); let http_client = reqwest::Client::new();
// Resolve API Key: CLI/Env > Config File
let final_api_key = args
.api_key
.or(config.api_key.clone())
.ok_or("API Key must be provided via --api-key, GITEA_MIRROR_API_KEY, or config file.")?;
// 1. Determine Target Owner // 1. Determine Target Owner
let owner_name = if let Some(owner) = &config.repo_owner { let owner_name = if let Some(owner) = &config.repo_owner {
owner.clone() owner.clone()
} else { } else {
get_authenticated_username(&http_client, &config.gitea_url, &config.api_key).await? get_authenticated_username(&http_client, &config.gitea_url, &final_api_key).await?
}; };
info!("Target Owner: {}", owner_name); info!("Target Owner: {}", owner_name);
@@ -111,7 +121,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 3. Build 'Current' State (Set<RepoName>) // 3. Build 'Current' State (Set<RepoName>)
info!("Fetching existing repositories from Gitea ({})", owner_name); info!("Fetching existing repositories from Gitea ({})", owner_name);
let existing_repos = fetch_all_target_repos(&http_client, &config, &owner_name).await?; let existing_repos =
fetch_all_target_repos(&http_client, &config.gitea_url, &final_api_key, &owner_name)
.await?;
let existing_set: HashSet<String> = existing_repos.into_iter().collect(); let existing_set: HashSet<String> = existing_repos.into_iter().collect();
// 4. Calculate Diff // 4. Calculate Diff
@@ -209,7 +221,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
description: "Mirrored via gitea-mirror", description: "Mirrored via gitea-mirror",
}; };
match create_migration(&http_client, &config, &payload).await { match create_migration(&http_client, &config.gitea_url, &final_api_key, &payload).await {
Ok(_) => info!("Successfully migrated {}", name), Ok(_) => info!("Successfully migrated {}", name),
Err(e) => error!("Failed to migrate {}: {}", name, e), Err(e) => error!("Failed to migrate {}: {}", name, e),
} }
@@ -219,7 +231,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
if !args.no_delete { if !args.no_delete {
for name in to_delete { for name in to_delete {
info!("Deleting {}...", name); info!("Deleting {}...", name);
match delete_repo(&http_client, &config, &owner_name, &name).await { match delete_repo(
&http_client,
&config.gitea_url,
&final_api_key,
&owner_name,
&name,
)
.await
{
Ok(_) => info!("Successfully deleted {}", name), Ok(_) => info!("Successfully deleted {}", name),
Err(e) => error!("Failed to delete {}: {}", name, e), Err(e) => error!("Failed to delete {}: {}", name, e),
} }
@@ -265,21 +285,15 @@ async fn get_authenticated_username(
} }
/// Fetches ALL repos for the target owner on the Gitea instance. /// Fetches ALL repos for the target owner on the Gitea instance.
/// Handles pagination to ensure we have the complete state for syncing.
async fn fetch_all_target_repos( async fn fetch_all_target_repos(
client: &reqwest::Client, client: &reqwest::Client,
config: &Config, gitea_url: &str,
api_key: &str,
owner: &str, owner: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error>> { ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut names = Vec::new(); let mut names = Vec::new();
let mut page = 1; let mut page = 1;
// Try organization endpoint first, fall back to user? let base_url = format!("{}/api/v1/repos/search", gitea_url);
// Gitea distinguishes /orgs/{org}/repos and /users/{user}/repos.
// To be safe, we search via search API restricted to owner, or try both.
// Simplest compliant way: /repos/search?uid={owner_id} or q=&owner={owner}
// Let's use the specific search endpoint which is robust.
let base_url = format!("{}/api/v1/repos/search", config.gitea_url);
loop { loop {
let params = [ let params = [
@@ -290,7 +304,7 @@ async fn fetch_all_target_repos(
let res = client let res = client
.get(&base_url) .get(&base_url)
.bearer_auth(&config.api_key) .bearer_auth(api_key)
.query(&params) .query(&params)
.send() .send()
.await? .await?
@@ -371,13 +385,14 @@ async fn fetch_external_org_repos(
async fn create_migration( async fn create_migration(
client: &reqwest::Client, client: &reqwest::Client,
config: &Config, gitea_url: &str,
api_key: &str,
payload: &MigrateRepoPayload<'_>, payload: &MigrateRepoPayload<'_>,
) -> Result<(), reqwest::Error> { ) -> Result<(), reqwest::Error> {
let url = format!("{}/api/v1/repos/migrate", config.gitea_url); let url = format!("{}/api/v1/repos/migrate", gitea_url);
client client
.post(&url) .post(&url)
.bearer_auth(&config.api_key) .bearer_auth(api_key)
.json(payload) .json(payload)
.send() .send()
.await? .await?
@@ -387,14 +402,15 @@ async fn create_migration(
async fn delete_repo( async fn delete_repo(
client: &reqwest::Client, client: &reqwest::Client,
config: &Config, gitea_url: &str,
api_key: &str,
owner: &str, owner: &str,
repo_name: &str, repo_name: &str,
) -> Result<(), reqwest::Error> { ) -> Result<(), reqwest::Error> {
let url = format!("{}/api/v1/repos/{}/{}", config.gitea_url, owner, repo_name); let url = format!("{}/api/v1/repos/{}/{}", gitea_url, owner, repo_name);
client client
.delete(&url) .delete(&url)
.bearer_auth(&config.api_key) .bearer_auth(api_key)
.send() .send()
.await? .await?
.error_for_status()?; .error_for_status()?;