clean_rs/
lib.rs

1#![allow(clippy::needless_return)]
2#![doc = include_str!("../README.md")]
3
4use std::{
5    io::Write,
6    path::{Path, PathBuf},
7    sync::Arc,
8};
9
10use async_recursion::async_recursion;
11use conf::{Config, Plan};
12use futures::future::{try_join_all, TryJoinAll};
13use tokio::{
14    fs,
15    sync::{
16        mpsc::{self, Receiver, Sender},
17        Mutex,
18    },
19    task::JoinHandle,
20};
21
22mod cmd;
23pub mod conf;
24mod error;
25pub use error::Error;
26
27pub(crate) type IOResult<T> = std::io::Result<T>;
28pub type Result<T> = anyhow::Result<T>;
29
30pub async fn clean<P>(entry: P) -> Result<bool>
31where
32    P: AsRef<Path>,
33{
34    clean_with_config(entry, Config::home().await?).await
35}
36
37pub async fn clean_with_config<P>(entry: P, config: Config) -> Result<bool>
38where
39    P: AsRef<Path>,
40{
41    assert_dir_exists(entry.as_ref())?;
42    let ncpus = num_cpus::get();
43    let (tx, rx) = mpsc::channel::<Execution>(ncpus);
44
45    let tasks = spawn((ncpus >> 1).max(1), Arc::new(Mutex::new(rx)));
46    collect(entry, Arc::new(config), tx).await?;
47
48    return tasks
49        .await?
50        .into_iter()
51        .try_fold(true, |status, result| result.map(|each| each || status));
52
53    type ExecutionRecv = Arc<Mutex<Receiver<Execution<'static>>>>;
54    fn spawn(n: usize, rx: ExecutionRecv) -> TryJoinAll<JoinHandle<Result<bool>>> {
55        try_join_all((0..n).map(move |_| {
56            let rx = rx.clone();
57            tokio::spawn(async move {
58                let mut clean = false;
59                while let Some(execution) = rx.lock().await.recv().await {
60                    clean = execution.run().await? || clean;
61                }
62                Result::Ok(clean)
63            })
64        }))
65    }
66
67    fn assert_dir_exists(path: &Path) -> Result<()> {
68        if !path.exists() {
69            return Err(Error::other(format!(
70                "Directory not found: {}",
71                path.display()
72            )))?;
73        }
74        if !path.is_dir() {
75            return Err(Error::other(format!(
76                "{} is not a directory",
77                path.display()
78            )))?;
79        }
80        Ok(())
81    }
82}
83
84#[async_recursion(?Send)]
85async fn collect<P>(entry: P, config: Arc<Config>, tx: Sender<Execution<'static>>) -> IOResult<()>
86where
87    P: AsRef<Path>,
88{
89    macro_rules! try_unwrap {
90        ($exp: expr) => {
91            match $exp {
92                Ok(value) => value,
93                Err(err) => match err.kind() {
94                    std::io::ErrorKind::NotFound => return Ok(()),
95                    _ => return Err(err),
96                },
97            }
98        };
99    }
100    let entry = entry.as_ref();
101    let mut dir = try_unwrap!(fs::read_dir(entry).await);
102
103    while let Some(current) = try_unwrap!(dir.next_entry().await).map(|e| e.path()) {
104        if let Some(plan) = config.parse(&current) {
105            let _ = tx.send(Execution(plan, entry.to_owned())).await;
106        }
107        if current.is_dir() {
108            collect(current, config.clone(), tx.clone()).await?;
109        }
110    }
111
112    return Ok(());
113}
114
115#[derive(Debug, Clone)]
116struct Execution<'a>(Plan<'a>, PathBuf);
117
118impl<'a> Execution<'a> {
119    async fn run(&self) -> Result<bool> {
120        let result = self.0.run(&self.1).await;
121        write(self, &result)?;
122
123        return result;
124
125        use termcolor::{
126            Buffer, BufferWriter, Color, ColorChoice, ColorSpec, HyperlinkSpec, WriteColor,
127        };
128        fn write(exe: &Execution, result: &Result<bool>) -> Result<()> {
129            use std::io::{stdout, IsTerminal};
130            let out = BufferWriter::stdout(match stdout().is_terminal() {
131                true => ColorChoice::Always,
132                _ => ColorChoice::Never,
133            });
134            return Ok(out.print(&try_concat(tag(exe, &out), colorized(result, &out))?)?);
135        }
136
137        fn try_concat(head: IOResult<Buffer>, tail: IOResult<Buffer>) -> IOResult<Buffer> {
138            let mut buf = Buffer::ansi();
139            buf.write_all(head?.as_slice())?;
140            buf.write_all(tail?.as_slice())?;
141            buf.write_all(b"\n")?;
142            return Ok(buf);
143        }
144
145        fn tag(exe: &Execution, out: &BufferWriter) -> IOResult<Buffer> {
146            let mut buf = colorized_text(exe.0.cmd().as_ref(), Color::Cyan, out)?;
147            let url = {
148                use path_absolutize::Absolutize;
149                let url = format!("file://{}", exe.1.absolutize()?.display());
150                #[cfg(target_os = "windows")]
151                let url = url.replace('\\', "/");
152                url
153            };
154            write!(buf, " clean: ")?;
155            buf.set_hyperlink(&HyperlinkSpec::open(url.as_bytes()))?;
156            write!(buf, "{}", exe.1.display())?;
157            buf.set_hyperlink(&HyperlinkSpec::close())?;
158            write!(buf, "? ")?;
159            return Ok(buf);
160        }
161
162        fn colorized(result: &Result<bool>, out: &BufferWriter) -> IOResult<Buffer> {
163            let (fg, text) = if let Ok(true) = result {
164                (Color::Green, "ok")
165            } else {
166                (Color::Red, "error")
167            };
168            return colorized_text(text, fg, out);
169        }
170
171        fn colorized_text(text: &str, fg: Color, out: &BufferWriter) -> IOResult<Buffer> {
172            let mut buf = out.buffer();
173            let mut spec = ColorSpec::new();
174            buf.set_color(spec.set_fg(Some(fg)))?;
175            write!(buf, "{}", text)?;
176            spec.clear();
177            buf.set_color(&spec)?;
178            return Ok(buf);
179        }
180    }
181}