use super::url::{parse_clone_url, ParsedGitUrl};
use anyhow::Result;
use reqwest::blocking::Client;
use reqwest::header::{ACCEPT, AUTHORIZATION, USER_AGENT};
use serde::Deserialize;
use serde_json::Value;
use std::collections::VecDeque;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::Builder as TempDirBuilder;
#[derive(Deserialize, Debug)]
struct ContentItem {
path: String,
#[serde(rename = "type")]
item_type: String,
download_url: Option<String>,
}
#[derive(Deserialize, Debug)]
struct RepoInfo {
default_branch: String,
}
pub fn download_directory_via_api(
url_parts: &ParsedGitUrl,
branch_override: &Option<String>,
) -> Result<PathBuf> {
let temp_dir = TempDirBuilder::new().prefix("dircat-git-api-").tempdir()?;
let client = build_reqwest_client()?;
let (owner, repo) = parse_clone_url(&url_parts.clone_url)?;
let branch_to_use = if let Some(branch_override) = branch_override {
log::debug!("Using branch from override: {}", branch_override);
branch_override.clone()
} else if url_parts.branch != "HEAD" {
log::debug!("Using branch from URL: {}", url_parts.branch);
url_parts.branch.clone()
} else {
log::debug!("Fetching default branch for {}/{}", owner, repo);
fetch_default_branch(&owner, &repo, &client)?
};
log::info!("Processing repository on branch: {}", branch_to_use);
let files_to_download =
list_all_files_recursively(&client, &owner, &repo, &branch_to_use, url_parts)?;
if files_to_download.is_empty() {
return Ok(temp_dir.keep());
}
use rayon::prelude::*;
files_to_download
.par_iter()
.map(|file_item| download_and_write_file(&client, file_item, temp_dir.path()))
.collect::<Result<()>>()?;
Ok(temp_dir.keep())
}
fn build_reqwest_client() -> Result<Client> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(ACCEPT, "application/vnd.github.v3+json".parse()?);
headers.insert(USER_AGENT, "dircat-rust-downloader".parse()?);
if let Ok(token) = env::var("GITHUB_TOKEN") {
headers.insert(AUTHORIZATION, format!("Bearer {}", token).parse()?);
log::debug!("Using GITHUB_TOKEN for authentication.");
}
Ok(Client::builder().default_headers(headers).build()?)
}
fn fetch_default_branch(owner: &str, repo: &str, client: &Client) -> Result<String> {
let api_url = format!("https://api.github.com/repos/{}/{}", owner, repo);
log::debug!("Fetching repo metadata from: {}", api_url);
let response = client.get(&api_url).send()?.error_for_status()?;
let repo_info: RepoInfo = response.json()?;
Ok(repo_info.default_branch)
}
fn list_all_files_recursively(
client: &Client,
owner: &str,
repo: &str,
branch: &str,
url_parts: &ParsedGitUrl,
) -> Result<Vec<ContentItem>> {
let mut files = Vec::new();
let mut queue: VecDeque<String> = VecDeque::new();
queue.push_back(url_parts.subdirectory.clone());
while let Some(path) = queue.pop_front() {
let api_url = format!(
"https://api.github.com/repos/{}/{}/contents/{}?ref={}",
owner, repo, path, branch
);
log::debug!("Fetching directory contents from: {}", api_url);
let response = client.get(&api_url).send()?.error_for_status()?;
let response_text = response.text()?;
let json_value: Value = serde_json::from_str(&response_text)?;
let items: Vec<ContentItem> = if json_value.is_array() {
serde_json::from_value(json_value)?
} else if json_value.is_object() {
vec![serde_json::from_value(json_value)?]
} else {
vec![]
};
for item in items {
if item.item_type == "file" {
if item.download_url.is_some() {
files.push(item);
} else {
log::warn!("Skipping file with no download_url: {}", item.path);
}
} else if item.item_type == "dir" {
queue.push_back(item.path);
}
}
}
Ok(files)
}
fn download_and_write_file(
client: &Client,
file_item: &ContentItem,
base_dir: &Path,
) -> Result<()> {
use anyhow::Context;
let download_url = file_item.download_url.as_ref().unwrap(); log::debug!("Downloading file from: {}", download_url);
let response = client.get(download_url).send()?.error_for_status()?;
let content = response.bytes()?;
let local_path = base_dir.join(&file_item.path);
if let Some(parent_dir) = local_path.parent() {
fs::create_dir_all(parent_dir).with_context(|| {
format!(
"Failed to create directory structure for '{}'",
local_path.display()
)
})?;
}
fs::write(&local_path, content).with_context(|| {
format!(
"Failed to write downloaded content to '{}'",
local_path.display()
)
})
}