fix: panic when a second zip extract dialog is opened

* Syncs extracted zip files on the compio executor's thread
* Close the first zip extract dialog before opening a new one to fix a panic
---------

Co-authored-by: James A DellaMorte <dellamorte.james@comcast.net>
This commit is contained in:
JADella94 2026-06-24 14:12:10 -04:00 committed by GitHub
parent c653b69c75
commit cd427c746c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 49 additions and 23 deletions

View file

@ -1009,6 +1009,12 @@ impl App {
.and_then(|first| first.as_ref().parent()) .and_then(|first| first.as_ref().parent())
.map(Path::to_path_buf) .map(Path::to_path_buf)
{ {
let mut tasks = Vec::new();
if let Some(old_dialog) = self.file_dialog_opt.take() {
let old_id = old_dialog.window_id();
self.windows.remove(&old_id);
tasks.push(window::close(old_id));
}
let (mut dialog, dialog_task) = Dialog::new( let (mut dialog, dialog_task) = Dialog::new(
DialogSettings::new() DialogSettings::new()
.kind(DialogKind::OpenFolder) .kind(DialogKind::OpenFolder)
@ -1025,7 +1031,9 @@ impl App {
))), ))),
); );
self.file_dialog_opt = Some(dialog); self.file_dialog_opt = Some(dialog);
Task::batch([set_title_task, dialog_task]) tasks.push(set_title_task);
tasks.push(dialog_task);
Task::batch(tasks)
} else { } else {
Task::none() Task::none()
} }

View file

@ -1,5 +1,5 @@
use crate::mime_icon::mime_for_path; use crate::mime_icon::mime_for_path;
use crate::operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk}; use crate::operation::{Controller, OpReader, OperationError, OperationErrorType};
use cosmic::iced::futures; use cosmic::iced::futures;
use jiff::Zoned; use jiff::Zoned;
use jiff::civil::DateTime; use jiff::civil::DateTime;
@ -45,7 +45,7 @@ pub fn extract(
new_dir: &Path, new_dir: &Path,
password: &Option<String>, password: &Option<String>,
controller: &Controller, controller: &Controller,
) -> Result<(), OperationError> { ) -> Result<(Vec<PathBuf>, HashSet<PathBuf>), OperationError> {
let mime = mime_for_path(path, None, false); let mime = mime_for_path(path, None, false);
let password = password.as_deref(); let password = password.as_deref();
match mime.essence_str() { match mime.essence_str() {
@ -55,13 +55,15 @@ pub fn extract(
.map(flate2::read::GzDecoder::new) .map(flate2::read::GzDecoder::new)
.map(tar::Archive::new) .map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir)) .and_then(|mut archive| archive.unpack(new_dir))
.map_err(|e| OperationError::from_err(e, controller))?; .map_err(|e| OperationError::from_err(e, controller))
.map(|_| Default::default())
} }
"application/x-tar" => OpReader::new(path, controller.clone()) "application/x-tar" => OpReader::new(path, controller.clone())
.map(io::BufReader::new) .map(io::BufReader::new)
.map(tar::Archive::new) .map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir)) .and_then(|mut archive| archive.unpack(new_dir))
.map_err(|e| OperationError::from_err(e, controller))?, .map_err(|e| OperationError::from_err(e, controller))
.map(|_| Default::default()),
"application/zip" => fs::File::open(path) "application/zip" => fs::File::open(path)
.map(io::BufReader::new) .map(io::BufReader::new)
.map(zip::ZipArchive::new) .map(zip::ZipArchive::new)
@ -75,7 +77,7 @@ pub fn extract(
OperationError::from_kind(OperationErrorType::PasswordRequired, controller) OperationError::from_kind(OperationErrorType::PasswordRequired, controller)
} }
_ => OperationError::from_err(e, controller), _ => OperationError::from_err(e, controller),
})?, }),
#[cfg(feature = "bzip2")] #[cfg(feature = "bzip2")]
"application/x-bzip" "application/x-bzip"
| "application/x-bzip-compressed-tar" | "application/x-bzip-compressed-tar"
@ -85,7 +87,8 @@ pub fn extract(
.map(bzip2::read::BzDecoder::new) .map(bzip2::read::BzDecoder::new)
.map(tar::Archive::new) .map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir)) .and_then(|mut archive| archive.unpack(new_dir))
.map_err(|e| OperationError::from_err(e, controller))?, .map_err(|e| OperationError::from_err(e, controller))
.map(|_| Default::default()),
#[cfg(feature = "lzma-rust2")] #[cfg(feature = "lzma-rust2")]
"application/x-xz" | "application/x-xz-compressed-tar" => { "application/x-xz" | "application/x-xz-compressed-tar" => {
OpReader::new(path, controller.clone()) OpReader::new(path, controller.clone())
@ -93,14 +96,14 @@ pub fn extract(
.map(|reader| lzma_rust2::XzReader::new(reader, true)) .map(|reader| lzma_rust2::XzReader::new(reader, true))
.map(tar::Archive::new) .map(tar::Archive::new)
.and_then(|mut archive| archive.unpack(new_dir)) .and_then(|mut archive| archive.unpack(new_dir))
.map_err(|e| OperationError::from_err(e, controller))?; .map_err(|e| OperationError::from_err(e, controller))
.map(|_| Default::default())
} }
_ => Err(OperationError::from_err( _ => Err(OperationError::from_err(
format!("unsupported mime type {mime:?}"), format!("unsupported mime type {mime:?}"),
controller, controller,
))?, )),
} }
Ok(())
} }
// From https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract, with cancellation and progress added // From https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract, with cancellation and progress added
@ -109,7 +112,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
directory: P, directory: P,
password: Option<&str>, password: Option<&str>,
controller: Controller, controller: Controller,
) -> zip::result::ZipResult<()> { ) -> zip::result::ZipResult<(Vec<PathBuf>, HashSet<PathBuf>)> {
use std::ffi::OsString; use std::ffi::OsString;
use std::fs; use std::fs;
use zip::result::ZipError; use zip::result::ZipError;
@ -282,10 +285,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
} }
} }
// Flush files to disk Ok((written_files, target_dirs))
futures::executor::block_on(async { sync_to_disk(written_files, target_dirs).await });
Ok(())
} }
fn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option<SystemTime> { fn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option<SystemTime> {

View file

@ -967,11 +967,13 @@ impl Operation {
password, password,
} => { } => {
let controller_clone = controller.clone(); let controller_clone = controller.clone();
compio::runtime::spawn_blocking( compio::runtime::spawn(async move {
move || -> Result<OperationSelection, OperationError> { let extracted = compio::runtime::spawn_blocking(move || {
let controller = controller_clone; let controller = controller_clone;
let total_paths = paths.len(); let total_paths = paths.len();
let mut op_sel = OperationSelection::default(); let mut op_sel = OperationSelection::default();
let mut written_files = Vec::new();
let mut target_dirs = std::collections::HashSet::new();
for (i, path) in paths.iter().enumerate() { for (i, path) in paths.iter().enumerate() {
futures::executor::block_on(async { futures::executor::block_on(async {
controller controller
@ -995,16 +997,32 @@ impl Operation {
op_sel.ignored.push(path.clone()); op_sel.ignored.push(path.clone());
op_sel.selected.push(new_dir.clone()); op_sel.selected.push(new_dir.clone());
crate::archive::extract(path, &new_dir, &password, &controller)?; let (files, dirs) = crate::archive::extract(
path,
&new_dir,
&password,
&controller,
)?;
written_files.extend(files);
target_dirs.extend(dirs);
} }
} }
Ok(op_sel) Ok::<_, OperationError>((op_sel, written_files, target_dirs))
}, })
) .await
.map_err(wrap_compio_spawn_error)??;
let (op_sel, written_files, target_dirs) = extracted;
if !written_files.is_empty() || !target_dirs.is_empty() {
sync_to_disk(written_files, target_dirs).await;
}
Ok::<_, OperationError>(op_sel)
})
.await
.map_err(wrap_compio_spawn_error)?
} }
.await
.map_err(wrap_compio_spawn_error)?,
Self::Move { Self::Move {
paths, paths,
to, to,