Skip to main content

omne_cli/
fetch.rs

1//! Download orchestration: GitHub release → progress bar → safe extraction.
2//!
3//! This is a thin glue layer (~30 lines of logic) that wires
4//! `github::release_asset_body` → `indicatif::ProgressBar::wrap_read` →
5//! `tarball::extract_safe`. It has no test file of its own — its behavior
6//! is exercised by the integration tests in Units 8b and 9.
7
8#![allow(dead_code)]
9
10use std::path::Path;
11
12use indicatif::{ProgressBar, ProgressStyle};
13
14use crate::error::CliError;
15use crate::github::GithubClient;
16use crate::tarball;
17
18/// Download a release tarball and extract it safely into `target`.
19///
20/// After extraction, verifies that `target/expected_top_level` exists as
21/// a directory. If not, returns `CliError::TarballLayoutMismatch` —
22/// an early-warning signal that the upstream release workflow changed
23/// its tarball structure.
24pub fn download_and_extract(
25    client: &GithubClient,
26    org: &str,
27    repo: &str,
28    tag: &str,
29    target: &Path,
30    expected_top_level: &str,
31) -> Result<(), CliError> {
32    let (content_length, body_reader) = client.release_asset_body(org, repo, tag)?;
33
34    let bar = ProgressBar::new(content_length);
35    bar.set_style(
36        ProgressStyle::default_bar()
37            .template("{msg} [{bar:30.cyan/dim}] {bytes}/{total_bytes} ({eta})")
38            .unwrap()
39            .progress_chars("█▓░"),
40    );
41    bar.set_message(format!("Downloading {org}/{repo} {tag}"));
42
43    let wrapped = bar.wrap_read(body_reader);
44    tarball::extract_safe(wrapped, target)?;
45    bar.finish_with_message("downloaded");
46
47    // Verify the tarball produced the expected top-level directory.
48    let expected_dir = target.join(expected_top_level);
49    if !expected_dir.is_dir() {
50        return Err(CliError::TarballLayoutMismatch {
51            expected: expected_top_level.to_string(),
52            found: list_top_level_entries(target),
53        });
54    }
55
56    Ok(())
57}
58
59/// List top-level directory entries for the layout mismatch error message.
60fn list_top_level_entries(dir: &Path) -> Vec<String> {
61    std::fs::read_dir(dir)
62        .ok()
63        .map(|entries| {
64            entries
65                .filter_map(|e| e.ok())
66                .map(|e| e.file_name().to_string_lossy().into_owned())
67                .collect()
68        })
69        .unwrap_or_default()
70}