mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-03 10:11:15 +02:00
refactor(core): improve performance of the extract API (#3963)
This commit is contained in:
committed by
GitHub
parent
edf85bc1d1
commit
f7d3d93b62
5
.changes/archive-format-plain-breaking-changes.md
Normal file
5
.changes/archive-format-plain-breaking-changes.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
**Breaking change:** Removed `tauri::api::file::ArchiveFormat::Plain`.
|
||||
5
.changes/extract-file-breaking.change.md
Normal file
5
.changes/extract-file-breaking.change.md
Normal 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`.
|
||||
5
.changes/extract-files-breaking-change.md
Normal file
5
.changes/extract-files-breaking-change.md
Normal 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.
|
||||
5
.changes/extract-performance.md
Normal file
5
.changes/extract-performance.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Improved the performance of the `tauri::api::fs::Extract` API.
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user