forked from matteo/sysroot-cleaner
213 lines
6.4 KiB
Rust
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,
|
|
}
|
|
}
|
|
}
|