Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 168 additions & 1 deletion src-tauri/src/binaries/binaries_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
use anyhow::{Error, anyhow};
use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use tari_common::configuration::Network;
use tari_shutdown::Shutdown;
use tauri_plugin_sentry::sentry;
Expand Down Expand Up @@ -448,6 +452,24 @@ impl BinaryManager {
self.selected_version.clone()
}

pub fn get_binary_folder(&self) -> Result<PathBuf, Error> {
self.adapter.get_binary_folder()
}

pub async fn cleanup_old_binary_versions(
&self,
retained_versions: Vec<String>,
) -> Result<Vec<PathBuf>, Error> {
let binary_folder = self.get_binary_folder()?;
let binary_name = self.binary_name.clone();

tokio::task::spawn_blocking(move || {
cleanup_old_binary_versions(&binary_folder, &retained_versions, &binary_name)
})
.await
.map_err(|error| anyhow!("Old binary cleanup task failed: {error:?}"))?
}

pub fn get_base_dir(&self) -> Result<PathBuf, Error> {
let selected_version = self.selected_version.clone();
let binary_folder_path = self.adapter.get_binary_folder()?;
Expand Down Expand Up @@ -491,3 +513,148 @@ fn check_binary_exists(path: &std::path::Path) -> bool {
false
})
}

fn cleanup_old_binary_versions(
binary_folder: &Path,
retained_versions: &[String],
binary_name: &str,
) -> Result<Vec<PathBuf>, Error> {
if !binary_folder.try_exists()? {
return Ok(Vec::new());
}

let mut removed_paths = Vec::new();
for entry in fs::read_dir(binary_folder)? {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
warn!(
target: LOG_TARGET_APP_LOGIC,
"Unable to read binary folder entry for {binary_name}. Error: {error:?}"
);
continue;
}
};

let file_type = match entry.file_type() {
Ok(file_type) => file_type,
Err(error) => {
warn!(
target: LOG_TARGET_APP_LOGIC,
"Unable to read binary folder entry type for {binary_name}. Path: {:?}, Error: {error:?}",
entry.path()
);
continue;
}
};

if !file_type.is_dir() {
continue;
}

let folder_name = entry.file_name();
let folder_name = folder_name.to_string_lossy();
if retained_versions
.iter()
.any(|version| version == folder_name.as_ref())
|| !is_binary_version_folder_name(&folder_name)
{
continue;
}

let path = entry.path();
match fs::remove_dir_all(&path) {
Ok(()) => {
info!(
target: LOG_TARGET_APP_LOGIC,
"Removed old binary version folder for {binary_name}: {}",
path.display()
);
removed_paths.push(path);
}
Err(error) => {
warn!(
target: LOG_TARGET_APP_LOGIC,
"Failed to remove old binary version folder for {binary_name}: {}. Error: {error:?}",
path.display()
);
}
}
}

Ok(removed_paths)
}

fn is_binary_version_folder_name(folder_name: &str) -> bool {
let version = folder_name
.strip_prefix('v')
.or_else(|| folder_name.strip_prefix('V'))
.unwrap_or(folder_name);

version.contains('.')
&& version.chars().next().is_some_and(|c| c.is_ascii_digit())
&& version
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '+'))
}

#[cfg(test)]
mod tests {
use super::{cleanup_old_binary_versions, is_binary_version_folder_name};
use tempfile::tempdir;

#[test]
fn cleanup_old_binary_versions_keeps_retained_versions() {
let temp_dir = tempdir().expect("create temp dir");
let binaries_dir = temp_dir.path();
let current_dir = binaries_dir.join("1.2.3");
let shared_current_dir = binaries_dir.join("1.2.2");
let another_old_dir = binaries_dir.join("1.2.1");
let cache_dir = binaries_dir.join("download-cache");
let checksum_file = binaries_dir.join("latest.sha256");

std::fs::create_dir_all(&current_dir).expect("create current version dir");
std::fs::create_dir_all(&shared_current_dir).expect("create shared current version dir");
std::fs::create_dir_all(&another_old_dir).expect("create another old version dir");
std::fs::create_dir_all(&cache_dir).expect("create non-version dir");
std::fs::write(&checksum_file, "checksum").expect("create sibling file");

let retained_versions = vec!["1.2.3".to_string(), "1.2.2".to_string()];
let removed_paths = cleanup_old_binary_versions(
binaries_dir,
&retained_versions,
"test-binary",
)
.expect("cleanup succeeds");

assert!(current_dir.exists());
assert!(shared_current_dir.exists());
assert!(cache_dir.exists());
assert!(checksum_file.exists());
assert!(!another_old_dir.exists());
assert_eq!(removed_paths.len(), 1);
}

#[test]
fn cleanup_old_binary_versions_ignores_missing_parent() {
let temp_dir = tempdir().expect("create temp dir");
let missing_dir = temp_dir.path().join("missing");
let retained_versions = vec!["1.2.3".to_string()];

let removed_paths =
cleanup_old_binary_versions(&missing_dir, &retained_versions, "test-binary")
.expect("cleanup succeeds");

assert!(removed_paths.is_empty());
}

#[test]
fn is_binary_version_folder_name_only_accepts_version_like_names() {
assert!(is_binary_version_folder_name("5.2.1"));
assert!(is_binary_version_folder_name("v6.25.0"));
assert!(is_binary_version_folder_name("1.0.0-rc.1"));
assert!(!is_binary_version_folder_name("download-cache"));
assert!(!is_binary_version_folder_name("2026-backup"));
assert!(!is_binary_version_folder_name("latest"));
}
}
56 changes: 55 additions & 1 deletion src-tauri/src/binaries/binaries_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::LOG_TARGET_APP_LOGIC;
use crate::progress_trackers::progress_stepper::IncrementalProgressTracker;
use anyhow::{Error, anyhow};
use async_trait::async_trait;
use log::debug;
use log::{debug, warn};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::LazyLock;
Expand Down Expand Up @@ -263,6 +263,7 @@ impl BinaryResolver {

if manager.check_if_files_for_version_exist() {
// If files already exist, we can skip the download
self.cleanup_old_binary_versions(manager).await;
return Ok(());
}

Expand All @@ -277,6 +278,7 @@ impl BinaryResolver {
let _lock = TARI_SUITE_DOWNLOAD_LOCK.lock().await;

if manager.check_if_files_for_version_exist() {
self.cleanup_old_binary_versions(manager).await;
return Ok(());
}
manager
Expand All @@ -288,6 +290,8 @@ impl BinaryResolver {
.await?;
}

self.cleanup_old_binary_versions(manager).await;

Ok(())
}

Expand All @@ -297,4 +301,54 @@ impl BinaryResolver {
.unwrap_or_else(|| panic!("Couldn't find manager for binary: {}", binary.name()))
.get_selected_version()
}

async fn cleanup_old_binary_versions(&self, manager: &BinaryManager) {
let retained_versions = self.retained_versions_for_binary_folder(manager);

match manager.cleanup_old_binary_versions(retained_versions).await {
Ok(removed_paths) => {
if !removed_paths.is_empty() {
debug!(
target: LOG_TARGET_APP_LOGIC,
"Removed {} old binary version folder(s)",
removed_paths.len()
);
}
}
Err(error) => {
warn!(
target: LOG_TARGET_APP_LOGIC,
"Old binary version cleanup failed. Error: {error:?}"
);
}
}
}

fn retained_versions_for_binary_folder(&self, manager: &BinaryManager) -> Vec<String> {
let binary_folder = match manager.get_binary_folder() {
Ok(path) => path,
Err(error) => {
warn!(
target: LOG_TARGET_APP_LOGIC,
"Unable to get binary folder for retained version lookup. Error: {error:?}"
);
return vec![manager.get_selected_version()];
}
};

self.managers
.values()
.filter_map(|candidate| match candidate.get_binary_folder() {
Ok(folder) if folder == binary_folder => Some(candidate.get_selected_version()),
Ok(_) => None,
Err(error) => {
warn!(
target: LOG_TARGET_APP_LOGIC,
"Unable to inspect binary folder while collecting retained versions. Error: {error:?}"
);
None
}
})
.collect()
}
}