Skip to main content

fb2_clean/config/
run.rs

1use super::Config;
2use crate::{Input, InputFile, InputFileType, Msg, Result, display, remove_xml_tags};
3use either::Either;
4use log::{error, info, warn};
5use quick_xml::{Reader, Writer};
6use rayon::prelude::*;
7use std::{
8    borrow::Cow,
9    fs::{self, File},
10    io::{BufRead, BufReader, BufWriter, Write},
11    iter,
12    path::{Component, Path, PathBuf},
13    sync::{Mutex, Once},
14};
15use walkdir::WalkDir;
16use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
17
18impl Config {
19    /// Run for current [`Config`].
20    pub fn run(&self) -> Result<()> {
21        let is_found_any = Once::new();
22        let it = Mutex::new(self.subdirs_src_iter());
23        let jobs = self.jobs.min(Self::default_jobs());
24
25        let src_dests: Vec<(InputFile, Dest)> = (0..jobs)
26            .into_par_iter()
27            .map(|_| job_src_dests(self, &is_found_any, &it))
28            .collect::<std::result::Result<Vec<_>, String>>()
29            .map(|vecs| vecs.into_iter().flatten().collect())?;
30
31        if !src_dests.is_empty() {
32            force_overwrites(src_dests);
33        }
34
35        if !is_found_any.is_completed() {
36            if let Input::Dir(d) = &self.input {
37                return Err(format!("{} '{}'", Msg::NotFoundAnyBookInDirectory, display(d)).into());
38            }
39        }
40
41        Ok(())
42    }
43
44    fn subdirs_src_iter(&self) -> impl Iterator<Item = (Option<Vec<PathBuf>>, InputFile)> {
45        match &self.input {
46            Input::Dir(d) if self.recursive != 0 => {
47                let it = WalkDir::new(d)
48                    .max_depth(self.recursive as usize)
49                    .into_iter()
50                    .filter_entry(|entry| {
51                        // skip non-directory
52                        if !entry.file_type().is_dir() {
53                            return false;
54                        }
55
56                        let entry_bytes = entry.path().as_os_str().as_encoded_bytes();
57                        let out_bytes = self.output.dir.as_os_str().as_encoded_bytes();
58
59                        // skip output directory
60                        if entry_bytes.len() == out_bytes.len() && entry_bytes.ends_with(out_bytes)
61                        {
62                            return false;
63                        }
64                        true
65                    })
66                    .filter_map(|e| e.ok())
67                    .map(move |e| {
68                        let subdirs: Vec<PathBuf> = e
69                            .path()
70                            .strip_prefix(d)
71                            .unwrap_or(Path::new(""))
72                            .components()
73                            .filter_map(|x| match x {
74                                Component::Normal(x) => Some(PathBuf::from(x)),
75                                _ => None,
76                            })
77                            .collect();
78
79                        (Some(subdirs), Input::Dir(e.into_path().into()))
80                    })
81                    .flat_map(|(subdirs, d)| iter::repeat(subdirs).zip(d.iter()));
82
83                Either::Left(it)
84            }
85            _ => Either::Right(iter::repeat(None).zip(self.input.iter())),
86        }
87    }
88}
89
90fn job_src_dests(
91    cfg: &Config,
92    is_found_any: &Once,
93    it: &Mutex<impl Iterator<Item = (Option<Vec<PathBuf>>, InputFile)>>,
94) -> std::result::Result<Vec<(InputFile, Dest)>, String> {
95    let mut src_dests: Vec<(InputFile, Dest)> = Vec::new();
96    let mut zip_owner: Option<ZipArchive<File>> = None;
97
98    loop {
99        let (src, dest) = {
100            let mut it = it.lock().ok();
101            match it.as_mut().and_then(|it| it.next()) {
102                Some((subdirs, src)) => {
103                    is_found_any.call_once(|| {
104                        info!("{}...", Msg::CleaningBooks);
105                    });
106                    let dest = Dest::new(cfg, subdirs, &src);
107                    (src, dest)
108                }
109                None => break,
110            }
111        };
112        info!("{} '{}'...", Msg::Cleaning, display(&src.path));
113
114        if !cfg.overwrite && dest.path.exists() {
115            warn!(
116                "{} '{}'. {}",
117                Msg::FileAlreadyExists,
118                display(&dest.path),
119                Msg::Skipping,
120            );
121            continue;
122        }
123
124        match try_reader_writer(&mut zip_owner, &src, &dest)
125            .and_then(|(mut r, mut w)| remove_xml_tags(&mut r, &mut w, &cfg.tags))
126        {
127            Err(e) if cfg.exit_on_err => return Err(e.to_string()),
128            Err(e) => {
129                error!("{}. {}", e, Msg::Skipping);
130                continue;
131            }
132            Ok(()) => {
133                info!("{} '{}'", Msg::SuccessfullyCleanedTo, display(&dest.path))
134            }
135        }
136
137        if cfg.overwrite {
138            src_dests.push((src, dest));
139        }
140    }
141    Ok(src_dests)
142}
143
144fn force_overwrites(src_dests: Vec<(InputFile, Dest)>) {
145    info!("\n{}...", Msg::OverwritingBooks);
146
147    for (src, dest) in &src_dests {
148        info!("{} '{}'...", Msg::Overwriting, display(&src.path));
149        match dest.force_overwrite(&src) {
150            Ok(()) => info!(
151                "{} '{}'",
152                Msg::SuccessfullyOverwritedFrom,
153                display(&dest.path)
154            ),
155            Err(e) => error!("{}: {}", Msg::Overwriting, e),
156        }
157    }
158
159    let mut dirs: Vec<PathBuf> = Vec::new();
160    for (_, dest) in src_dests {
161        for d in dest.created_dirs.into_iter().flat_map(|ds| ds.into_iter()) {
162            dirs.push(d);
163        }
164    }
165    dirs.par_sort_by(|a, b| b.as_os_str().len().cmp(&a.as_os_str().len()));
166
167    for d in dirs {
168        if let Err(e) = fs::remove_dir(&d) {
169            error!("{} '{}': {}", Msg::RemovingTempDirectory, display(&d), e);
170        }
171    }
172}
173
174fn try_reader_writer<'a>(
175    zip_owner: &'a mut Option<ZipArchive<File>>,
176    src: &'a InputFile,
177    dest: &'a Dest,
178) -> Result<(Reader<Box<dyn BufRead + 'a>>, Writer<Box<dyn Write + 'a>>)> {
179    let src_file = File::open(&src.path)?;
180    let dest_file = File::create(&dest.path)?;
181
182    let reader = match src.ty {
183        InputFileType::Fb2 => {
184            Reader::from_reader(Box::new(BufReader::new(src_file)) as Box<dyn BufRead>)
185        }
186        InputFileType::Fb2Zip => {
187            *zip_owner = Some(ZipArchive::new(src_file)?);
188            let zip = zip_owner.as_mut().unwrap();
189            let fb2_index = (0..zip.len())
190                .find(|&i| {
191                    zip.by_index(i)
192                        .ok()
193                        .map(|f| {
194                            f.name().is_ok_and(|name| {
195                                let bytes = name.as_bytes();
196                                let len = bytes.len();
197                                len > 3 && bytes[len - 4..len].eq_ignore_ascii_case(b".fb2")
198                            })
199                        })
200                        .unwrap_or(false)
201                })
202                .ok_or_else(|| {
203                    format!("{} '{}'", Msg::NotFoundAnyFb2InArchive, display(&src.path))
204                })?;
205
206            let fb2_file = zip.by_index(fb2_index)?;
207            Reader::from_reader(Box::new(BufReader::new(fb2_file)) as Box<dyn BufRead>)
208        }
209    };
210
211    let writer = match dest.ty {
212        InputFileType::Fb2 => Writer::new(Box::new(BufWriter::new(dest_file)) as Box<dyn Write>),
213        InputFileType::Fb2Zip => {
214            let mut zip_writer = ZipWriter::new(BufWriter::new(dest_file));
215            zip_writer.start_file(dest.zip_start_file(), SimpleFileOptions::default())?;
216            Writer::new(Box::new(zip_writer) as Box<dyn Write>)
217        }
218    };
219
220    Ok((reader, writer))
221}
222
223struct Dest {
224    created_dirs: Option<Vec<PathBuf>>,
225    ty: InputFileType,
226    stem: PathBuf,
227    path: PathBuf,
228}
229
230impl Dest {
231    fn new(cfg: &Config, subdirs: Option<Vec<PathBuf>>, src: &InputFile) -> Dest {
232        let ty = if cfg.zip {
233            InputFileType::Fb2Zip
234        } else if cfg.unzip {
235            InputFileType::Fb2
236        } else {
237            src.ty
238        };
239
240        let stem = match src.path.file_stem() {
241            Some(os) => {
242                let mut p = PathBuf::from(os);
243                if src.ty.is_fb2_zip() {
244                    p.set_extension("");
245                }
246                p
247            }
248            _ => PathBuf::from("cleaned"),
249        };
250
251        let mut path = cfg.output.dir.clone().into_path_buf();
252        let mut created_dirs: Option<Vec<PathBuf>> = None;
253
254        if let Some(xs) = &subdirs {
255            for x in xs {
256                path.push(x);
257                if let Ok(()) = fs::create_dir(&path) {
258                    created_dirs
259                        .get_or_insert_with(|| Vec::with_capacity(xs.len()))
260                        .push(path.clone())
261                }
262            }
263        }
264        path.push(&stem);
265        path.add_extension(ty.as_extension());
266
267        Dest {
268            created_dirs,
269            ty,
270            stem,
271            path,
272        }
273    }
274
275    fn force_overwrite(&self, src: &InputFile) -> Result<()> {
276        let force_path = self.force_path(src);
277
278        if let Err(_) = fs::rename(&self.path, &force_path) {
279            fs::copy(&self.path, &force_path)?;
280            if let Err(e) = fs::remove_file(&self.path) {
281                error!("{} '{}': {}", Msg::RemovingTempFile, display(&self.path), e);
282            }
283        }
284
285        if force_path != &*src.path {
286            if let Err(e) = fs::remove_file(&src.path) {
287                error!("{} '{}': {}", Msg::RemovingInputFile, display(&src.path), e);
288            }
289        }
290        Ok(())
291    }
292
293    fn force_path<'a>(&self, src: &'a InputFile) -> Cow<'a, Path> {
294        if self.ty == src.ty {
295            return Cow::Borrowed(&*src.path);
296        }
297
298        let mut p = match src.path.parent() {
299            Some(p) => PathBuf::from(p),
300            None => PathBuf::from("."),
301        };
302        p.push(&self.stem);
303        p.add_extension(self.ty.as_extension());
304        Cow::Owned(p)
305    }
306
307    fn zip_start_file(&self) -> String {
308        let mut s = self.stem.to_string_lossy().into_owned();
309        s.push_str(".fb2");
310        s
311    }
312}