epub_builder/
zip_command.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with
3// this file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use crate::zip::Zip;
6use crate::Result;
7
8use std::fs;
9use std::fs::DirBuilder;
10use std::fs::File;
11use std::io;
12use std::io::Read;
13use std::io::Write;
14use std::path::Path;
15use std::path::PathBuf;
16use std::process::Command;
17
18/// Zip files using the system `zip` command.
19///
20/// Create a temporary directory, write temp files in that directory, and then
21/// calls the zip command to generate an epub file.
22///
23/// This method will fail if `zip` (or the alternate specified command) is not installed
24/// on the user system.
25///
26/// Note that these takes care of adding the mimetype (since it must not be deflated), it
27/// should not be added manually.
28pub struct ZipCommand {
29    command: String,
30    temp_dir: tempfile::TempDir,
31    files: Vec<PathBuf>,
32}
33
34impl ZipCommand {
35    /// Creates a new ZipCommand, using default setting to create a temporary directory.
36    pub fn new() -> Result<ZipCommand> {
37        let temp_dir = tempfile::TempDir::new().map_err(|e| crate::Error::IoError {
38            msg: "could not create temporary directory".to_string(),
39            cause: e,
40        })?;
41        let zip = ZipCommand {
42            command: String::from("zip"),
43            temp_dir,
44            files: vec![],
45        };
46        Ok(zip)
47    }
48
49    /// Creates a new ZipCommand, specifying where to create a temporary directory.
50    ///
51    /// # Arguments
52    /// * `temp_path`: the path where a temporary directory should be created.
53    pub fn new_in<P: AsRef<Path>>(temp_path: P) -> Result<ZipCommand> {
54        let temp_dir = tempfile::TempDir::new_in(temp_path).map_err(|e| crate::Error::IoError {
55            msg: "could not create temporary directory".to_string(),
56            cause: e,
57        })?;
58        let zip = ZipCommand {
59            command: String::from("zip"),
60            temp_dir,
61            files: vec![],
62        };
63        Ok(zip)
64    }
65
66    /// Set zip command to use (default: "zip")
67    pub fn command<S: Into<String>>(&mut self, command: S) -> &mut Self {
68        self.command = command.into();
69        self
70    }
71
72    /// Test that zip command works correctly (i.e program is installed)
73    pub fn test(&self) -> Result<()> {
74        let output = Command::new(&self.command)
75            .current_dir(self.temp_dir.path())
76            .arg("-v")
77            .output()
78            .map_err(|e| crate::Error::IoError {
79                msg: format!("failed to run command {name}", name = self.command),
80                cause: e,
81            })?;
82        if !output.status.success() {
83            return Err(crate::Error::ZipCommandError(format!(
84                "command {name} did not exit successfully: {output}",
85                name = self.command,
86                output = String::from_utf8_lossy(&output.stderr)
87            )));
88        }
89        Ok(())
90    }
91
92    /// Adds a file to the temporary directory
93    fn add_to_tmp_dir<P: AsRef<Path>, R: Read>(&mut self, path: P, mut content: R) -> Result<()> {
94        let dest_file = self.temp_dir.path().join(path.as_ref());
95        let dest_dir = dest_file.parent().unwrap();
96        if fs::metadata(dest_dir).is_err() {
97            // dir does not exist, create it
98            DirBuilder::new()
99                .recursive(true)
100                .create(dest_dir)
101                .map_err(|e| crate::Error::IoError {
102                    msg: format!(
103                        "could not create temporary directory in {path}",
104                        path = dest_dir.display()
105                    ),
106                    cause: e,
107                })?;
108        }
109
110        let mut f = File::create(&dest_file).map_err(|e| crate::Error::IoError {
111            msg: format!(
112                "could not write to temporary file {file}",
113                file = path.as_ref().display()
114            ),
115            cause: e,
116        })?;
117        io::copy(&mut content, &mut f).map_err(|e| crate::Error::IoError {
118            msg: format!(
119                "could not write to temporary file {file}",
120                file = path.as_ref().display()
121            ),
122            cause: e,
123        })?;
124        Ok(())
125    }
126}
127
128impl Zip for ZipCommand {
129    fn write_file<P: AsRef<Path>, R: Read>(&mut self, path: P, content: R) -> Result<()> {
130        let path = path.as_ref();
131        if path.starts_with("..") || path.is_absolute() {
132            return Err(crate::Error::InvalidPath(format!(
133                "file {} refers to a path outside the temporary directory. This is \
134                   verbotten!",
135                path.display()
136            )));
137        }
138
139        self.add_to_tmp_dir(path, content)?;
140        self.files.push(path.to_path_buf());
141        Ok(())
142    }
143
144    fn generate<W: Write>(mut self, mut to: W) -> Result<()> {
145        // First, add mimetype and don't compress it
146        self.add_to_tmp_dir("mimetype", b"application/epub+zip".as_ref())?;
147        let output = Command::new(&self.command)
148            .current_dir(self.temp_dir.path())
149            .arg("-X0")
150            .arg("output.epub")
151            .arg("mimetype")
152            .output()
153            .map_err(|e| {
154                crate::Error::ZipCommandError(format!(
155                    "failed to run command {name}: {e:?}",
156                    name = self.command
157                ))
158            })?;
159        if !output.status.success() {
160            return Err(crate::Error::ZipCommandError(format!(
161                "command {name} didn't return successfully: {output}",
162                name = self.command,
163                output = String::from_utf8_lossy(&output.stderr)
164            )));
165        }
166
167        let mut command = Command::new(&self.command);
168        command
169            .current_dir(self.temp_dir.path())
170            .arg("-9")
171            .arg("output.epub");
172        for file in &self.files {
173            command.arg(format!("{}", file.display()));
174        }
175
176        let output = command.output().map_err(|e| {
177            crate::Error::ZipCommandError(format!(
178                "failed to run command {name}: {e:?}",
179                name = self.command
180            ))
181        })?;
182        if output.status.success() {
183            let mut f = File::open(self.temp_dir.path().join("output.epub")).map_err(|e| {
184                crate::Error::IoError {
185                    msg: "error reading temporary epub file".to_string(),
186                    cause: e,
187                }
188            })?;
189            io::copy(&mut f, &mut to).map_err(|e| crate::Error::IoError {
190                msg: "error writing result of the zip command".to_string(),
191                cause: e,
192            })?;
193            Ok(())
194        } else {
195            Err(crate::Error::ZipCommandError(format!(
196                "command {name} didn't return successfully: {output}",
197                name = self.command,
198                output = String::from_utf8_lossy(&output.stderr)
199            )))
200        }
201    }
202}
203
204#[test]
205fn zip_creation() {
206    ZipCommand::new().unwrap();
207}
208
209#[test]
210fn zip_ok() {
211    let command = ZipCommand::new().unwrap();
212    let res = command.test();
213    assert!(res.is_ok());
214}
215
216#[test]
217fn zip_not_ok() {
218    let mut command = ZipCommand::new().unwrap();
219    command.command("xkcodpd");
220    let res = command.test();
221    assert!(res.is_err());
222}