Skip to main content

pacenotes_installer/
package_download.rs

1//! Fetching a package into the cache. A payload enters the cache only after its
2//! sha256 matched the manifest, so nothing downstream verifies it again.
3
4/// The waits before the second and third attempts at one package.
5const RETRY_DELAYS_MS: [u64; 2] = [1_000, 3_000];
6/// How often a wait looks at Cancel.
7const CANCEL_POLL_MS: u64 = 100;
8
9/// One attempt's failure, and whether another attempt could succeed.
10struct Failure {
11    error: crate::http::HttpError,
12    retry: bool,
13}
14
15impl Failure {
16    fn transfer(error: crate::http::HttpError) -> Failure {
17        let retry = error.is_transient();
18        Failure { error, retry }
19    }
20
21    /// A full or locked disk is not the network's fault, and waiting will not fix it.
22    fn local(context: &str, error: std::io::Error) -> Failure {
23        Failure {
24            error: crate::http::HttpError::parse(format!("{context}: {error}")),
25            retry: false,
26        }
27    }
28}
29
30/// `download_to_cache`, tried again after a failure a retry could fix (see
31/// `HttpError::is_transient`, plus a payload that arrived corrupted), waiting
32/// between attempts; `on_retry` hears why, for the run log.
33pub fn download_retrying(
34    api: &crate::api::Api,
35    package: &pacenotes_shared::PackageInfo,
36    cache: &mut pacenotes_installer::cache::Cache,
37    progress: crate::http::ProgressFn,
38    cancel: &std::sync::atomic::AtomicBool,
39    on_retry: &mut dyn FnMut(&crate::http::HttpError, u64),
40) -> Result<(), crate::http::HttpError> {
41    let mut delays = RETRY_DELAYS_MS.iter();
42    loop {
43        let failure = match download_to_cache(api, package, cache, &mut *progress, cancel) {
44            Ok(()) => return Ok(()),
45            Err(failure) => failure,
46        };
47        let delay = match delays.next() {
48            Some(&delay) if failure.retry && !failure.error.is_cancelled() => delay,
49            _ => return Err(failure.error),
50        };
51        on_retry(&failure.error, delay);
52        wait(delay, cancel)?;
53    }
54}
55
56/// Sleeps `milliseconds`, giving up as soon as Cancel is pressed.
57fn wait(
58    milliseconds: u64,
59    cancel: &std::sync::atomic::AtomicBool,
60) -> Result<(), crate::http::HttpError> {
61    let mut waited = 0;
62    while waited < milliseconds {
63        if cancel.load(std::sync::atomic::Ordering::Relaxed) {
64            return Err(crate::http::HttpError::cancelled());
65        }
66        std::thread::sleep(std::time::Duration::from_millis(CANCEL_POLL_MS));
67        waited += CANCEL_POLL_MS;
68    }
69    Ok(())
70}
71
72/// Streams `package` to a temp file while hashing it, then promotes the file
73/// to `<sha>.zip` and records the entry; any failure deletes the temp file.
74fn download_to_cache(
75    api: &crate::api::Api,
76    package: &pacenotes_shared::PackageInfo,
77    cache: &mut pacenotes_installer::cache::Cache,
78    progress: crate::http::ProgressFn,
79    cancel: &std::sync::atomic::AtomicBool,
80) -> Result<(), Failure> {
81    std::fs::create_dir_all(cache.root())
82        .map_err(|e| Failure::local("cannot create the download cache", e))?;
83    // Named per package; two installer instances are kept apart by the single-instance check.
84    let temp = cache.root().join(format!(
85        "{}.part",
86        pacenotes_installer::sha256::hex_digest(package.download_path().as_bytes())
87    ));
88
89    let outcome = fetch_verified(api, package, &temp, progress, cancel)
90        .and_then(|()| promote(cache, package, &temp));
91    if outcome.is_err() {
92        let _ = std::fs::remove_file(&temp);
93    }
94    outcome
95}
96
97fn fetch_verified(
98    api: &crate::api::Api,
99    package: &pacenotes_shared::PackageInfo,
100    temp: &std::path::Path,
101    progress: crate::http::ProgressFn,
102    cancel: &std::sync::atomic::AtomicBool,
103) -> Result<(), Failure> {
104    let mut file =
105        std::fs::File::create(temp).map_err(|e| Failure::local("cannot write to the cache", e))?;
106    let mut hasher = pacenotes_installer::sha256::Sha256::new();
107    let mut write_error: Option<std::io::Error> = None;
108
109    let transfer = crate::http::get_streaming(
110        &api.download_url(package),
111        api.token.as_deref(),
112        &mut |chunk| {
113            hasher.update(chunk);
114            std::io::Write::write_all(&mut file, chunk)
115                .map_err(|e| write_error = Some(e))
116                .is_ok()
117        },
118        progress,
119        cancel,
120    );
121    // A full disk must not be reported as a network failure.
122    if let Some(e) = write_error {
123        return Err(Failure::local("cannot write to the cache", e));
124    }
125    transfer.map_err(Failure::transfer)?;
126
127    let received = pacenotes_installer::sha256::hex(&hasher.finalize());
128    if received != package.sha256 {
129        // Damaged on the way, which the next attempt may well not be.
130        return Err(Failure {
131            error: crate::http::HttpError::parse(format!(
132                "{} arrived corrupted: expected sha256 {}, got {received}",
133                package.label(),
134                package.sha256
135            )),
136            retry: true,
137        });
138    }
139    Ok(())
140}
141
142/// Identical bytes another package already stored are kept and the temp file dropped.
143fn promote(
144    cache: &mut pacenotes_installer::cache::Cache,
145    package: &pacenotes_shared::PackageInfo,
146    temp: &std::path::Path,
147) -> Result<(), Failure> {
148    let payload = cache.payload_path(&package.sha256);
149    if payload.exists() {
150        let _ = std::fs::remove_file(temp);
151    } else {
152        std::fs::rename(temp, &payload)
153            .map_err(|e| Failure::local("cannot move the download into the cache", e))?;
154    }
155    cache
156        .insert(pacenotes_installer::tree::CachedPackage::of(package))
157        .map_err(|e| Failure::local("cannot update the cache index", e))
158}