fb2_clean/config/
run.rs

1use super::Config;
2use crate::{Input, InputFile, InputFileType, Result, clean_xml};
3use quick_xml::{Reader, Writer};
4use std::{
5    ffi::OsString,
6    fs::{self, File},
7    io::{BufRead, BufReader, BufWriter, Write},
8    iter,
9    path::{Component, Path, PathBuf},
10};
11use walkdir::WalkDir;
12use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
13
14impl Config {
15    /// Run for current [`Config`].
16    pub fn run(&self) -> Result<()> {
17        let mut zip_owner: Option<ZipArchive<File>> = None;
18        let mut is_found_any = false;
19
20        for (subdirs, src) in self.subdirs_src_iter() {
21            is_found_any = true;
22            println!("Cleaning '{}'...", src.path.display());
23            let dest = Dest::new(self, subdirs.as_ref(), &src);
24
25            if !self.force && dest.path.exists() {
26                eprintln!(
27                    "Warning: output is already exists '{}'. Skipping",
28                    dest.path.display()
29                );
30                continue;
31            }
32
33            match try_reader_writer(&mut zip_owner, &src, &dest)
34                .and_then(|(mut r, mut w)| clean_xml(&mut r, &mut w, &self.tags))
35            {
36                Err(e) if self.exit_on_err => return Err(e),
37                Err(e) => eprintln!("Error: {}. Skipping", e),
38                Ok(()) => println!("Success cleaned and saved to '{}'", dest.path.display()),
39            }
40        }
41
42        if !is_found_any {
43            if let Input::Dir(d) = &self.input {
44                eprintln!("Warning: not found any fb2 in dir '{}'", d.display());
45            }
46        }
47
48        Ok(())
49    }
50
51    fn subdirs_src_iter(&self) -> Box<dyn Iterator<Item = (Option<Vec<PathBuf>>, InputFile)> + '_> {
52        match &self.input {
53            Input::Dir(d) if self.recursive != 0 => {
54                let it = WalkDir::new(d)
55                    .max_depth(self.recursive as usize)
56                    .into_iter()
57                    .filter_map(|e| e.ok())
58                    .filter(|e| e.file_type().is_dir())
59                    .filter(|e| !e.path().starts_with(&*self.output.dir))
60                    .map(move |e| {
61                        let subdirs: Vec<PathBuf> = e
62                            .path()
63                            .strip_prefix(d)
64                            .unwrap_or(Path::new(""))
65                            .components()
66                            .filter_map(|x| match x {
67                                Component::Normal(x) => Some(PathBuf::from(x)),
68                                _ => None,
69                            })
70                            .collect();
71
72                        (Some(subdirs), Input::Dir(e.into_path().into()))
73                    })
74                    .flat_map(|(subdirs, d)| iter::repeat(subdirs).zip(d.iter()));
75
76                Box::new(it)
77            }
78            _ => Box::new(iter::repeat(None).zip(self.input.iter())),
79        }
80    }
81}
82
83fn try_reader_writer<'a>(
84    zip_owner: &'a mut Option<ZipArchive<File>>,
85    src: &'a InputFile,
86    dest: &'a Dest,
87) -> Result<(Reader<Box<dyn BufRead + 'a>>, Writer<Box<dyn Write + 'a>>)> {
88    let src_file = File::open(&src.path)?;
89    let dest_file = File::create(&dest.path)?;
90
91    let reader = match src.ty {
92        InputFileType::Fb2 => {
93            Reader::from_reader(Box::new(BufReader::new(src_file)) as Box<dyn BufRead>)
94        }
95        InputFileType::Fb2Zip => {
96            *zip_owner = Some(ZipArchive::new(src_file)?);
97            let zip = zip_owner.as_mut().unwrap();
98            let fb2_index = (0..zip.len())
99                .find(|&i| {
100                    zip.by_index(i)
101                        .ok()
102                        .map(|f| {
103                            let bytes = f.name().as_bytes();
104                            let len = bytes.len();
105                            len > 3 && bytes[len - 4..len].eq_ignore_ascii_case(b".fb2")
106                        })
107                        .unwrap_or(false)
108                })
109                .ok_or_else(|| format!("fb2 not found in archive '{}'", src.path.display()))?;
110
111            let fb2_file = zip.by_index(fb2_index)?;
112            Reader::from_reader(Box::new(BufReader::new(fb2_file)) as Box<dyn BufRead>)
113        }
114    };
115
116    let writer = match dest.ty {
117        InputFileType::Fb2 => Writer::new(Box::new(BufWriter::new(dest_file)) as Box<dyn Write>),
118        InputFileType::Fb2Zip => {
119            let mut zip_writer = ZipWriter::new(BufWriter::new(dest_file));
120            zip_writer.start_file(dest.zip_start_file(), SimpleFileOptions::default())?;
121            Writer::new(Box::new(zip_writer) as Box<dyn Write>)
122        }
123    };
124
125    Ok((reader, writer))
126}
127
128struct Dest {
129    ty: InputFileType,
130    name: OsString,
131    path: PathBuf,
132}
133
134impl Dest {
135    fn new(cfg: &Config, subdirs: Option<&Vec<PathBuf>>, src: &InputFile) -> Dest {
136        let ty = if cfg.zip {
137            InputFileType::Fb2Zip
138        } else if cfg.unzip {
139            InputFileType::Fb2
140        } else {
141            src.ty
142        };
143
144        let name = match src.path.file_stem() {
145            Some(os) => {
146                let mut os = os.to_owned();
147                if src.ty.is_fb2() {
148                    os.push(if ty.is_fb2() { ".fb2" } else { ".fb2.zip" });
149                } else if ty.is_fb2_zip() {
150                    os.push(".zip")
151                }
152                os
153            }
154            None if ty.is_fb2() => OsString::from("cleaned.fb2"),
155            None => OsString::from("cleaned.fb2.zip"),
156        };
157
158        let mut path = cfg.output.dir.clone().into_path_buf();
159        if let Some(xs) = subdirs {
160            for x in xs {
161                path.push(x);
162            }
163            let _ = fs::create_dir_all(&path);
164        }
165        path.push(&name);
166
167        Dest { path, ty, name }
168    }
169
170    fn zip_start_file(&self) -> String {
171        let mut s = self.name.to_string_lossy().into_owned();
172        if self.ty.is_fb2_zip() {
173            s.truncate(s.len() - 4);
174        }
175        s
176    }
177}