fb2_clean/config/
run.rs

1use super::Config;
2use crate::{Input, InputFile, InputFileType, Result, clean_xml};
3use quick_xml::{Reader, Writer};
4use std::{
5    borrow::Cow,
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 src_dests: Vec<(InputFile, Dest)> = Vec::new();
18        let mut zip_owner: Option<ZipArchive<File>> = None;
19        let mut is_found_any = false;
20
21        for (subdirs, src) in self.subdirs_src_iter() {
22            if !is_found_any {
23                println!("Cleaning input files...");
24                is_found_any = true;
25            }
26
27            println!("Cleaning '{}'...", src.path.display());
28            let dest = Dest::new(self, subdirs, &src);
29
30            if !self.force && dest.path.exists() {
31                eprintln!(
32                    "Warning: output is already exists '{}'. Skipping",
33                    dest.path.display()
34                );
35                continue;
36            }
37
38            match try_reader_writer(&mut zip_owner, &src, &dest)
39                .and_then(|(mut r, mut w)| clean_xml(&mut r, &mut w, &self.tags))
40            {
41                Err(e) if self.exit_on_err => return Err(e),
42                Err(e) => {
43                    eprintln!("Error: {}. Skipping", e);
44                    continue;
45                }
46                Ok(()) => println!("Success cleaned and saved to '{}'", dest.path.display()),
47            }
48
49            if self.force {
50                src_dests.push((src, dest));
51            }
52        }
53
54        if !src_dests.is_empty() {
55            force_overwrites(src_dests);
56        }
57
58        if !is_found_any {
59            if let Input::Dir(d) = &self.input {
60                eprintln!("Warning: not found any fb2 in dir '{}'", d.display());
61            }
62        }
63
64        Ok(())
65    }
66
67    fn subdirs_src_iter(&self) -> Box<dyn Iterator<Item = (Option<Vec<PathBuf>>, InputFile)> + '_> {
68        match &self.input {
69            Input::Dir(d) if self.recursive != 0 => {
70                let it = WalkDir::new(d)
71                    .max_depth(self.recursive as usize)
72                    .into_iter()
73                    .filter_map(|e| e.ok())
74                    .filter(|e| e.file_type().is_dir())
75                    .filter(|e| !e.path().starts_with(&*self.output.dir))
76                    .map(move |e| {
77                        let subdirs: Vec<PathBuf> = e
78                            .path()
79                            .strip_prefix(d)
80                            .unwrap_or(Path::new(""))
81                            .components()
82                            .filter_map(|x| match x {
83                                Component::Normal(x) => Some(PathBuf::from(x)),
84                                _ => None,
85                            })
86                            .collect();
87
88                        (Some(subdirs), Input::Dir(e.into_path().into()))
89                    })
90                    .flat_map(|(subdirs, d)| iter::repeat(subdirs).zip(d.iter()));
91
92                Box::new(it)
93            }
94            _ => Box::new(iter::repeat(None).zip(self.input.iter())),
95        }
96    }
97}
98
99fn force_overwrites(mut src_dests: Vec<(InputFile, Dest)>) {
100    println!("\nOverwriting input files...");
101
102    for (src, dest) in &src_dests {
103        println!("Overwriting '{}'...", src.path.display());
104        match dest.force_overwrite(&src) {
105            Ok(()) => println!("Success overwrited from '{}'", dest.path.display()),
106            Err(e) => eprintln!("Error: Fail overwrite: {}", e),
107        }
108    }
109
110    src_dests.sort_by(|a, b| b.1.len_created_dirs.cmp(&a.1.len_created_dirs));
111
112    for (_, dest) in &src_dests {
113        dest.remove_created_dirs();
114    }
115}
116
117fn try_reader_writer<'a>(
118    zip_owner: &'a mut Option<ZipArchive<File>>,
119    src: &'a InputFile,
120    dest: &'a Dest,
121) -> Result<(Reader<Box<dyn BufRead + 'a>>, Writer<Box<dyn Write + 'a>>)> {
122    let src_file = File::open(&src.path)?;
123    let dest_file = File::create(&dest.path)?;
124
125    let reader = match src.ty {
126        InputFileType::Fb2 => {
127            Reader::from_reader(Box::new(BufReader::new(src_file)) as Box<dyn BufRead>)
128        }
129        InputFileType::Fb2Zip => {
130            *zip_owner = Some(ZipArchive::new(src_file)?);
131            let zip = zip_owner.as_mut().unwrap();
132            let fb2_index = (0..zip.len())
133                .find(|&i| {
134                    zip.by_index(i)
135                        .ok()
136                        .map(|f| {
137                            let bytes = f.name().as_bytes();
138                            let len = bytes.len();
139                            len > 3 && bytes[len - 4..len].eq_ignore_ascii_case(b".fb2")
140                        })
141                        .unwrap_or(false)
142                })
143                .ok_or_else(|| format!("fb2 not found in archive '{}'", src.path.display()))?;
144
145            let fb2_file = zip.by_index(fb2_index)?;
146            Reader::from_reader(Box::new(BufReader::new(fb2_file)) as Box<dyn BufRead>)
147        }
148    };
149
150    let writer = match dest.ty {
151        InputFileType::Fb2 => Writer::new(Box::new(BufWriter::new(dest_file)) as Box<dyn Write>),
152        InputFileType::Fb2Zip => {
153            let mut zip_writer = ZipWriter::new(BufWriter::new(dest_file));
154            zip_writer.start_file(dest.zip_start_file(), SimpleFileOptions::default())?;
155            Writer::new(Box::new(zip_writer) as Box<dyn Write>)
156        }
157    };
158
159    Ok((reader, writer))
160}
161
162struct Dest {
163    created_dirs: Option<Vec<PathBuf>>,
164    len_created_dirs: usize,
165    ty: InputFileType,
166    stem: PathBuf,
167    path: PathBuf,
168}
169
170impl Dest {
171    fn new(cfg: &Config, subdirs: Option<Vec<PathBuf>>, src: &InputFile) -> Dest {
172        let ty = if cfg.zip {
173            InputFileType::Fb2Zip
174        } else if cfg.unzip {
175            InputFileType::Fb2
176        } else {
177            src.ty
178        };
179
180        let stem = match src.path.file_stem() {
181            Some(os) => {
182                let mut p = PathBuf::from(os);
183                if src.ty.is_fb2_zip() {
184                    p.set_extension("");
185                }
186                p
187            }
188            _ => PathBuf::from("cleaned"),
189        };
190
191        let mut path = cfg.output.dir.clone().into_path_buf();
192        let mut created_dirs: Option<Vec<PathBuf>> = None;
193
194        if let Some(xs) = &subdirs {
195            for x in xs {
196                path.push(x);
197                if let Ok(()) = fs::create_dir(&path) {
198                    created_dirs
199                        .get_or_insert_with(|| Vec::with_capacity(xs.len()))
200                        .push(path.clone())
201                }
202            }
203        }
204        path.push(&stem);
205        path.add_extension(ty.as_extension());
206
207        Dest {
208            len_created_dirs: created_dirs.as_ref().map(|xs| xs.len()).unwrap_or(0),
209            created_dirs,
210            ty,
211            stem,
212            path,
213        }
214    }
215
216    fn force_overwrite(&self, src: &InputFile) -> Result<()> {
217        let force_path = self.force_path(src);
218
219        if let Err(_) = fs::rename(&self.path, &force_path) {
220            fs::copy(&self.path, &force_path)?;
221            if let Err(e) = fs::remove_file(&self.path) {
222                eprintln!(
223                    "Error: fail remove temp file '{}': {}",
224                    self.path.display(),
225                    e
226                );
227            }
228        }
229
230        if force_path != &*src.path {
231            if let Err(e) = fs::remove_file(&src.path) {
232                eprintln!(
233                    "Error: fail remove input file '{}': {}",
234                    src.path.display(),
235                    e
236                );
237            }
238        }
239        Ok(())
240    }
241
242    fn force_path<'a>(&self, src: &'a InputFile) -> Cow<'a, Path> {
243        if self.ty == src.ty {
244            return Cow::Borrowed(&*src.path);
245        }
246
247        let mut p = match src.path.parent() {
248            Some(p) => PathBuf::from(p),
249            None => PathBuf::from("."),
250        };
251        p.push(&self.stem);
252        p.add_extension(self.ty.as_extension());
253        Cow::Owned(p)
254    }
255
256    fn remove_created_dirs(&self) {
257        if let Some(xs) = &self.created_dirs {
258            for x in xs.iter().rev() {
259                if let Err(e) = fs::remove_dir(x) {
260                    eprintln!("Error: Fail remove temp directory '{}': {}", x.display(), e);
261                }
262            }
263        }
264    }
265
266    fn zip_start_file(&self) -> String {
267        let mut s = self.stem.to_string_lossy().into_owned();
268        s.push_str(".fb2");
269        s
270    }
271}