epub_builder/
zip_command.rs1use 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
18pub struct ZipCommand {
29 command: String,
30 temp_dir: tempfile::TempDir,
31 files: Vec<PathBuf>,
32}
33
34impl ZipCommand {
35 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 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 pub fn command<S: Into<String>>(&mut self, command: S) -> &mut Self {
68 self.command = command.into();
69 self
70 }
71
72 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 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 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 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}