tauri_api/file/
extract.rs

1use either::{self, Either};
2
3use std::fs;
4use std::io;
5use std::path;
6
7/// The supported archive formats.
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum ArchiveFormat {
10  /// Tar archive.
11  Tar(Option<Compression>),
12  /// Plain archive.
13  Plain(Option<Compression>),
14  /// Zip archive.
15  Zip,
16}
17
18/// The supported compression types.
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum Compression {
21  /// Gz compression (e.g. `.tar.gz` archives)
22  Gz,
23}
24
25/// The extract manager.
26#[derive(Debug)]
27pub struct Extract<'a> {
28  source: &'a path::Path,
29  archive_format: Option<ArchiveFormat>,
30}
31
32fn detect_archive_type(path: &path::Path) -> ArchiveFormat {
33  match path.extension() {
34    Some(extension) if extension == std::ffi::OsStr::new("zip") => ArchiveFormat::Zip,
35    Some(extension) if extension == std::ffi::OsStr::new("tar") => ArchiveFormat::Tar(None),
36    Some(extension) if extension == std::ffi::OsStr::new("gz") => match path
37      .file_stem()
38      .map(|e| path::Path::new(e))
39      .and_then(|f| f.extension())
40    {
41      Some(extension) if extension == std::ffi::OsStr::new("tar") => {
42        ArchiveFormat::Tar(Some(Compression::Gz))
43      }
44      _ => ArchiveFormat::Plain(Some(Compression::Gz)),
45    },
46    _ => ArchiveFormat::Plain(None),
47  }
48}
49
50impl<'a> Extract<'a> {
51  /// Create an `Extractor from a source path
52  pub fn from_source(source: &'a path::Path) -> Extract<'a> {
53    Self {
54      source,
55      archive_format: None,
56    }
57  }
58
59  /// Specify an archive format of the source being extracted. If not specified, the
60  /// archive format will determined from the file extension.
61  pub fn archive_format(&mut self, format: ArchiveFormat) -> &mut Self {
62    self.archive_format = Some(format);
63    self
64  }
65
66  fn get_archive_reader(
67    source: fs::File,
68    compression: Option<Compression>,
69  ) -> Either<fs::File, flate2::read::GzDecoder<fs::File>> {
70    match compression {
71      Some(Compression::Gz) => Either::Right(flate2::read::GzDecoder::new(source)),
72      None => Either::Left(source),
73    }
74  }
75
76  /// Extract an entire source archive into a specified path. If the source is a single compressed
77  /// file and not an archive, it will be extracted into a file with the same name inside of
78  /// `into_dir`.
79  pub fn extract_into(&self, into_dir: &path::Path) -> crate::Result<()> {
80    let source = fs::File::open(self.source)?;
81    let archive = self
82      .archive_format
83      .unwrap_or_else(|| detect_archive_type(&self.source));
84
85    match archive {
86      ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => {
87        let mut reader = Self::get_archive_reader(source, compression);
88
89        match archive {
90          ArchiveFormat::Plain(_) => {
91            match fs::create_dir_all(into_dir) {
92              Ok(_) => (),
93              Err(e) => {
94                if e.kind() != io::ErrorKind::AlreadyExists {
95                  return Err(e.into());
96                }
97              }
98            }
99            let file_name = self
100              .source
101              .file_name()
102              .ok_or_else(|| crate::Error::Extract("Extractor source has no file-name".into()))?;
103            let mut out_path = into_dir.join(file_name);
104            out_path.set_extension("");
105            let mut out_file = fs::File::create(&out_path)?;
106            io::copy(&mut reader, &mut out_file)?;
107          }
108          ArchiveFormat::Tar(_) => {
109            let mut archive = tar::Archive::new(reader);
110            archive.unpack(into_dir)?;
111          }
112          _ => unreachable!(),
113        };
114      }
115      ArchiveFormat::Zip => {
116        let mut archive = zip::ZipArchive::new(source)?;
117        for i in 0..archive.len() {
118          let mut file = archive.by_index(i)?;
119          let path = into_dir.join(file.name());
120          let mut output = fs::File::create(path)?;
121          io::copy(&mut file, &mut output)?;
122        }
123      }
124    };
125    Ok(())
126  }
127
128  /// Extract a single file from a source and save to a file of the same name in `into_dir`.
129  /// If the source is a single compressed file, it will be saved with the name `file_to_extract`
130  /// in the specified `into_dir`.
131  pub fn extract_file<T: AsRef<path::Path>>(
132    &self,
133    into_dir: &path::Path,
134    file_to_extract: T,
135  ) -> crate::Result<()> {
136    let file_to_extract = file_to_extract.as_ref();
137    let source = fs::File::open(self.source)?;
138    let archive = self
139      .archive_format
140      .unwrap_or_else(|| detect_archive_type(&self.source));
141
142    match archive {
143      ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => {
144        let mut reader = Self::get_archive_reader(source, compression);
145
146        match archive {
147          ArchiveFormat::Plain(_) => {
148            match fs::create_dir_all(into_dir) {
149              Ok(_) => (),
150              Err(e) => {
151                if e.kind() != io::ErrorKind::AlreadyExists {
152                  return Err(e.into());
153                }
154              }
155            }
156            let file_name = file_to_extract
157              .file_name()
158              .ok_or_else(|| crate::Error::Extract("Extractor source has no file-name".into()))?;
159            let out_path = into_dir.join(file_name);
160            let mut out_file = fs::File::create(&out_path)?;
161            io::copy(&mut reader, &mut out_file)?;
162          }
163          ArchiveFormat::Tar(_) => {
164            let mut archive = tar::Archive::new(reader);
165            let mut entry = archive
166              .entries()?
167              .filter_map(|e| e.ok())
168              .find(|e| e.path().ok().filter(|p| p == file_to_extract).is_some())
169              .ok_or_else(|| {
170                crate::Error::Extract(format!(
171                  "Could not find the required path in the archive: {:?}",
172                  file_to_extract
173                ))
174              })?;
175            entry.unpack_in(into_dir)?;
176          }
177          _ => {
178            panic!("Unreasonable code");
179          }
180        };
181      }
182      ArchiveFormat::Zip => {
183        let mut archive = zip::ZipArchive::new(source)?;
184        let mut file = archive.by_name(
185          file_to_extract
186            .to_str()
187            .expect("Could not convert file to str"),
188        )?;
189        let mut output = fs::File::create(into_dir.join(file.name()))?;
190        io::copy(&mut file, &mut output)?;
191      }
192    };
193    Ok(())
194  }
195}