refactor(core): improve performance of the extract API (#3963)

This commit is contained in:
Lucas Fernandes Nogueira
2022-04-25 16:52:02 -07:00
committed by GitHub
parent edf85bc1d1
commit f7d3d93b62
7 changed files with 217 additions and 191 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri": patch
---
**Breaking change:** Removed `tauri::api::file::ArchiveFormat::Plain`.

View File

@@ -0,0 +1,5 @@
---
"tauri": patch
---
**Breaking change:** The `tauri::api::file::Extract#extract_file` function has been moved to `tauri::api::file::Entry#extract`.

View File

@@ -0,0 +1,5 @@
---
"tauri": patch
---
**Breaking change:** The `tauri::api::file::Extract#files` function has been renamed to `with_files` for performance reasons.

View File

@@ -0,0 +1,5 @@
---
"tauri": patch
---
Improved the performance of the `tauri::api::fs::Extract` API.

View File

@@ -9,9 +9,6 @@ pub enum Error {
/// Command error.
#[error("Command Error: {0}")]
Command(String),
/// The extract archive error.
#[error("Extract Error: {0}")]
Extract(String),
/// The path operation error.
#[error("Path Error: {0}")]
Path(String),
@@ -69,6 +66,10 @@ pub enum Error {
#[cfg(feature = "fs-extract-api")]
#[error(transparent)]
Zip(#[from] zip::result::ZipError),
/// Extract error.
#[cfg(feature = "fs-extract-api")]
#[error("Failed to extract: {0}")]
Extract(String),
/// Notification error.
#[cfg(notification_all)]
#[error(transparent)]

View File

@@ -2,21 +2,46 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use either::{self, Either};
use std::{
borrow::Cow,
fs,
io::{self, Read, Seek},
io::{self, Cursor, Read, Seek},
path::{self, Path, PathBuf},
};
/// The archive reader.
#[derive(Debug)]
pub enum ArchiveReader<R: Read + Seek> {
/// A plain reader.
Plain(R),
/// A GZ- compressed reader (decoder).
GzCompressed(flate2::read::GzDecoder<R>),
}
impl<R: Read + Seek> Read for ArchiveReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::Plain(r) => r.read(buf),
Self::GzCompressed(decoder) => decoder.read(buf),
}
}
}
impl<R: Read + Seek> ArchiveReader<R> {
fn get_mut(&mut self) -> &mut R {
match self {
Self::Plain(r) => r,
Self::GzCompressed(decoder) => decoder.get_mut(),
}
}
}
/// The supported archive formats.
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum ArchiveFormat {
/// Tar archive.
Tar(Option<Compression>),
/// Plain archive.
Plain(Option<Compression>),
/// Zip archive.
Zip,
}
@@ -29,112 +54,182 @@ pub enum Compression {
Gz,
}
/// The extract manager to retrieve files from archives.
#[derive(Debug)]
pub struct Extract<R> {
reader: R,
archive_format: ArchiveFormat,
/// The zip entry.
pub struct ZipEntry {
path: PathBuf,
is_dir: bool,
file_contents: Vec<u8>,
}
impl<R: Read + Seek> Extract<R> {
/// Create archive from reader.
pub fn from_cursor(mut reader: R, archive_format: ArchiveFormat) -> Extract<R> {
if reader.seek(io::SeekFrom::Start(0)).is_err() {
#[cfg(debug_assertions)]
eprintln!("Could not seek to start of the file");
}
Extract {
reader,
archive_format,
/// A read-only view into an entry of an archive.
#[non_exhaustive]
pub enum Entry<'a, R: Read> {
/// An entry of a tar archive.
#[non_exhaustive]
Tar(Box<tar::Entry<'a, R>>),
/// An entry of a zip archive.
#[non_exhaustive]
Zip(ZipEntry),
}
impl<'a, R: Read> Entry<'a, R> {
/// The entry path.
pub fn path(&self) -> crate::api::Result<Cow<'_, Path>> {
match self {
Self::Tar(e) => e.path().map_err(Into::into),
Self::Zip(e) => Ok(Cow::Borrowed(&e.path)),
}
}
/// Get the archive content.
pub fn files(&mut self) -> crate::api::Result<Vec<PathBuf>> {
let reader = &mut self.reader;
let mut all_files = Vec::new();
if reader.seek(io::SeekFrom::Start(0)).is_err() {
#[cfg(debug_assertions)]
eprintln!("Could not seek to start of the file");
}
match self.archive_format {
ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => {
let reader = Self::get_archive_reader(reader, compression);
match self.archive_format {
ArchiveFormat::Tar(_) => {
let mut archive = tar::Archive::new(reader);
for entry in archive.entries()?.flatten() {
if let Ok(path) = entry.path() {
all_files.push(path.to_path_buf());
/// Extract this entry into `into_path`.
/// If it's a directory, the target will be created, if it's a file, it'll be extracted at this location.
/// Note: You need to include the complete path, with file name and extension.
pub fn extract(self, into_path: &path::Path) -> crate::api::Result<()> {
match self {
Self::Tar(mut entry) => {
// determine if it's a file or a directory
if entry.header().entry_type() == tar::EntryType::Directory {
// this is a directory, lets create it
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
_ => unreachable!(),
};
}
} else {
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut entry, &mut out_file)?;
ArchiveFormat::Zip => {
let archive = zip::ZipArchive::new(reader)?;
for entry in archive.file_names() {
all_files.push(PathBuf::from(entry));
// make sure we set permissions
if let Ok(mode) = entry.header().mode() {
set_perms(into_path, Some(&mut out_file), mode, true)?;
}
}
}
Self::Zip(entry) => {
if entry.is_dir {
// this is a directory, lets create it
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
} else {
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut Cursor::new(entry.file_contents), &mut out_file)?;
}
}
}
Ok(all_files)
Ok(())
}
}
// Get the reader based on the compression type.
fn get_archive_reader(
source: &mut R,
compression: Option<Compression>,
) -> Either<&mut R, flate2::read::GzDecoder<&mut R>> {
if source.seek(io::SeekFrom::Start(0)).is_err() {
/// The extract manager to retrieve files from archives.
pub struct Extract<'a, R: Read + Seek> {
reader: ArchiveReader<R>,
archive_format: ArchiveFormat,
tar_archive: Option<tar::Archive<&'a mut ArchiveReader<R>>>,
}
impl<'a, R: std::fmt::Debug + Read + Seek> std::fmt::Debug for Extract<'a, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Extract")
.field("reader", &self.reader)
.field("archive_format", &self.archive_format)
.finish()
}
}
impl<'a, R: Read + Seek> Extract<'a, R> {
/// Create archive from reader.
pub fn from_cursor(mut reader: R, archive_format: ArchiveFormat) -> Extract<'a, R> {
if reader.seek(io::SeekFrom::Start(0)).is_err() {
#[cfg(debug_assertions)]
eprintln!("Could not seek to start of the file");
}
match compression {
Some(Compression::Gz) => Either::Right(flate2::read::GzDecoder::new(source)),
None => Either::Left(source),
let compression = if let ArchiveFormat::Tar(compression) = archive_format {
compression
} else {
None
};
Extract {
reader: match compression {
Some(Compression::Gz) => ArchiveReader::GzCompressed(flate2::read::GzDecoder::new(reader)),
_ => ArchiveReader::Plain(reader),
},
archive_format,
tar_archive: None,
}
}
/// Reads the archive content.
pub fn with_files<
E: Into<crate::api::Error>,
F: FnMut(Entry<'_, &mut ArchiveReader<R>>) -> std::result::Result<bool, E>,
>(
&'a mut self,
mut f: F,
) -> crate::api::Result<()> {
match self.archive_format {
ArchiveFormat::Tar(_) => {
let archive = tar::Archive::new(&mut self.reader);
self.tar_archive.replace(archive);
for entry in self.tar_archive.as_mut().unwrap().entries()? {
let entry = entry?;
if entry.path().is_ok() {
let stop = f(Entry::Tar(Box::new(entry))).map_err(Into::into)?;
if stop {
break;
}
}
}
}
ArchiveFormat::Zip => {
let mut archive = zip::ZipArchive::new(self.reader.get_mut())?;
let file_names = archive
.file_names()
.map(|f| f.to_string())
.collect::<Vec<String>>();
for path in file_names {
let mut zip_file = archive.by_name(&path)?;
let is_dir = zip_file.is_dir();
let mut file_contents = Vec::new();
zip_file.read_to_end(&mut file_contents)?;
let stop = f(Entry::Zip(ZipEntry {
path: path.into(),
is_dir,
file_contents,
}))
.map_err(Into::into)?;
if stop {
break;
}
}
}
}
Ok(())
}
/// Extract an entire source archive into a specified path. If the source is a single compressed
/// file and not an archive, it will be extracted into a file with the same name inside of
/// `into_dir`.
pub fn extract_into(&mut self, into_dir: &path::Path) -> crate::api::Result<()> {
let reader = &mut self.reader;
if reader.seek(io::SeekFrom::Start(0)).is_err() {
#[cfg(debug_assertions)]
eprintln!("Could not seek to start of the file");
}
match self.archive_format {
ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => {
let mut reader = Self::get_archive_reader(reader, compression);
match self.archive_format {
ArchiveFormat::Plain(_) => {
match fs::create_dir_all(into_dir) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
let mut out_file = fs::File::create(&into_dir)?;
io::copy(&mut reader, &mut out_file)?;
}
ArchiveFormat::Tar(_) => {
let mut archive = tar::Archive::new(reader);
archive.unpack(into_dir)?;
}
_ => unreachable!(),
};
ArchiveFormat::Tar(_) => {
let mut archive = tar::Archive::new(&mut self.reader);
archive.unpack(into_dir)?;
}
ArchiveFormat::Zip => {
let mut archive = zip::ZipArchive::new(reader)?;
let mut archive = zip::ZipArchive::new(self.reader.get_mut())?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
// Decode the file name from raw bytes instead of using file.name() directly.
@@ -165,100 +260,6 @@ impl<R: Read + Seek> Extract<R> {
}
Ok(())
}
/// Extract a single file from a source and extract it `into_path`.
/// If it's a directory, the target will be created, if it's a file, it'll be extracted at this location.
/// Note: You need to include the complete path, with file name and extension.
pub fn extract_file<T: AsRef<path::Path>>(
&mut self,
into_path: &path::Path,
file_to_extract: T,
) -> crate::api::Result<()> {
let file_to_extract = file_to_extract.as_ref();
let reader = &mut self.reader;
match self.archive_format {
ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => {
let mut reader = Self::get_archive_reader(reader, compression);
match self.archive_format {
ArchiveFormat::Plain(_) => {
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut reader, &mut out_file)?;
}
ArchiveFormat::Tar(_) => {
let mut archive = tar::Archive::new(reader);
let mut entry = archive
.entries()?
.filter_map(|e| e.ok())
.find(|e| e.path().ok().filter(|p| p == file_to_extract).is_some())
.ok_or_else(|| {
crate::api::Error::Extract(format!(
"Could not find the required path in the archive: {:?}",
file_to_extract
))
})?;
// determine if it's a file or a directory
if entry.header().entry_type() == tar::EntryType::Directory {
// this is a directory, lets create it
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
} else {
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut entry, &mut out_file)?;
// make sure we set permissions
if let Ok(mode) = entry.header().mode() {
set_perms(into_path, Some(&mut out_file), mode, true)?;
}
}
}
_ => {
panic!("Unreasonable code");
}
};
}
ArchiveFormat::Zip => {
let mut archive = zip::ZipArchive::new(reader)?;
let mut file = archive.by_name(
file_to_extract
.to_str()
.expect("Could not convert file to str"),
)?;
if file.is_dir() {
// this is a directory, lets create it
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
} else {
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut file, &mut out_file)?;
}
}
}
Ok(())
}
}
fn set_perms(

View File

@@ -643,17 +643,19 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
let mut extractor =
Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz)));
for file in extractor.files()? {
if file.extension() == Some(OsStr::new("AppImage")) {
extractor.with_files(|entry| {
let path = entry.path()?;
if path.extension() == Some(OsStr::new("AppImage")) {
// if something went wrong during the extraction, we should restore previous app
if let Err(err) = extractor.extract_file(extract_path, &file) {
if let Err(err) = entry.extract(extract_path) {
Move::from_source(tmp_app_image).to_dest(extract_path)?;
return Err(Error::Extract(err.to_string()));
return Err(crate::api::Error::Extract(err.to_string()));
}
// early finish we have everything we need here
return Ok(());
return Ok(true);
}
}
Ok(false)
})?;
Ok(())
}
@@ -785,7 +787,6 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz)));
// the first file in the tar.gz will always be
// <app_name>/Contents
let all_files = extractor.files()?;
let tmp_dir = tempfile::Builder::new()
.prefix("tauri_current_app")
.tempdir()?;
@@ -794,14 +795,15 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
Move::from_source(extract_path).to_dest(tmp_dir.path())?;
// extract all the files
for file in all_files {
extractor.with_files(|entry| {
let path = entry.path()?;
// skip the first folder (should be the app name)
let collected_path: PathBuf = file.iter().skip(1).collect();
let collected_path: PathBuf = path.iter().skip(1).collect();
let extraction_path = extract_path.join(collected_path);
// if something went wrong during the extraction, we should restore previous app
if let Err(err) = extractor.extract_file(&extraction_path, &file) {
for file in extracted_files {
if let Err(err) = entry.extract(&extraction_path) {
for file in &extracted_files {
// delete all the files we extracted
if file.is_dir() {
std::fs::remove_dir(file)?;
@@ -810,11 +812,13 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
}
}
Move::from_source(tmp_dir.path()).to_dest(extract_path)?;
return Err(Error::Extract(err.to_string()));
return Err(crate::api::Error::Extract(err.to_string()));
}
extracted_files.push(extraction_path);
}
Ok(false)
})?;
Ok(())
}