novel_cli/cmd/
zip.rs

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}