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 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}