novel_cli/cmd/
zip.rs

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