use std::{fs::File, path::Path};
use tracing::debug;
use zsync_rs::{checksum::calc_sha1_stream, ControlFile, HttpClient, ZsyncAssembly};
use crate::{error::DownloadError, types::Progress};
#[derive(Debug, Clone)]
pub struct ZsyncTarget {
pub sha1: Option<String>,
pub length: u64,
pub filename: Option<String>,
pub mtime: Option<String>,
pub urls: Vec<String>,
}
impl ZsyncTarget {
pub fn artifact_url(&self, control_url: &str) -> Option<String> {
let base = control_url.rsplit_once('/').map(|(dir, _)| dir)?;
match self.urls.first() {
Some(url) if url.starts_with("http://") || url.starts_with("https://") => {
Some(url.clone())
}
Some(url) => Some(format!("{base}/{url}")),
None => control_url.strip_suffix(".zsync").map(str::to_string),
}
}
}
impl From<ControlFile> for ZsyncTarget {
fn from(control: ControlFile) -> Self {
Self {
sha1: control.sha1,
length: control.length,
filename: control.filename,
mtime: control.mtime,
urls: control.urls,
}
}
}
pub fn feed_beside(artifact_url: &str) -> Option<String> {
let feed = format!("{artifact_url}.zsync");
crate::http::Http::head(&feed).ok().map(|_| feed)
}
pub fn fetch_target(url: &str) -> Result<ZsyncTarget, DownloadError> {
let http = HttpClient::new();
let control = http
.fetch_control_file(url)
.map_err(|e| DownloadError::Zsync(format!("fetching zsync control file: {e}")))?;
Ok(control.into())
}
pub fn file_sha1(path: impl AsRef<Path>) -> Result<String, DownloadError> {
let mut file = File::open(path)?;
let digest = calc_sha1_stream(&mut file)?;
Ok(digest.iter().map(|b| format!("{b:02x}")).collect())
}
pub fn differs_from(target: &ZsyncTarget, installed: impl AsRef<Path>) -> bool {
let Some(ref remote) = target.sha1 else {
return true;
};
match file_sha1(installed) {
Ok(local) => !local.eq_ignore_ascii_case(remote),
Err(_) => true,
}
}
pub fn download<F>(
url: &str,
seed: &Path,
output: &Path,
on_progress: Option<F>,
) -> Result<(), DownloadError>
where
F: Fn(Progress) + Send + Sync + 'static,
{
let mut assembly = ZsyncAssembly::from_url(url, output)
.map_err(|e| DownloadError::Zsync(format!("reading zsync control file: {e}")))?;
if let Some(callback) = on_progress {
let total = 0;
callback(Progress::Starting {
total,
});
assembly.set_progress_callback(move |done, total| {
callback(Progress::Chunk {
total,
current: done,
});
});
}
if seed.exists() {
assembly
.submit_source_file(seed)
.map_err(|e| DownloadError::Zsync(format!("reading {}: {e}", seed.display())))?;
let (reused, total) = assembly.block_stats();
debug!("zsync: {reused}/{total} blocks taken from the installed copy");
}
while !assembly.is_complete() {
let fetched = assembly
.download_missing_blocks()
.map_err(|e| DownloadError::Zsync(format!("fetching blocks: {e}")))?;
if fetched == 0 {
return Err(DownloadError::Zsync(
"zsync transfer stalled with blocks still missing".to_string(),
));
}
}
assembly
.complete()
.map_err(|e| DownloadError::Zsync(format!("verifying zsync result: {e}")))
}