use super::{decompress_index, fetch_repo_file, warm_repo_paths, MirrorResult};
pub async fn run_rpm_mirror(
client: &reqwest::Client,
registry: &str,
repo: &str,
arches: Option<&[String]>,
concurrency: usize,
) -> Result<MirrorResult, String> {
let base = registry.trim_end_matches('/');
let repomd = fetch_repo_file(client, base, "rpm", repo, "repodata/repomd.xml").await?;
let repomd = String::from_utf8(repomd).map_err(|e| format!("repomd.xml: {e}"))?;
for extra in ["repodata/repomd.xml.asc", "repodata/repomd.xml.key"] {
let _ = fetch_repo_file(client, base, "rpm", repo, extra).await;
}
let primary_href =
primary_location(&repomd).ok_or("repomd.xml has no <data type=\"primary\"> location")?;
let primary_raw = fetch_repo_file(client, base, "rpm", repo, &primary_href).await?;
let primary = decompress_index(&primary_href, primary_raw)?;
let mut paths = location_hrefs(&primary);
if let Some(arches) = arches {
let suffixes: Vec<String> = arches.iter().map(|a| format!(".{a}.rpm")).collect();
paths.retain(|p| suffixes.iter().any(|s| p.ends_with(s.as_str())));
}
if paths.is_empty() {
return Err("no packages found in primary.xml (wrong repo or arch filter?)".to_string());
}
let repodata_extras: Vec<String> = location_hrefs(&repomd)
.into_iter()
.filter(|h| *h != primary_href)
.collect();
println!(
"Mirroring {} rpm packages from repo '{repo}' via {registry}...",
paths.len()
);
let meta = warm_repo_paths(client, base, "rpm", repo, &repodata_extras, concurrency).await;
let mut result = warm_repo_paths(client, base, "rpm", repo, &paths, concurrency).await;
result.total += meta.total;
result.fetched += meta.fetched;
result.failed += meta.failed;
result.bytes += meta.bytes;
Ok(result)
}
fn primary_location(repomd: &str) -> Option<String> {
let start = repomd.find("<data type=\"primary\">")?;
let block = &repomd[start..];
let end = block.find("</data>").unwrap_or(block.len());
location_hrefs(&block[..end]).into_iter().next()
}
fn location_hrefs(xml: &str) -> Vec<String> {
let mut out = Vec::new();
let mut rest = xml;
while let Some(pos) = rest.find("<location") {
rest = &rest[pos..];
let tag_end = match rest.find('>') {
Some(e) => e,
None => break,
};
let tag = &rest[..tag_end];
if let Some(href_start) = tag.find("href=\"") {
let val = &tag[href_start + 6..];
if let Some(href_end) = val.find('"') {
out.push(val[..href_end].to_string());
}
}
rest = &rest[tag_end..];
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const REPOMD: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo">
<data type="primary">
<location href="repodata/abc123-primary.xml.gz"/>
</data>
<data type="filelists">
<location href="repodata/def456-filelists.xml.gz"/>
</data>
<data type="primary_db">
<location href="repodata/ffff-primary.sqlite.bz2"/>
</data>
</repomd>"#;
#[test]
fn primary_location_ignores_primary_db() {
assert_eq!(
primary_location(REPOMD).as_deref(),
Some("repodata/abc123-primary.xml.gz")
);
}
#[test]
fn location_hrefs_in_order() {
assert_eq!(
location_hrefs(REPOMD),
vec![
"repodata/abc123-primary.xml.gz",
"repodata/def456-filelists.xml.gz",
"repodata/ffff-primary.sqlite.bz2",
]
);
}
#[test]
fn location_hrefs_from_primary() {
let primary = r#"<metadata packages="2">
<package type="rpm"><name>a</name>
<location href="Packages/a/a-1.0-1.x86_64.rpm"/>
</package>
<package type="rpm"><name>b</name>
<location href="Packages/b/b-2.0-1.noarch.rpm"/>
</package>
</metadata>"#;
assert_eq!(
location_hrefs(primary),
vec![
"Packages/a/a-1.0-1.x86_64.rpm",
"Packages/b/b-2.0-1.noarch.rpm"
]
);
}
#[test]
fn no_primary_in_empty_repomd() {
assert_eq!(primary_location("<repomd></repomd>"), None);
}
#[tokio::test]
async fn mirror_warms_repodata_and_packages() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let nora = MockServer::start().await;
let repomd = r#"<repomd>
<data type="primary"><location href="repodata/primary.xml"/></data>
<data type="filelists"><location href="repodata/filelists.xml"/></data>
</repomd>"#;
let primary = r#"<metadata>
<package><location href="Packages/a-1.0-1.x86_64.rpm"/></package>
<package><location href="Packages/b-1.0-1.aarch64.rpm"/></package>
</metadata>"#;
for (p, body) in [
("/rpm/fedora/repodata/repomd.xml", repomd),
("/rpm/fedora/repodata/primary.xml", primary),
("/rpm/fedora/repodata/filelists.xml", "<filelists/>"),
("/rpm/fedora/Packages/a-1.0-1.x86_64.rpm", "rpm-a"),
] {
Mock::given(method("GET"))
.and(path(p))
.respond_with(ResponseTemplate::new(200).set_body_string(body))
.mount(&nora)
.await;
}
let client = reqwest::Client::new();
let result = run_rpm_mirror(
&client,
&nora.uri(),
"fedora",
Some(&["x86_64".to_string()]),
4,
)
.await
.unwrap();
assert_eq!(result.total, 2);
assert_eq!(result.fetched, 2);
assert_eq!(result.failed, 0);
}
}