use std::path::{Path, PathBuf};
use std::sync::Arc;
use rtb_vcs::{ReleaseAsset, ReleaseProvider};
use tokio::io::AsyncReadExt as _;
use crate::error::{Result, UpdateError};
use crate::options::{ProgressEvent, ProgressSink, RunOutcome};
pub type SwapFn = Arc<dyn Fn(&Path) -> std::io::Result<()> + Send + Sync + 'static>;
pub type SelfTestFn = Arc<dyn Fn(&Path) -> std::io::Result<String> + Send + Sync + 'static>;
#[must_use]
pub fn default_swap_fn() -> SwapFn {
Arc::new(|src: &Path| self_replace::self_replace(src))
}
#[must_use]
pub fn default_self_test_fn() -> SelfTestFn {
Arc::new(|binary: &Path| {
let output = std::process::Command::new(binary).arg("--version").output()?;
if !output.status.success() {
return Err(std::io::Error::other(format!(
"--version exited with status {}",
output.status
)));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
})
}
pub async fn download_to_file(
provider: &dyn ReleaseProvider,
asset: &ReleaseAsset,
dest: &Path,
progress: Option<&ProgressSink>,
) -> Result<u64> {
let (mut reader, total) = provider.download_asset(asset).await?;
let mut file = tokio::fs::File::create(dest).await?;
let mut buf = vec![0u8; 64 * 1024];
let mut done = 0u64;
loop {
let n = reader.read(&mut buf).await?;
if n == 0 {
break;
}
tokio::io::AsyncWriteExt::write_all(&mut file, &buf[..n]).await?;
done += n as u64;
if let Some(sink) = progress {
sink(ProgressEvent::Downloading { bytes_done: done, bytes_total: total });
}
}
tokio::io::AsyncWriteExt::flush(&mut file).await?;
Ok(done)
}
pub async fn fetch_small_asset(
provider: &dyn ReleaseProvider,
asset: &ReleaseAsset,
) -> Result<Vec<u8>> {
let (mut reader, _) = provider.download_asset(asset).await?;
let mut out = Vec::new();
reader.read_to_end(&mut out).await?;
Ok(out)
}
pub fn extract_binary(src: &Path, dest_dir: &Path, tool_name: &str) -> Result<PathBuf> {
std::fs::create_dir_all(dest_dir)?;
let file_name =
src.file_name().and_then(|n| n.to_str()).unwrap_or_default().to_ascii_lowercase();
#[allow(clippy::case_sensitive_file_extension_comparisons)]
let is_tar_gz = file_name.ends_with(".tar.gz") || file_name.ends_with(".tgz");
#[allow(clippy::case_sensitive_file_extension_comparisons)]
let is_zip = file_name.ends_with(".zip");
if is_tar_gz {
extract_tar_gz(src, dest_dir)?;
} else if is_zip {
extract_zip(src, dest_dir)?;
} else {
return Err(UpdateError::Archive(format!("unsupported archive extension: {file_name}")));
}
let expected_name_unix = tool_name.to_string();
let expected_name_windows = format!("{tool_name}.exe");
for entry in walk_files(dest_dir) {
let name = entry.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name == expected_name_unix || name == expected_name_windows {
return Ok(entry);
}
}
Err(UpdateError::Archive(format!("extracted archive contained no `{tool_name}` binary")))
}
fn extract_tar_gz(src: &Path, dest_dir: &Path) -> Result<()> {
let file = std::fs::File::open(src)?;
let gz = flate2::read::GzDecoder::new(file);
let mut archive = tar::Archive::new(gz);
archive.unpack(dest_dir).map_err(|e| UpdateError::Archive(e.to_string()))
}
fn extract_zip(src: &Path, dest_dir: &Path) -> Result<()> {
let file = std::fs::File::open(src)?;
let mut archive =
zip::ZipArchive::new(file).map_err(|e| UpdateError::Archive(e.to_string()))?;
for i in 0..archive.len() {
let mut entry = archive.by_index(i).map_err(|e| UpdateError::Archive(e.to_string()))?;
let Some(rel_path) = entry.enclosed_name() else {
continue;
};
let out_path = dest_dir.join(rel_path);
if entry.is_dir() {
std::fs::create_dir_all(&out_path)?;
continue;
}
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut out_file = std::fs::File::create(&out_path)?;
std::io::copy(&mut entry, &mut out_file)?;
}
Ok(())
}
fn walk_files(root: &Path) -> Vec<PathBuf> {
let mut stack = vec![root.to_path_buf()];
let mut out = Vec::new();
while let Some(dir) = stack.pop() {
let Ok(read) = std::fs::read_dir(&dir) else {
continue;
};
for entry in read.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else {
out.push(path);
}
}
}
out
}
pub fn mark_executable(path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mut perm = std::fs::metadata(path)?.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(path, perm)?;
}
#[cfg(not(unix))]
let _ = path;
Ok(())
}
#[must_use]
pub const fn dry_run_outcome(
from: semver::Version,
to: semver::Version,
bytes: u64,
staged_at: PathBuf,
) -> RunOutcome {
RunOutcome {
from_version: from,
to_version: to,
bytes,
swapped: false,
staged_at: Some(staged_at),
}
}
#[must_use]
pub const fn swap_outcome(from: semver::Version, to: semver::Version, bytes: u64) -> RunOutcome {
RunOutcome { from_version: from, to_version: to, bytes, swapped: true, staged_at: None }
}
#[must_use]
pub fn parse_release_tag(tag: &str) -> Option<semver::Version> {
let stripped = tag.strip_prefix(['v', 'V']).unwrap_or(tag);
semver::Version::parse(stripped).ok()
}
pub fn cache_dir_for(tool_name: &str, version: &str) -> PathBuf {
let base = directories::ProjectDirs::from("", "", tool_name)
.map_or_else(std::env::temp_dir, |p| p.cache_dir().to_path_buf());
base.join("update").join(version)
}