// SPDX-FileCopyrightText: Matteo Settenvini // SPDX-License-Identifier: EUPL-1.2 mod dso; mod list; use crate::{ args::Args, decision::{Action, Decision}, }; use anyhow::Result; use async_trait::async_trait; use dso::DsoCleaner; use list::ListCleaner; use nix::libc::EXDEV; use std::{ collections::HashMap, io, path::{Path, PathBuf}, }; use tokio::{ sync::{broadcast, mpsc}, task::JoinSet, }; use walkdir::{DirEntry, WalkDir}; #[async_trait] pub trait Cleaner { async fn run( &mut self, mut files: broadcast::Receiver, decisions: mpsc::Sender, ) -> Result<()>; } type Cleaners = Vec>; type RemovalFn = Box io::Result<()>>; pub struct Runner { cleaners: Cleaners, removal_fn: RemovalFn, } const CHANNEL_SIZE: usize = 100; const CHANNEL_MAX_LOAD: usize = CHANNEL_SIZE * 3 / 4; impl Runner { pub fn new(args: Args) -> Self { let removal_fn = Self::new_removal_fn(&args); let mut cleaners: Cleaners = vec![]; cleaners.push(Box::new(DsoCleaner::new(args.output_dotfile))); if let Some(wl) = args.allowlist { cleaners.push(Box::new(ListCleaner::new(Action::Keep, wl))); } if let Some(bl) = args.blocklist { cleaners.push(Box::new(ListCleaner::new(Action::Remove, bl))); } Self { cleaners, removal_fn, } } pub async fn run(self) -> Result<()> { let input_tx = broadcast::Sender::new(CHANNEL_SIZE); let (output_tx, output_rx) = mpsc::channel(CHANNEL_SIZE); let mut tasks = JoinSet::new(); // Processors for mut cleaner in self.cleaners { let input_rx = input_tx.subscribe(); let output_tx_clone = output_tx.clone(); tasks.spawn(async move { cleaner.run(input_rx, output_tx_clone).await }); } drop(output_tx); // Producer of inputs (note that this needs to happen // after all channels have been created) tasks.spawn(Self::input_producer(input_tx)); // Output consumer Self::output_consumer(self.removal_fn, output_rx).await; while let Some(task) = tasks.join_next().await { if let Err(err) = task? { log::error!("{}", err); } } Ok(()) } async fn input_producer(input_tx: broadcast::Sender) -> Result<()> { let walker = WalkDir::new(".").follow_links(false); for entry in walker { match entry { Ok(e) if !Self::is_dir(&e) => { if input_tx.len() >= CHANNEL_MAX_LOAD { // TODO: FIXME: make this better, this is a quick hack tokio::time::sleep(std::time::Duration::from_millis(50)).await; } input_tx.send(e.into_path())?; } Ok(_) => continue, Err(err) => log::warn!("unable to access path: {}", err), } } // we handle errors by warning the user, otherwise we always succeed Ok(()) } fn is_dir(entry: &DirEntry) -> bool { let ty = entry.file_type(); if ty.is_dir() { true } else if ty.is_file() { false } else { // it is a symlink match std::fs::metadata(entry.path()) { Ok(metadata) => metadata.is_dir(), Err(e) => { log::debug!( "unable to resolve symlink {}: {}", entry.path().display(), e ); false } } } } async fn output_consumer(removal_fn: RemovalFn, mut output_rx: mpsc::Receiver) { let mut to_remove = HashMap::new(); while let Some(decision) = output_rx.recv().await { match to_remove.get_mut(&decision.path) { Some(prev_action) => { if decision.action == Action::Keep { *prev_action = Action::Keep; } } None => { to_remove.insert(decision.path, decision.action); } } } for (file, action) in to_remove { if action == Action::Remove { if let Err(err) = (removal_fn)(&file) { log::error!("{}: {}", file.display(), err); } } } } fn new_removal_fn(args: &Args) -> RemovalFn { if let Some(dest) = args.split_to.clone() { if args.dry_run { Box::new(move |path| { log::info!( "(dry-run) would move {} to {}", path.display(), dest.display() ); Ok(()) }) } else { Box::new(move |path| { log::info!("moving {} to {}", path.display(), dest.display()); Self::move_preserve(&path, &dest) }) } } else { if args.dry_run { Box::new(|path| { let ty = if path.is_symlink() { "symlink" } else { "regular file" }; log::info!("(dry-run) would remove {} {}", ty, path.display()); Ok(()) }) } else { Box::new(move |path| { log::info!("removing {}", path.display()); std::fs::remove_file(&path) }) } } } fn move_preserve(src: &Path, dest: &Path) -> io::Result<()> { assert!(src.is_relative()); let abs_dest = dest.join(src); if let Some(parent) = abs_dest.parent() { std::fs::create_dir_all(parent)?; } match std::fs::rename(&src, &abs_dest) { Err(err) if err.raw_os_error() == Some(EXDEV) => { log::trace!( "different filesystems, falling back to copying {} to {}", src.display(), abs_dest.display() ); std::fs::copy(src, abs_dest).and_then(|_| std::fs::remove_file(src)) } other => other, } } }