sysroot-cleaner/src/cleaners.rs

213 lines
6.4 KiB
Rust

// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: EUPL-1.2
mod dso;
mod list;
use crate::{
args::Args,
decision::{Action, Decision},
};
use anyhow::{Error, Result};
use async_trait::async_trait;
use dso::DsoCleaner;
use list::ListCleaner;
use nix::libc::EXDEV;
use std::{collections::HashMap, io, path::Path};
use tokio::{sync::mpsc, task::JoinSet};
use walkdir::{DirEntry, WalkDir};
#[async_trait]
pub trait Cleaner {
async fn run(
&mut self,
mut input: mpsc::Receiver<Decision>,
output: mpsc::Sender<Decision>,
) -> Result<()>;
}
type Cleaners = Vec<Box<dyn Cleaner + Send>>;
type RemovalFn = Box<dyn Fn(&Path) -> io::Result<()>>;
pub struct Runner {
cleaners: Cleaners,
removal_fn: RemovalFn,
}
const CHANNEL_SIZE: usize = 100;
impl Runner {
pub fn new(args: Args) -> Self {
let removal_fn = Self::new_removal_fn(&args);
let mut cleaners: Cleaners = vec![];
if let Some(wl) = args.allowlist {
cleaners.push(Box::new(ListCleaner::new(list::ListType::Allow, wl)));
}
if let Some(bl) = args.blocklist {
cleaners.push(Box::new(ListCleaner::new(list::ListType::Block, bl)));
}
cleaners.push(Box::new(DsoCleaner::new(args.output_dotfile)));
Self {
cleaners,
removal_fn,
}
}
pub async fn run(self) -> Result<()> {
let mut tasks = JoinSet::new();
let paths_producer = Self::paths_producer(&mut tasks).await;
let output = self
.cleaners
.into_iter()
.fold(paths_producer, |input, mut cleaner| {
let (tx, rx) = mpsc::channel(CHANNEL_SIZE);
tasks.spawn(async move { cleaner.run(input, tx).await });
rx
});
Self::final_decision(self.removal_fn, output).await;
while let Some(task) = tasks.join_next().await {
if let Err(err) = task? {
log::error!("{}", err);
}
}
Ok(())
}
async fn paths_producer(tasks: &mut JoinSet<Result<(), Error>>) -> mpsc::Receiver<Decision> {
let (input_tx, input_rx) = mpsc::channel(CHANNEL_SIZE);
let walker = WalkDir::new(".").follow_links(false).same_file_system(true);
tasks.spawn(async move {
for entry in walker {
match entry {
Ok(e) if !Self::is_dir(&e) => {
input_tx
.send(Decision {
path: e.into_path().strip_prefix(".")?.to_path_buf(),
action: Action::Undecided,
})
.await?;
}
Ok(_) => continue,
Err(err) => log::warn!("unable to access path: {}", err),
}
}
Ok(())
});
input_rx
}
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 final_decision(removal_fn: RemovalFn, mut output_rx: mpsc::Receiver<Decision>) {
let mut final_decisions = HashMap::new();
while let Some(input_decision) = output_rx.recv().await {
if input_decision.action == Action::Undecided {
continue;
}
match final_decisions.get_mut(&input_decision.path) {
Some(action) if *action == Action::Keep => { /* nothing to do */ }
Some(action) => {
*action = input_decision.action;
}
None => {
final_decisions.insert(input_decision.path, input_decision.action);
}
}
}
for (file, action) in final_decisions {
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,
}
}
}