use std::collections::{HashMap, HashSet};
use std::fs;
use crate::lfs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use color_eyre::Result;
use color_eyre::eyre::WrapErr;
use crate::models::*;
use crate::dirs::*;
use crate::io;
use crate::utils;
use crate::deinit::force_remove_dir_all_with_progress;
#[derive(Debug)]
pub struct GcPlan {
pub old_downloads: Option<Vec<PathBuf>>,
pub unused_channels: Vec<PathBuf>,
pub unused_packages: Vec<PathBuf>,
pub old_unpack_dirs: Vec<PathBuf>,
pub old_aur_build_dirs: Vec<PathBuf>,
}
impl GcPlan {
pub fn new() -> Self {
Self {
old_downloads: None,
unused_channels: Vec::new(),
unused_packages: Vec::new(),
old_unpack_dirs: Vec::new(),
old_aur_build_dirs: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.old_downloads.as_ref().map_or(true, |v| v.is_empty()) &&
self.unused_channels.is_empty() &&
self.unused_packages.is_empty() &&
self.old_unpack_dirs.is_empty() &&
self.old_aur_build_dirs.is_empty()
}
}
pub fn gc_epkg(old_downloads_days: Option<u64>) -> Result<()> {
let mut plan = GcPlan::new();
if let Some(days) = old_downloads_days {
plan.old_downloads = Some(collect_old_downloads(days)?);
} else {
let (in_use_channels, in_use_packages) = collect_in_use_resources()?;
plan.unused_channels = collect_unused_channels(&in_use_channels)?;
plan.unused_packages = collect_unused_packages(&in_use_packages)?;
plan.old_unpack_dirs = collect_old_unpack_dirs()?;
plan.old_aur_build_dirs = collect_old_aur_build_dirs()?;
}
if plan.is_empty() {
println!("No cleanup required.");
return Ok(());
}
display_gc_plan(&plan, old_downloads_days.is_some())?;
if !utils::user_prompt_and_confirm()? {
println!("Garbage collection cancelled by user.");
return Ok(());
}
execute_gc_plan(&plan)?;
println!("Garbage collection completed successfully.");
Ok(())
}
fn collect_old_downloads(days: u64) -> Result<Vec<PathBuf>> {
let downloads_dir = dirs().epkg_downloads_cache.clone();
if !lfs::exists_on_host(&downloads_dir) {
return Ok(Vec::new());
}
let cutoff_time = if days == 0 {
SystemTime::now()
} else {
SystemTime::now() - Duration::from_secs(days * 24 * 60 * 60)
};
let mut old_files = Vec::new();
if let Ok(entries) = fs::read_dir(&downloads_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Ok(metadata) = lfs::metadata_on_host(&path) {
if let Ok(modified_time) = metadata.modified() {
if modified_time < cutoff_time {
old_files.push(path);
}
}
}
}
}
}
Ok(old_files)
}
fn collect_in_use_resources() -> Result<(HashSet<String>, HashSet<String>)> {
let mut in_use_channels = HashSet::new();
let mut in_use_packages = HashSet::new();
let shared_store = config().init.shared_store;
let user_envs = dirs_ref().user_envs.clone();
walk_environments(shared_store, &user_envs, |env_path, _owner| {
collect_env_configs(env_path, &mut in_use_channels, &mut in_use_packages)
})?;
if config().common.verbose {
println!("In-use channels: {:?}", in_use_channels);
println!("In-use packages: {:#?}", in_use_packages);
}
Ok((in_use_channels, in_use_packages))
}
fn collect_env_configs(
env_path: &Path,
in_use_channels: &mut HashSet<String>,
in_use_packages: &mut HashSet<String>
) -> Result<()> {
let channel_config_path = env_root_channel_yaml(&env_path);
if lfs::exists_in_env(&channel_config_path) {
if let Ok(mut channel_config) = io::read_yaml_file::<ChannelConfig>(&channel_config_path) {
if let Ok(()) = crate::io::set_channel_config_defaults(&mut channel_config, None) {
if config().common.verbose {
println!("Found channel config: {} -> {}", env_path.display(), channel_config.channel);
}
in_use_channels.insert(channel_config.channel);
}
}
}
let env_config_path = env_root_env_yaml(env_path);
if lfs::exists_in_env(&env_config_path) {
if io::read_yaml_file::<EnvConfig>(&env_config_path).is_ok() {
let packages_path = env_path.join("generations").join("current").join("installed-packages.json");
if lfs::exists_in_env(&packages_path) {
if let Ok(packages_raw) = crate::io::read_json_file::<HashMap<String, InstalledPackageInfo>>(&packages_path) {
let packages: InstalledPackagesMap = packages_raw.into_iter().map(|(k, v)| (k, std::sync::Arc::new(v))).collect();
for (_pkgkey, pkg_info) in &packages {
in_use_packages.insert(pkg_info.pkgline.clone());
}
}
}
}
}
Ok(())
}
fn collect_unused_channels(in_use_channels: &HashSet<String>) -> Result<Vec<PathBuf>> {
let channels_cache_dir = dirs().epkg_channels_cache.clone();
if !channels_cache_dir.exists() {
return Ok(Vec::new());
}
let mut unused_channels = Vec::new();
let mut all_channels = Vec::new();
walk_bottom_dir(&channels_cache_dir, None, &mut |channel_path, _owner| {
let channel_name = channel_path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("");
all_channels.push(channel_name.to_string());
if !in_use_channels.contains(channel_name) {
unused_channels.push(channel_path.to_path_buf());
}
Ok(())
})?;
if config().common.verbose {
println!("All channel directories: {:?}", all_channels);
println!("Unused channel directories: {}", unused_channels.len());
}
Ok(unused_channels)
}
fn collect_unused_packages(in_use_packages: &HashSet<String>) -> Result<Vec<PathBuf>> {
let store_dir = dirs().epkg_store.clone();
if !store_dir.exists() {
return Ok(Vec::new());
}
let mut unused_packages = Vec::new();
walk_bottom_dir(&store_dir, None, &mut |package_path, _owner| {
let package_name = package_path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("");
if !in_use_packages.contains(package_name) {
unused_packages.push(package_path.to_path_buf());
}
Ok(())
})?;
Ok(unused_packages)
}
fn collect_old_items_in_dir(dir: &Path, min_age_seconds: u64) -> Result<Vec<PathBuf>> {
if !dir.exists() {
return Ok(Vec::new());
}
let mut old_items = Vec::new();
let cutoff_time = SystemTime::now() - Duration::from_secs(min_age_seconds);
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let item_path = entry.path();
if let Ok(metadata) = lfs::metadata_on_host(&item_path) {
if let Ok(modified_time) = metadata.modified() {
if modified_time < cutoff_time {
old_items.push(item_path);
}
}
}
}
}
Ok(old_items)
}
fn collect_old_unpack_dirs() -> Result<Vec<PathBuf>> {
let unpack_dir = crate::dirs::unpack_basedir();
collect_old_items_in_dir(&unpack_dir, 3600)
}
fn collect_old_aur_build_dirs() -> Result<Vec<PathBuf>> {
let aur_builds_dir = dirs().user_aur_builds.clone();
collect_old_items_in_dir(&aur_builds_dir, 3600 * 24)
}
fn display_gc_plan(plan: &GcPlan, is_old_downloads: bool) -> Result<()> {
println!("\n=== Epkg Garbage Collection Plan ===");
if is_old_downloads {
if let Some(ref downloads) = plan.old_downloads {
if !downloads.is_empty() {
println!("\nOld download files to remove:");
let total_size = downloads.iter()
.map(|p| lfs::metadata_on_host(p).map(|m| m.len()).unwrap_or(0))
.sum::<u64>();
println!(" Directory: {}", dirs().epkg_downloads_cache.display());
println!(" Files: {}", downloads.len());
println!(" Total size: {}", utils::format_size(total_size));
if config().common.verbose {
for file in downloads.iter().take(10) {
println!(" {}", file.display());
}
if downloads.len() > 10 {
println!(" ... and {} more files", downloads.len() - 10);
}
}
} else {
println!("\nNo old download files found.");
}
}
} else {
if !plan.unused_channels.is_empty() {
println!("\nUnused directories to remove under: {}", dirs().epkg_channels_cache.display());
let mut unused_channel_names: Vec<String> = plan.unused_channels
.iter()
.map(|dir| dir.file_name()
.map(|f| f.to_string_lossy().to_string())
.unwrap_or_else(|| dir.display().to_string()))
.collect();
unused_channel_names.sort();
println!(
" Directories: {} [{}]",
plan.unused_channels.len(),
unused_channel_names.join(" ")
);
if config().common.verbose {
for dir in &plan.unused_channels {
println!(" {}", dir.display());
}
}
}
if !plan.unused_packages.is_empty() {
println!("\nUnused directories to remove under: {}", dirs().epkg_store.display());
println!(" Directories: {}", plan.unused_packages.len());
if config().common.verbose {
for dir in &plan.unused_packages {
println!(" {}", dir.display());
}
}
}
if !plan.old_unpack_dirs.is_empty() {
println!("\nUnused directories to remove under: {}", crate::dirs::unpack_basedir().display());
println!(" Directories: {}", plan.old_unpack_dirs.len());
if config().common.verbose {
for dir in &plan.old_unpack_dirs {
println!(" {}", dir.display());
}
}
}
if !plan.old_aur_build_dirs.is_empty() {
println!("\nStale files/directories to remove under: {}", dirs().user_aur_builds.display());
println!(" Items: {}", plan.old_aur_build_dirs.len());
if config().common.verbose {
for item in &plan.old_aur_build_dirs {
println!(" {}", item.display());
}
}
}
}
if plan.is_empty() {
println!("No cleanup required.");
} else {
println!();
}
Ok(())
}
fn execute_gc_plan(plan: &GcPlan) -> Result<()> {
if let Some(ref downloads) = plan.old_downloads {
for file in downloads {
if file.exists() {
println!("Removing old download file: {}", file.display());
lfs::remove_file(file)?;
}
}
}
for dir in &plan.unused_channels {
if dir.exists() {
println!("Removing unused channel directory: {}", dir.display());
force_remove_dir_all_with_progress(dir)
.wrap_err_with(|| format!("Failed to remove directory: {}", dir.display()))?;
}
}
for dir in &plan.unused_packages {
if dir.exists() {
println!("Removing unused package directory: {}", dir.display());
force_remove_dir_all_with_progress(dir)
.wrap_err_with(|| format!("Failed to remove directory: {}", dir.display()))?;
}
}
for dir in &plan.old_unpack_dirs {
if dir.exists() {
println!("Removing unused unpack directory: {}", dir.display());
force_remove_dir_all_with_progress(dir)
.wrap_err_with(|| format!("Failed to remove directory: {}", dir.display()))?;
}
}
for item in &plan.old_aur_build_dirs {
if item.exists() {
if item.is_dir() {
println!("Removing stale AUR build directory: {}", item.display());
force_remove_dir_all_with_progress(item)
.wrap_err_with(|| format!("Failed to remove directory: {}", item.display()))?;
} else {
println!("Removing stale AUR build log: {}", item.display());
lfs::remove_file(item)?;
}
}
}
Ok(())
}