1use std::env;
2use std::fs::File;
3use std::io::{Read, Seek, Write};
4use std::path::{Path, PathBuf};
5
6use clap::Args;
7use color_eyre::eyre::Result;
8use fluent_templates::Loader;
9use novel_api::Timing;
10use walkdir::{DirEntry, WalkDir};
11use zip::write::SimpleFileOptions;
12use zip::{CompressionMethod, ZipWriter};
13
14use crate::{LANG_ID, LOCALES, utils};
15
16#[must_use]
17#[derive(Args)]
18#[command(arg_required_else_help = true,
19 about = LOCALES.lookup(&LANG_ID, "zip_command"))]
20pub struct Zip {
21 #[arg(help = LOCALES.lookup(&LANG_ID, "epub_dir_path"))]
22 pub epub_dir_path: PathBuf,
23
24 #[arg(short, long, default_value_t = false,
25 help = LOCALES.lookup(&LANG_ID, "delete"))]
26 pub delete: bool,
27}
28
29pub fn execute(config: Zip) -> Result<()> {
30 let mut timing = Timing::new();
31
32 utils::ensure_epub_dir(&config.epub_dir_path)?;
33
34 let epub_file_path = env::current_dir()?
35 .join(config.epub_dir_path.file_stem().unwrap())
36 .with_extension("epub");
37 if epub_file_path.try_exists()? {
38 tracing::warn!("The epub output file already exists and will be deleted");
39 utils::remove_file_or_dir(&epub_file_path)?;
40 }
41
42 let file = File::create(epub_file_path)?;
43 let walkdir = WalkDir::new(&config.epub_dir_path);
44 zip_dir(
45 &mut walkdir.into_iter().filter_map(|e| e.ok()),
46 &config.epub_dir_path,
47 file,
48 )?;
49
50 if config.delete {
51 utils::remove_file_or_dir(&config.epub_dir_path)?;
52 }
53
54 tracing::debug!("Time spent on `zip`: {}", timing.elapsed()?);
55
56 Ok(())
57}
58
59fn zip_dir<T, E>(iter: &mut dyn Iterator<Item = DirEntry>, prefix: T, writer: E) -> Result<()>
60where
61 T: AsRef<Path>,
62 E: Write + Seek,
63{
64 let mut zip = ZipWriter::new(writer);
65 let options = SimpleFileOptions::default()
66 .compression_method(CompressionMethod::Deflated)
67 .compression_level(Some(9));
68
69 let mut buffer = Vec::new();
70 for entry in iter {
71 let path = entry.path();
72 let name = path.strip_prefix(prefix.as_ref())?;
73
74 if path.is_file() {
75 zip.start_file(name.to_str().unwrap(), options)?;
76 let mut f = File::open(path)?;
77
78 f.read_to_end(&mut buffer)?;
79 zip.write_all(&buffer)?;
80 buffer.clear();
81 } else if !name.as_os_str().is_empty() {
82 zip.add_directory(name.to_str().unwrap(), options)?;
83 }
84 }
85 zip.finish()?;
86
87 Ok(())
88}