use std::fmt::Write;
use std::fs;
use std::io::Write as IoWrite;
use std::path::{Path, PathBuf};
use tokio::task;
use super::{OfficialPkg, idx, save_to_disk};
use crate::sources::{
check_circuit_breaker, extract_retry_after_from_error, increase_archlinux_backoff,
rate_limit_archlinux, record_circuit_breaker_outcome, reset_archlinux_backoff,
};
use crate::util::curl;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
pub async fn fetch_mirrors_to_repo_dir(repo_dir: &Path) -> Result<PathBuf> {
let repo_dir = repo_dir.to_path_buf();
task::spawn_blocking(move || {
fs::create_dir_all(&repo_dir)?;
let status_url = "https://archlinux.org/mirrors/status/json/";
let json = curl::curl_json(status_url)?;
let mirrors_json_path = repo_dir.join("mirrors.json");
fs::write(&mirrors_json_path, serde_json::to_vec_pretty(&json)?)?;
let mut https_urls: Vec<String> = Vec::new();
if let Some(arr) = json.get("urls").and_then(|v| v.as_array()) {
for u in arr {
let active = u
.get("active")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let url = u.get("url").and_then(|v| v.as_str()).unwrap_or_default();
let protocols = u
.get("protocols")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let has_https = protocols
.iter()
.any(|p| p.as_str().is_some_and(|s| s.eq_ignore_ascii_case("https")));
if active && has_https && !url.is_empty() {
https_urls.push(url.to_string());
}
}
}
https_urls.sort();
https_urls.dedup();
if https_urls.len() > 40 {
https_urls.truncate(40);
}
let mut mirrorlist: String = String::new();
mirrorlist.push_str("# Generated from Arch mirror status (Windows)\n");
mirrorlist.push_str("# Only HTTPS and active mirrors are listed.\n");
for base in &https_urls {
let base = base.trim_end_matches('/');
let _ = writeln!(mirrorlist, "Server = {base}/$repo/os/$arch");
}
let mirrorlist_path = repo_dir.join("mirrorlist.txt");
fs::write(&mirrorlist_path, mirrorlist.as_bytes())?;
Ok::<PathBuf, Box<dyn std::error::Error + Send + Sync>>(mirrorlist_path)
})
.await?
}
fn parse_package_from_json(obj: &serde_json::Value, repo: &str, arch: &str) -> Option<OfficialPkg> {
let name = obj
.get("pkgname")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
if name.is_empty() {
return None;
}
let version = obj
.get("pkgver")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let description = obj
.get("pkgdesc")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let arch_val = obj
.get("arch")
.and_then(|v| v.as_str())
.unwrap_or(arch)
.to_string();
let repo_val = obj
.get("repo")
.and_then(|v| v.as_str())
.unwrap_or(repo)
.to_string();
Some(OfficialPkg {
name,
repo: repo_val,
arch: arch_val,
version,
description,
})
}
fn try_alternative_url_formats(
repo: &str,
arch: &str,
page: usize,
limit: usize,
) -> Result<(serde_json::Value, Vec<serde_json::Value>)> {
let alternatives = vec![
(
"q=*",
format!(
"https://archlinux.org/packages/search/json/?q=*&repo={repo}&arch={arch}&limit={limit}&page={page}"
),
),
(
"q=%2A",
format!(
"https://archlinux.org/packages/search/json/?q=%2A&repo={repo}&arch={arch}&limit={limit}&page={page}"
),
),
(
"q=a",
format!(
"https://archlinux.org/packages/search/json/?q=a&repo={repo}&arch={arch}&limit={limit}&page={page}"
),
),
(
"q=",
format!(
"https://archlinux.org/packages/search/json/?q=&repo={repo}&arch={arch}&limit={limit}&page={page}"
),
),
];
for (format_name, alt_url) in alternatives {
tracing::debug!(
repo = repo,
page = page,
format = format_name,
url = %alt_url,
"Trying alternative API URL format"
);
match curl::curl_json(&alt_url) {
Ok(alt_v) => {
let alt_results = alt_v
.get("results")
.and_then(|x| x.as_array())
.cloned()
.unwrap_or_default();
let alt_valid = alt_v
.get("valid")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
if alt_valid && !alt_results.is_empty() {
tracing::info!(
repo = repo,
page = page,
format = format_name,
"Alternative URL format worked"
);
return Ok((alt_v, alt_results));
} else if !alt_results.is_empty() {
tracing::warn!(
repo = repo,
page = page,
format = format_name,
"Alternative URL returned results despite valid=false"
);
return Ok((alt_v, alt_results));
}
tracing::debug!(
repo = repo,
page = page,
format = format_name,
valid = alt_valid,
result_count = alt_results.len(),
"Alternative format returned no results"
);
}
Err(alt_e) => {
tracing::debug!(
repo = repo,
page = page,
format = format_name,
error = %alt_e,
"Alternative URL format failed"
);
}
}
}
Err(format!(
"Arch Linux Packages API returned invalid query response for {repo} (page {page}). All URL formats failed with valid=false and no results. The API may have changed or requires different parameters."
).into())
}
fn log_empty_results_debug(v: &serde_json::Value, repo: &str, page: usize, url: &str) {
if page == 1 {
let response_str = serde_json::to_string_pretty(v)
.unwrap_or_else(|_| "Failed to serialize response".to_string());
let response_preview = if response_str.len() > 500 {
{
let preview = &response_str[..500];
format!("{preview}...")
}
} else {
response_str
};
tracing::warn!(
repo = repo,
url = %url,
response_preview = %response_preview,
"First page returned empty results - checking API response structure"
);
if let Some(count) = v.get("count").and_then(serde_json::Value::as_u64) {
tracing::warn!(
repo = repo,
total_count = count,
"API reports total count but results array is empty"
);
}
if let Some(limit_val) = v.get("limit").and_then(serde_json::Value::as_u64) {
tracing::debug!(repo = repo, api_limit = limit_val, "API limit value");
}
}
}
fn fetch_package_page(
repo: &str,
arch: &str,
page: usize,
limit: usize,
) -> Result<(Vec<serde_json::Value>, bool)> {
let url = format!(
"https://archlinux.org/packages/search/json/?repo={repo}&arch={arch}&limit={limit}&page={page}"
);
tracing::debug!(repo = repo, page = page, url = %url, "Fetching package page from API");
let mut v = curl::curl_json(&url).map_err(|e| {
tracing::error!(repo = repo, page = page, error = %e, "Failed to fetch package page");
Box::<dyn std::error::Error + Send + Sync>::from(format!(
"Failed to fetch package list for {repo} (page {page}): {e}"
))
})?;
let mut results = v
.get("results")
.and_then(|x| x.as_array())
.cloned()
.unwrap_or_default();
if let Some(valid) = v.get("valid").and_then(serde_json::Value::as_bool) {
if !valid && results.is_empty() {
let response_str = serde_json::to_string_pretty(&v)
.unwrap_or_else(|_| "Failed to serialize response".to_string());
tracing::warn!(
repo = repo,
page = page,
url = %url,
response = %response_str,
"API query returned valid=false with no results, trying with q parameter"
);
let (alt_v, alt_results) = try_alternative_url_formats(repo, arch, page, limit)?;
v = alt_v;
results = alt_results;
} else if !valid && !results.is_empty() {
tracing::warn!(
repo = repo,
page = page,
result_count = results.len(),
"API returned valid=false but has results, processing anyway"
);
}
}
if page == 1 {
tracing::debug!(
repo = repo,
response_keys = ?v.as_object().map(|o| o.keys().collect::<Vec<_>>()),
"API response structure"
);
}
if results.is_empty() {
tracing::debug!(repo = repo, page = page, "No more results for repository");
log_empty_results_debug(&v, repo, page, &url);
return Ok((results, false));
}
tracing::debug!(
repo = repo,
page = page,
count = results.len(),
"Fetched package page"
);
Ok((results, true))
}
async fn fetch_repo_packages_with_rate_limit(
repo: &str,
arch: &str,
endpoint_pattern: &str,
net_err_tx: &tokio::sync::mpsc::UnboundedSender<String>,
) -> Result<Vec<OfficialPkg>> {
tracing::info!(repo = repo, "Fetching packages from repository");
let mut pkgs: Vec<OfficialPkg> = Vec::new();
let mut page: usize = 1;
let limit: usize = 250;
loop {
if let Err(e) = check_circuit_breaker(endpoint_pattern) {
let msg = format!(
"Circuit breaker open for {}: {}. Stopping repository fetch for {}",
endpoint_pattern, e, repo
);
tracing::warn!(repo = repo, page = page, error = %e, "Circuit breaker blocked page fetch");
let _ = net_err_tx.send(msg);
return Err(format!("Circuit breaker open: {}", e).into());
}
let _permit = rate_limit_archlinux().await;
let fetch_result = task::spawn_blocking({
let repo = repo.to_string();
let arch = arch.to_string();
move || fetch_package_page_sync(&repo, &arch, page, limit)
})
.await;
match fetch_result {
Ok(Ok((results, has_more))) => {
reset_archlinux_backoff();
record_circuit_breaker_outcome(endpoint_pattern, true);
for obj in results {
if let Some(pkg) = parse_package_from_json(&obj, repo, arch) {
pkgs.push(pkg);
}
}
if !has_more {
break;
}
page += 1;
}
Ok(Err(e)) => {
let error_str = e.to_string();
let retry_after_seconds = extract_retry_after_from_error(&error_str);
if error_str.contains("429") || error_str.contains("503") {
if let Some(retry_after) = retry_after_seconds {
tracing::warn!(
repo = repo,
page = page,
retry_after_seconds = retry_after,
"HTTP {} detected, using Retry-After for backoff",
if error_str.contains("429") {
"429"
} else {
"503"
}
);
increase_archlinux_backoff(Some(retry_after));
} else {
tracing::warn!(
repo = repo,
page = page,
"HTTP {} detected, increasing backoff",
if error_str.contains("429") {
"429"
} else {
"503"
}
);
increase_archlinux_backoff(None);
}
} else {
increase_archlinux_backoff(None);
}
record_circuit_breaker_outcome(endpoint_pattern, false);
return Err(e);
}
Err(join_err) => {
let msg = format!("Task join error during page fetch: {}", join_err);
tracing::error!(repo = repo, page = page, error = %join_err, "Task join error");
let _ = net_err_tx.send(msg);
return Err(format!("Task join error: {}", join_err).into());
}
}
}
tracing::info!(
repo = repo,
package_count = pkgs.len(),
"Completed fetching repository"
);
Ok(pkgs)
}
fn fetch_package_page_sync(
repo: &str,
arch: &str,
page: usize,
limit: usize,
) -> Result<(Vec<serde_json::Value>, bool)> {
fetch_package_page(repo, arch, page, limit)
}
pub async fn refresh_official_index_from_arch_api(
persist_path: PathBuf,
net_err_tx: tokio::sync::mpsc::UnboundedSender<String>,
notify_tx: tokio::sync::mpsc::UnboundedSender<()>,
) {
let repos = vec!["core", "extra", "multilib"];
let arch = "x86_64";
let endpoint_pattern = "/packages/*/json/";
let res: Result<Vec<OfficialPkg>> = async {
let mut pkgs: Vec<OfficialPkg> = Vec::new();
for repo in repos {
if let Err(e) = check_circuit_breaker(endpoint_pattern) {
let _ = &e;
let msg = format!(
"Circuit breaker open for {}: {}. Skipping repository {}",
endpoint_pattern, e, repo
);
tracing::warn!(repo = repo, error = %e, "Circuit breaker blocked repository fetch");
let _ = net_err_tx.send(msg);
continue;
}
match fetch_repo_packages_with_rate_limit(repo, arch, endpoint_pattern, &net_err_tx)
.await
{
Ok(repo_pkgs) => {
pkgs.extend(repo_pkgs);
record_circuit_breaker_outcome(endpoint_pattern, true);
}
Err(e) => {
let _ = &e;
let msg = format!("Failed to fetch repository {}: {}", repo, e);
tracing::error!(repo = repo, error = %e, "Failed to fetch repository");
let _ = net_err_tx.send(msg);
record_circuit_breaker_outcome(endpoint_pattern, false);
}
}
}
pkgs.sort_by(|a, b| a.repo.cmp(&b.repo).then(a.name.cmp(&b.name)));
let before_dedup = pkgs.len();
pkgs.dedup_by(|a, b| a.repo == b.repo && a.name == b.name);
let after_dedup = pkgs.len();
if before_dedup != after_dedup {
tracing::debug!(
before = before_dedup,
after = after_dedup,
removed = before_dedup - after_dedup,
"Deduplicated packages"
);
}
tracing::info!(
total_packages = pkgs.len(),
"Completed fetching all repositories"
);
Ok(pkgs)
}
.await;
match res {
Ok(new_list) => {
tracing::info!(
package_count = new_list.len(),
path = %persist_path.display(),
"Successfully fetched official package index"
);
if let Ok(mut guard) = idx().write() {
guard.pkgs.clone_from(&new_list);
guard.rebuild_name_index();
tracing::debug!("Updated in-memory index with {} packages", guard.pkgs.len());
} else {
tracing::warn!("Failed to acquire write lock for index update");
}
save_to_disk(&persist_path);
tracing::info!(path = %persist_path.display(), "Persisted index to disk");
let _ = notify_tx.send(());
}
Err(e) => {
let msg = format!("Failed to fetch official index via API: {e}");
let _ = net_err_tx.send(msg);
tracing::error!(error = %e, "Failed to fetch official index");
}
}
}
pub fn check_curl_availability() -> Result<()> {
let output = std::process::Command::new("curl")
.arg("--version")
.output()
.map_err(|e| format!("curl not found in PATH: {e}"))?;
if !output.status.success() {
return Err(format!(
"curl command failed with exit code: {:?}",
output.status.code()
)
.into());
}
Ok(())
}
pub fn verify_index_file(index_path: &Path) -> Result<(usize, u64)> {
if !index_path.exists() {
return Err(format!("Index file does not exist: {}", index_path.display()).into());
}
let metadata =
fs::metadata(index_path).map_err(|e| format!("Failed to read index file metadata: {e}"))?;
let size = metadata.len();
if size == 0 {
return Err("Index file is empty".into());
}
let content =
fs::read_to_string(index_path).map_err(|e| format!("Failed to read index file: {e}"))?;
let index: super::OfficialIndex =
serde_json::from_str(&content).map_err(|e| format!("Failed to parse index JSON: {e}"))?;
let count = index.pkgs.len();
if count == 0 {
return Err("Index file contains no packages".into());
}
Ok((count, size))
}
pub async fn refresh_windows_mirrors_and_index(
persist_path: PathBuf,
repo_dir: PathBuf,
net_err_tx: tokio::sync::mpsc::UnboundedSender<String>,
notify_tx: tokio::sync::mpsc::UnboundedSender<()>,
) {
match check_curl_availability() {
Ok(()) => {
tracing::info!("curl is available for Windows index refresh");
}
Err(e) => {
let msg = format!(
"curl is not available: {e}. Windows index refresh requires curl to be installed and in PATH."
);
let _ = net_err_tx.send(msg);
tracing::error!(error = %e, "curl availability check failed");
return;
}
}
if persist_path.exists() {
match verify_index_file(&persist_path) {
Ok((count, size)) => {
tracing::info!(
path = %persist_path.display(),
package_count = count,
file_size_bytes = size,
"Existing index file found and verified"
);
}
Err(e) => {
tracing::warn!(
path = %persist_path.display(),
error = %e,
"Existing index file is invalid or empty, will refresh"
);
}
}
} else {
tracing::info!(
path = %persist_path.display(),
"Index file does not exist, will create new index"
);
}
match fetch_mirrors_to_repo_dir(&repo_dir).await {
Ok(path) => {
let _ = notify_tx.send(());
tracing::info!(mirrorlist = %path.display(), "Saved mirror list for reference");
}
Err(e) => {
let _ = net_err_tx.send(format!("Failed to fetch mirrors: {e}"));
tracing::warn!(error = %e, "Failed to fetch mirrors");
}
}
tracing::info!("Starting official package index refresh from Arch API");
refresh_official_index_from_arch_api(
persist_path.clone(),
net_err_tx.clone(),
notify_tx.clone(),
)
.await;
match verify_index_file(&persist_path) {
Ok((count, size)) => {
tracing::info!(
path = %persist_path.display(),
package_count = count,
file_size_bytes = size,
"Index refresh completed successfully"
);
let _ = notify_tx.send(());
}
Err(e) => {
let msg = format!("Index refresh completed but verification failed: {e}");
let _ = net_err_tx.send(msg);
tracing::error!(
path = %persist_path.display(),
error = %e,
"Index verification failed after refresh"
);
}
}
}
#[cfg(test)]
#[cfg(not(target_os = "windows"))]
mod tests {
use super::*;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time;
#[tokio::test]
async fn fetch_mirrors_to_repo_dir_writes_json_and_filtered_mirrorlist() {
let mut repo_dir = std::env::temp_dir();
repo_dir.push(format!(
"pacsea_test_mirrors_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
std::fs::create_dir_all(&repo_dir).expect("failed to create test repo directory");
let old_path = std::env::var("PATH").unwrap_or_default();
struct PathGuard {
original: String,
}
impl Drop for PathGuard {
fn drop(&mut self) {
unsafe {
std::env::set_var("PATH", &self.original);
}
}
}
let _path_guard = PathGuard {
original: old_path.clone(),
};
let mut shim_root = std::env::temp_dir();
shim_root.push(format!(
"pacsea_fake_curl_mirrors_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
std::fs::create_dir_all(&shim_root).expect("failed to create test shim root directory");
let mut bin = shim_root.clone();
bin.push("bin");
std::fs::create_dir_all(&bin).expect("failed to create test bin directory");
let mut script = bin.clone();
script.push("curl");
let body = r#"#!/usr/bin/env bash
set -e
if [[ "$1" == "-sSLf" ]]; then
cat <<'EOF'
{"urls":[{"url":"https://fast.example/", "active":true, "protocols":["https"]},{"url":"http://slow.example/", "active":true, "protocols":["http"]},{"url":"https://inactive.example/", "active":false, "protocols":["https"]}]}
EOF
exit 0
fi
exit 1
"#;
std::fs::write(&script, body).expect("failed to write test curl script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&script)
.expect("failed to read test curl script metadata")
.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&script, perm)
.expect("failed to set test curl script permissions");
}
let new_path = format!("{}:{old_path}", bin.to_string_lossy());
unsafe {
std::env::set_var("PATH", &new_path);
}
let mirrorlist_path = super::fetch_mirrors_to_repo_dir(&repo_dir)
.await
.expect("Failed to fetch mirrors in test");
let raw_json_path = repo_dir.join("mirrors.json");
assert!(raw_json_path.exists());
assert!(mirrorlist_path.exists());
let mirrorlist_body =
std::fs::read_to_string(&mirrorlist_path).expect("failed to read test mirrorlist file");
assert!(mirrorlist_body.contains("https://fast.example/$repo/os/$arch"));
assert!(!mirrorlist_body.contains("slow.example"));
assert!(!mirrorlist_body.contains("inactive.example"));
let _ = std::fs::remove_dir_all(&repo_dir);
let _ = std::fs::remove_dir_all(&shim_root);
}
#[allow(clippy::await_holding_lock)]
#[tokio::test]
async fn refresh_official_index_from_arch_api_consumes_api_results_and_persists() {
let _guard = crate::index::test_mutex()
.lock()
.expect("Test mutex poisoned");
if let Ok(mut g) = super::idx().write() {
g.pkgs.clear();
}
let mut persist_path = std::env::temp_dir();
persist_path.push(format!(
"pacsea_mirrors_index_refresh_{}_{}.json",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
let (net_err_tx, mut net_err_rx) = mpsc::unbounded_channel::<String>();
let (notify_tx, mut notify_rx) = mpsc::unbounded_channel::<()>();
let old_path = std::env::var("PATH").unwrap_or_default();
struct PathGuard {
original: String,
}
impl Drop for PathGuard {
fn drop(&mut self) {
unsafe {
std::env::set_var("PATH", &self.original);
}
}
}
let _path_guard = PathGuard {
original: old_path.clone(),
};
let mut shim_root = std::env::temp_dir();
shim_root.push(format!(
"pacsea_fake_curl_index_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
std::fs::create_dir_all(&shim_root).expect("failed to create test shim root directory");
let mut bin = shim_root.clone();
bin.push("bin");
std::fs::create_dir_all(&bin).expect("failed to create test bin directory");
let mut script = bin.clone();
script.push("curl");
let body = r#"#!/usr/bin/env bash
set -e
if [[ "$1" == "-sSLf" ]]; then
url="$2"
if [[ "$url" == *"page=1"* ]]; then
if [[ "$url" == *"repo=core"* ]]; then
cat <<'EOF'
{"results":[{"pkgname":"core-pkg","pkgver":"1.0","pkgdesc":"Core package","arch":"x86_64","repo":"core"}]}
EOF
elif [[ "$url" == *"repo=extra"* ]]; then
cat <<'EOF'
{"results":[{"pkgname":"extra-pkg","pkgver":"2.0","pkgdesc":"Extra package","arch":"x86_64","repo":"extra"}]}
EOF
else
cat <<'EOF'
{"results":[]}
EOF
fi
else
cat <<'EOF'
{"results":[]}
EOF
fi
exit 0
fi
exit 1
"#;
std::fs::write(&script, body).expect("failed to write test curl script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&script)
.expect("failed to read test curl script metadata")
.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&script, perm)
.expect("failed to set test curl script permissions");
}
let new_path = format!("{}:{old_path}", bin.to_string_lossy());
unsafe {
std::env::set_var("PATH", &new_path);
}
super::refresh_official_index_from_arch_api(persist_path.clone(), net_err_tx, notify_tx)
.await;
let notified = time::timeout(Duration::from_millis(200), notify_rx.recv())
.await
.ok()
.flatten()
.is_some();
assert!(notified);
let err = time::timeout(Duration::from_millis(200), net_err_rx.recv())
.await
.ok()
.flatten();
assert!(err.is_none());
let mut names: Vec<String> = crate::index::all_official()
.into_iter()
.map(|p| p.name)
.collect();
names.sort();
assert_eq!(names, vec!["core-pkg".to_string(), "extra-pkg".to_string()]);
let body = std::fs::read_to_string(&persist_path).expect("failed to read test index file");
assert!(body.contains("\"core-pkg\""));
assert!(body.contains("\"extra-pkg\""));
if let Ok(mut g) = super::idx().write() {
g.pkgs.clear();
}
let _ = std::fs::remove_file(&persist_path);
let _ = std::fs::remove_dir_all(&shim_root);
}
}
pub async fn download_sync_db(repo_dir: &Path, repo: &str, arch: &str) -> Result<PathBuf> {
let base = "https://geo.mirror.pkgbuild.com";
let url = format!("{base}/{repo}/os/{arch}/{repo}.db");
let out_path = repo_dir.join(format!("{repo}-{arch}.db"));
let out_path_clone = out_path.clone();
let body = task::spawn_blocking(move || curl::curl_text(&url)).await??;
task::spawn_blocking(move || -> Result<()> {
fs::create_dir_all(out_path_clone.parent().unwrap_or_else(|| Path::new(".")))?;
let mut f = fs::File::create(&out_path_clone)?;
f.write_all(body.as_bytes())?;
Ok(())
})
.await??;
Ok(out_path)
}