1use std::{fs::File, path::Path};
10
11use tracing::debug;
12use zsync_rs::{checksum::calc_sha1_stream, ControlFile, HttpClient, ZsyncAssembly};
13
14use crate::{error::DownloadError, types::Progress};
15
16#[derive(Debug, Clone)]
18pub struct ZsyncTarget {
19 pub sha1: Option<String>,
21 pub length: u64,
23 pub filename: Option<String>,
25 pub mtime: Option<String>,
27 pub urls: Vec<String>,
30}
31
32impl ZsyncTarget {
33 pub fn artifact_url(&self, control_url: &str) -> Option<String> {
39 let base = control_url.rsplit_once('/').map(|(dir, _)| dir)?;
40 match self.urls.first() {
41 Some(url) if url.starts_with("http://") || url.starts_with("https://") => {
42 Some(url.clone())
43 }
44 Some(url) => Some(format!("{base}/{url}")),
45 None => control_url.strip_suffix(".zsync").map(str::to_string),
46 }
47 }
48}
49
50impl From<ControlFile> for ZsyncTarget {
51 fn from(control: ControlFile) -> Self {
52 Self {
53 sha1: control.sha1,
54 length: control.length,
55 filename: control.filename,
56 mtime: control.mtime,
57 urls: control.urls,
58 }
59 }
60}
61
62pub fn feed_beside(artifact_url: &str) -> Option<String> {
67 let feed = format!("{artifact_url}.zsync");
68 crate::http::Http::head(&feed).ok().map(|_| feed)
69}
70
71pub fn fetch_target(url: &str) -> Result<ZsyncTarget, DownloadError> {
73 let http = HttpClient::new();
74 let control = http
75 .fetch_control_file(url)
76 .map_err(|e| DownloadError::Zsync(format!("fetching zsync control file: {e}")))?;
77 Ok(control.into())
78}
79
80pub fn file_sha1(path: impl AsRef<Path>) -> Result<String, DownloadError> {
83 let mut file = File::open(path)?;
84 let digest = calc_sha1_stream(&mut file)?;
85 Ok(digest.iter().map(|b| format!("{b:02x}")).collect())
86}
87
88pub fn differs_from(target: &ZsyncTarget, installed: impl AsRef<Path>) -> bool {
93 let Some(ref remote) = target.sha1 else {
94 return true;
95 };
96 match file_sha1(installed) {
97 Ok(local) => !local.eq_ignore_ascii_case(remote),
98 Err(_) => true,
99 }
100}
101
102pub fn download<F>(
109 url: &str,
110 seed: &Path,
111 output: &Path,
112 on_progress: Option<F>,
113) -> Result<(), DownloadError>
114where
115 F: Fn(Progress) + Send + Sync + 'static,
116{
117 let mut assembly = ZsyncAssembly::from_url(url, output)
118 .map_err(|e| DownloadError::Zsync(format!("reading zsync control file: {e}")))?;
119
120 if let Some(callback) = on_progress {
121 let total = 0;
122 callback(Progress::Starting {
123 total,
124 });
125 assembly.set_progress_callback(move |done, total| {
126 callback(Progress::Chunk {
127 total,
128 current: done,
129 });
130 });
131 }
132
133 if seed.exists() {
136 assembly
137 .submit_source_file(seed)
138 .map_err(|e| DownloadError::Zsync(format!("reading {}: {e}", seed.display())))?;
139 let (reused, total) = assembly.block_stats();
140 debug!("zsync: {reused}/{total} blocks taken from the installed copy");
141 }
142
143 while !assembly.is_complete() {
144 let fetched = assembly
145 .download_missing_blocks()
146 .map_err(|e| DownloadError::Zsync(format!("fetching blocks: {e}")))?;
147 if fetched == 0 {
150 return Err(DownloadError::Zsync(
151 "zsync transfer stalled with blocks still missing".to_string(),
152 ));
153 }
154 }
155
156 assembly
157 .complete()
158 .map_err(|e| DownloadError::Zsync(format!("verifying zsync result: {e}")))
159}