Merge pull request #779 from ellieplayswow/feature/compress-extract-password-zips

adding in support to extract/compress zip files with passwords
This commit is contained in:
Jeremy Soller 2025-02-03 13:25:12 -07:00 committed by GitHub
commit 2f668b0bd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 249 additions and 96 deletions

View file

@ -39,6 +39,9 @@ resume = Resume
## Compress Dialog ## Compress Dialog
create-archive = Create archive create-archive = Create archive
## Extract Dialog
extract-password-required = Password required
## Empty Trash Dialog ## Empty Trash Dialog
empty-trash = Empty trash empty-trash = Empty trash
empty-trash-warning = Are you sure you want to permanently delete all the items in Trash? empty-trash-warning = Are you sure you want to permanently delete all the items in Trash?

View file

@ -71,6 +71,7 @@ use crate::{
spawn_detached::spawn_detached, spawn_detached::spawn_detached,
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION}, tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
}; };
use crate::operation::{OperationError, OperationErrorType};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Mode { pub enum Mode {
@ -319,7 +320,7 @@ pub enum Message {
PendingCancelAll, PendingCancelAll,
PendingComplete(u64, OperationSelection), PendingComplete(u64, OperationSelection),
PendingDismiss, PendingDismiss,
PendingError(u64, String), PendingError(u64, OperationError),
PendingPause(u64, bool), PendingPause(u64, bool),
PendingPauseAll(bool), PendingPauseAll(bool),
Preview(Option<Entity>), Preview(Option<Entity>),
@ -417,9 +418,14 @@ pub enum DialogPage {
to: PathBuf, to: PathBuf,
name: String, name: String,
archive_type: ArchiveType, archive_type: ArchiveType,
password: Option<String>
}, },
EmptyTrash, EmptyTrash,
FailedOperation(u64), FailedOperation(u64),
ExtractPassword {
id: u64,
password: String
},
MountError { MountError {
mounter_key: MounterKey, mounter_key: MounterKey,
item: MounterItem, item: MounterItem,
@ -1884,6 +1890,7 @@ impl Application for App {
to, to,
name, name,
archive_type, archive_type,
password: None
}); });
return widget::text_input::focus(self.dialog_text_input.clone()); return widget::text_input::focus(self.dialog_text_input.clone());
} }
@ -1961,6 +1968,7 @@ impl Application for App {
to, to,
name, name,
archive_type, archive_type,
password
} => { } => {
let extension = archive_type.extension(); let extension = archive_type.extension();
let name = format!("{}{}", name, extension); let name = format!("{}{}", name, extension);
@ -1969,6 +1977,7 @@ impl Application for App {
paths, paths,
to, to,
archive_type, archive_type,
password
}) })
} }
DialogPage::EmptyTrash => { DialogPage::EmptyTrash => {
@ -1977,6 +1986,21 @@ impl Application for App {
DialogPage::FailedOperation(id) => { DialogPage::FailedOperation(id) => {
log::warn!("TODO: retry operation {}", id); log::warn!("TODO: retry operation {}", id);
} }
DialogPage::ExtractPassword {
id,
password
} => {
let (operation, _, _err) = self.failed_operations.get(&id).unwrap();
let new_op = match &operation {
Operation::Extract { to, paths, .. } => Operation::Extract {
to: to.clone(),
paths: paths.clone(),
password: Some(password)
},
_ => unreachable!()
};
self.operation(new_op);
}
DialogPage::MountError { DialogPage::MountError {
mounter_key, mounter_key,
item, item,
@ -2093,6 +2117,7 @@ impl Application for App {
self.operation(Operation::Extract { self.operation(Operation::Extract {
paths, paths,
to: destination, to: destination,
password: None
}); });
} }
} }
@ -2573,11 +2598,19 @@ impl Application for App {
if let Some((op, controller)) = self.pending_operations.remove(&id) { if let Some((op, controller)) = self.pending_operations.remove(&id) {
// Only show dialog if not cancelled // Only show dialog if not cancelled
if !controller.is_cancelled() { if !controller.is_cancelled() {
self.dialog_pages.push_back(DialogPage::FailedOperation(id)); self.dialog_pages.push_back(
match err.kind {
OperationErrorType::Generic(_) => DialogPage::FailedOperation(id),
OperationErrorType::PasswordRequired => DialogPage::ExtractPassword {
id: id,
password: String::from("")
}
}
);
} }
// Remove from progress // Remove from progress
self.progress_operations.remove(&id); self.progress_operations.remove(&id);
self.failed_operations.insert(id, (op, controller, err)); self.failed_operations.insert(id, (op, controller, err.to_string()));
} }
// Close progress notification if all relavent operations are finished // Close progress notification if all relavent operations are finished
if !self if !self
@ -3572,6 +3605,7 @@ impl Application for App {
to, to,
name, name,
archive_type, archive_type,
password
} => { } => {
let mut dialog = widget::dialog().title(fl!("create-archive")); let mut dialog = widget::dialog().title(fl!("create-archive"));
@ -3604,7 +3638,7 @@ impl Application for App {
let archive_types = ArchiveType::all(); let archive_types = ArchiveType::all();
let selected = archive_types.iter().position(|&x| x == *archive_type); let selected = archive_types.iter().position(|&x| x == *archive_type);
dialog dialog = dialog
.primary_action( .primary_action(
widget::button::suggested(fl!("create")) widget::button::suggested(fl!("create"))
.on_press_maybe(complete_maybe.clone()), .on_press_maybe(complete_maybe.clone()),
@ -3624,9 +3658,10 @@ impl Application for App {
to: to.clone(), to: to.clone(),
name: name.clone(), name: name.clone(),
archive_type: *archive_type, archive_type: *archive_type,
password: password.clone(),
}) })
}) })
.on_submit_maybe(complete_maybe) .on_submit_maybe(complete_maybe.clone())
.into(), .into(),
widget::dropdown(archive_types, selected, move |index| { widget::dropdown(archive_types, selected, move |index| {
Message::DialogUpdate(DialogPage::Compress { Message::DialogUpdate(DialogPage::Compress {
@ -3634,6 +3669,7 @@ impl Application for App {
to: to.clone(), to: to.clone(),
name: name.clone(), name: name.clone(),
archive_type: archive_types[index], archive_type: archive_types[index],
password: password.clone(),
}) })
}) })
.into(), .into(),
@ -3643,7 +3679,29 @@ impl Application for App {
.into(), .into(),
]) ])
.spacing(space_xxs), .spacing(space_xxs),
) );
if *archive_type == ArchiveType::Zip {
let password_unwrapped = password.clone().unwrap_or_else(String::default);
dialog = dialog.control(
widget::column::with_children(vec![
widget::text::body(fl!("password")).into(),
widget::text_input("", password_unwrapped).password().on_input(move |password_unwrapped| {
Message::DialogUpdate(DialogPage::Compress {
paths: paths.clone(),
to: to.clone(),
name: name.clone(),
archive_type: *archive_type,
password: Some(password_unwrapped),
})
})
.on_submit_maybe(complete_maybe)
.into(),
])
);
}
dialog
} }
DialogPage::EmptyTrash => widget::dialog() DialogPage::EmptyTrash => widget::dialog()
.title(fl!("empty-trash")) .title(fl!("empty-trash"))
@ -3668,6 +3726,26 @@ impl Application for App {
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
) )
} }
DialogPage::ExtractPassword {
id,
password
} => {
widget::dialog()
.title(fl!("extract-password-required"))
.icon(widget::icon::from_name("dialog-error").size(64))
.control(widget::text_input("", password).password().on_input(move |password| {
Message::DialogUpdate(DialogPage::ExtractPassword {
id: *id,
password
})
}))
.primary_action(
widget::button::suggested(fl!("extract-here")).on_press(Message::DialogComplete),
)
.secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
)
}
DialogPage::MountError { DialogPage::MountError {
mounter_key: _, mounter_key: _,
item: _, item: _,
@ -4697,8 +4775,9 @@ impl Application for App {
let _ = msg_tx let _ = msg_tx
.lock() .lock()
.await .await
.send(Message::PendingError(id, err.to_string())) .send(Message::PendingError(id, err))
.await; .await;
} }
} }

View file

@ -6,13 +6,16 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use std::collections::VecDeque;
use std::fmt::{Formatter};
use tokio::sync::{mpsc, Mutex as TokioMutex}; use tokio::sync::{mpsc, Mutex as TokioMutex};
use walkdir::WalkDir; use walkdir::WalkDir;
use zip::AesMode::Aes256;
use zip::result::ZipError;
use crate::{ use crate::{
app::{ArchiveType, DialogPage, Message}, app::{ArchiveType, DialogPage, Message},
config::IconSizes, config::IconSizes,
err_str, fl, fl,
mime_icon::mime_for_path, mime_icon::mime_for_path,
spawn_detached::spawn_detached, spawn_detached::spawn_detached,
tab, tab,
@ -91,6 +94,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
archive: &mut zip::ZipArchive<R>, archive: &mut zip::ZipArchive<R>,
directory: P, directory: P,
controller: Controller, controller: Controller,
password: Option<String>
) -> zip::result::ZipResult<()> { ) -> zip::result::ZipResult<()> {
use std::{ffi::OsString, fs}; use std::{ffi::OsString, fs};
use zip::result::ZipError; use zip::result::ZipError;
@ -111,10 +115,13 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
Ok(()) Ok(())
} }
#[cfg(unix)] #[cfg(unix)]
let mut files_by_unix_mode = Vec::new(); let mut files_by_unix_mode = Vec::new();
let mut buffer = vec![0; 4 * 1024 * 1024]; let mut buffer = vec![0; 4 * 1024 * 1024];
let total_files = archive.len(); let total_files = archive.len();
let mut pending_directory_creates = VecDeque::new();
for i in 0..total_files { for i in 0..total_files {
controller controller
.check() .check()
@ -122,7 +129,10 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
controller.set_progress((i as f32) / total_files as f32); controller.set_progress((i as f32) / total_files as f32);
let mut file = archive.by_index(i)?; let mut file = match &password {
None => archive.by_index(i),
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes())
}.map_err(|e| e)?;
let filepath = file let filepath = file
.enclosed_name() .enclosed_name()
.ok_or(ZipError::InvalidArchive("Invalid file path"))?; .ok_or(ZipError::InvalidArchive("Invalid file path"))?;
@ -130,7 +140,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
let outpath = directory.as_ref().join(filepath); let outpath = directory.as_ref().join(filepath);
if file.is_dir() { if file.is_dir() {
make_writable_dir_all(&outpath)?; pending_directory_creates.push_back(outpath.clone());
continue; continue;
} }
let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) { let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
@ -141,10 +151,16 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
None None
}; };
drop(file); drop(file);
if let Some(p) = outpath.parent() {
make_writable_dir_all(p)?;
}
if let Some(target) = symlink_target { if let Some(target) = symlink_target {
// create all pending dirs
while let Some(pending_dir) = pending_directory_creates.pop_front() {
make_writable_dir_all(pending_dir)?;
}
if let Some(p) = outpath.parent() {
make_writable_dir_all(p)?;
}
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::ffi::OsStringExt; use std::os::unix::ffi::OsStringExt;
@ -175,7 +191,20 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
} }
continue; continue;
} }
let mut file = archive.by_index(i)?; let mut file = match &password {
None => archive.by_index(i),
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes())
}.map_err(|e| e)?;
// create all pending dirs
while let Some(pending_dir) = pending_directory_creates.pop_front() {
make_writable_dir_all(pending_dir)?;
}
if let Some(p) = outpath.parent() {
make_writable_dir_all(p)?;
}
let total = file.size(); let total = file.size();
let mut outfile = fs::File::create(&outpath)?; let mut outfile = fs::File::create(&outpath)?;
let mut current = 0; let mut current = 0;
@ -236,9 +265,9 @@ async fn copy_or_move(
moving: bool, moving: bool,
msg_tx: &Arc<TokioMutex<Sender<Message>>>, msg_tx: &Arc<TokioMutex<Sender<Message>>>,
controller: Controller, controller: Controller,
) -> Result<OperationSelection, String> { ) -> Result<OperationSelection, OperationError> {
let msg_tx = msg_tx.clone(); let msg_tx = msg_tx.clone();
tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, OperationError> {
log::info!( log::info!(
"{} {:?} to {:?}", "{} {:?} to {:?}",
if moving { "Move" } else { "Copy" }, if moving { "Move" } else { "Copy" },
@ -293,13 +322,13 @@ async fn copy_or_move(
}); });
} }
context.recursive_copy_or_move(from_to_pairs, moving)?; context.recursive_copy_or_move(from_to_pairs, moving).map_err(OperationError::from_str)?;
Ok(context.op_sel) Ok(context.op_sel)
}) })
.await .await
.map_err(err_str)? .map_err(OperationError::from_str)?
.map_err(err_str) //.map_err(OperationError::from_str)
} }
fn copy_unique_path(from: &Path, to: &Path) -> PathBuf { fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
@ -417,6 +446,7 @@ pub enum Operation {
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
to: PathBuf, to: PathBuf,
archive_type: ArchiveType, archive_type: ArchiveType,
password: Option<String>
}, },
/// Copy items /// Copy items
Copy { Copy {
@ -433,6 +463,7 @@ pub enum Operation {
Extract { Extract {
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
to: PathBuf, to: PathBuf,
password: Option<String>
}, },
/// Move items /// Move items
Move { Move {
@ -459,6 +490,33 @@ pub enum Operation {
}, },
} }
#[derive(Clone, Debug)]
pub enum OperationErrorType {
Generic(String),
PasswordRequired
}
#[derive(Clone, Debug)]
pub struct OperationError {
pub kind: OperationErrorType,
}
impl OperationError {
pub fn from_str<T: ToString>(err: T) -> Self {
OperationError {
kind: OperationErrorType::Generic(err.to_string()),
}
}
}
impl std::fmt::Display for OperationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.kind {
OperationErrorType::Generic(s) => s.fmt(f),
OperationErrorType::PasswordRequired => f.write_str("Password required")
}
}
}
impl Operation { impl Operation {
pub fn pending_text(&self, ratio: f32, state: ControllerState) -> String { pub fn pending_text(&self, ratio: f32, state: ControllerState) -> String {
let percent = (ratio * 100.0) as i32; let percent = (ratio * 100.0) as i32;
@ -490,7 +548,7 @@ impl Operation {
progress = progress() progress = progress()
), ),
Self::EmptyTrash => fl!("emptying-trash", progress = progress()), Self::EmptyTrash => fl!("emptying-trash", progress = progress()),
Self::Extract { paths, to } => fl!( Self::Extract { paths, to, password: _ } => fl!(
"extracting", "extracting",
items = paths.len(), items = paths.len(),
from = paths_parent_name(paths), from = paths_parent_name(paths),
@ -545,7 +603,7 @@ impl Operation {
to = fl!("trash") to = fl!("trash")
), ),
Self::EmptyTrash => fl!("emptied-trash"), Self::EmptyTrash => fl!("emptied-trash"),
Self::Extract { paths, to } => fl!( Self::Extract { paths, to, password: _ } => fl!(
"extracted", "extracted",
items = paths.len(), items = paths.len(),
from = paths_parent_name(paths), from = paths_parent_name(paths),
@ -607,7 +665,7 @@ impl Operation {
self, self,
msg_tx: &Arc<TokioMutex<Sender<Message>>>, msg_tx: &Arc<TokioMutex<Sender<Message>>>,
controller: Controller, controller: Controller,
) -> Result<OperationSelection, String> { ) -> Result<OperationSelection, OperationError> {
let controller_clone = controller.clone(); let controller_clone = controller.clone();
//TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE //TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE
@ -616,10 +674,11 @@ impl Operation {
paths, paths,
to, to,
archive_type, archive_type,
password
} => { } => {
tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, OperationError> {
let Some(relative_root) = to.parent() else { let Some(relative_root) = to.parent() else {
return Err(format!("path {:?} has no parent directory", to)); return Err(OperationError::from_str(format!("path {:?} has no parent directory", to)));
}; };
let op_sel = OperationSelection { let op_sel = OperationSelection {
@ -632,7 +691,7 @@ impl Operation {
if path.is_dir() { if path.is_dir() {
let new_paths_it = WalkDir::new(path).into_iter(); let new_paths_it = WalkDir::new(path).into_iter();
for entry in new_paths_it.skip(1) { for entry in new_paths_it.skip(1) {
let entry = entry.map_err(err_str)?; let entry = entry.map_err(OperationError::from_str)?;
paths.push(entry.into_path()); paths.push(entry.into_path());
} }
} }
@ -646,45 +705,48 @@ impl Operation {
flate2::write::GzEncoder::new(w, flate2::Compression::default()) flate2::write::GzEncoder::new(w, flate2::Compression::default())
}) })
.map(tar::Builder::new) .map(tar::Builder::new)
.map_err(err_str)?; .map_err(OperationError::from_str)?;
let total_paths = paths.len(); let total_paths = paths.len();
for (i, path) in paths.iter().enumerate() { for (i, path) in paths.iter().enumerate() {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
controller.set_progress((i as f32) / total_paths as f32); controller.set_progress((i as f32) / total_paths as f32);
if let Some(relative_path) = if let Some(relative_path) =
path.strip_prefix(relative_root).map_err(err_str)?.to_str() path.strip_prefix(relative_root).map_err(OperationError::from_str)?.to_str()
{ {
archive archive
.append_path_with_name(path, relative_path) .append_path_with_name(path, relative_path)
.map_err(err_str)?; .map_err(OperationError::from_str)?;
} }
} }
archive.finish().map_err(err_str)?; archive.finish().map_err(OperationError::from_str)?;
} }
ArchiveType::Zip => { ArchiveType::Zip => {
let mut archive = fs::File::create(&to) let mut archive = fs::File::create(&to)
.map(io::BufWriter::new) .map(io::BufWriter::new)
.map(zip::ZipWriter::new) .map(zip::ZipWriter::new)
.map_err(err_str)?; .map_err(OperationError::from_str)?;
let total_paths = paths.len(); let total_paths = paths.len();
let mut buffer = vec![0; 4 * 1024 * 1024]; let mut buffer = vec![0; 4 * 1024 * 1024];
for (i, path) in paths.iter().enumerate() { for (i, path) in paths.iter().enumerate() {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
controller.set_progress((i as f32) / total_paths as f32); controller.set_progress((i as f32) / total_paths as f32);
let mut zip_options = zip::write::SimpleFileOptions::default(); let mut zip_options = zip::write::SimpleFileOptions::default();
if password.is_some() {
zip_options = zip_options.with_aes_encryption(Aes256, password.as_deref().unwrap());
}
if let Some(relative_path) = if let Some(relative_path) =
path.strip_prefix(relative_root).map_err(err_str)?.to_str() path.strip_prefix(relative_root).map_err(OperationError::from_str)?.to_str()
{ {
if path.is_file() { if path.is_file() {
let mut file = fs::File::open(path).map_err(err_str)?; let mut file = fs::File::open(path).map_err(OperationError::from_str)?;
let metadata = file.metadata().map_err(err_str)?; let metadata = file.metadata().map_err(OperationError::from_str)?;
let total = metadata.len(); let total = metadata.len();
if total >= 4 * 1024 * 1024 * 1024 { if total >= 4 * 1024 * 1024 * 1024 {
// The large file option must be enabled for files above 4 GiB // The large file option must be enabled for files above 4 GiB
@ -698,16 +760,16 @@ impl Operation {
} }
archive archive
.start_file(relative_path, zip_options) .start_file(relative_path, zip_options)
.map_err(err_str)?; .map_err(OperationError::from_str)?;
let mut current = 0; let mut current = 0;
loop { loop {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
let count = file.read(&mut buffer).map_err(err_str)?; let count = file.read(&mut buffer).map_err(OperationError::from_str)?;
if count == 0 { if count == 0 {
break; break;
} }
archive.write_all(&buffer[..count]).map_err(err_str)?; archive.write_all(&buffer[..count]).map_err(OperationError::from_str)?;
current += count; current += count;
let file_progress = current as f32 / total as f32; let file_progress = current as f32 / total as f32;
@ -718,36 +780,36 @@ impl Operation {
} else { } else {
archive archive
.add_directory(relative_path, zip_options) .add_directory(relative_path, zip_options)
.map_err(err_str)?; .map_err(OperationError::from_str)?;
} }
} }
} }
archive.finish().map_err(err_str)?; archive.finish().map_err(OperationError::from_str)?;
} }
} }
Ok(op_sel) Ok(op_sel)
}) })
.await .await
.map_err(err_str)? .map_err(OperationError::from_str)?
.map_err(err_str)? //.map_err(|e| e)?
} }
Self::Copy { paths, to } => copy_or_move(paths, to, false, msg_tx, controller).await?, Self::Copy { paths, to } => copy_or_move(paths, to, false, msg_tx, controller).await,
Self::Delete { paths } => { Self::Delete { paths } => {
let total = paths.len(); let total = paths.len();
for (i, path) in paths.into_iter().enumerate() { for (i, path) in paths.into_iter().enumerate() {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
controller.set_progress((i as f32) / (total as f32)); controller.set_progress((i as f32) / (total as f32));
let _items_opt = tokio::task::spawn_blocking(|| trash::delete(path)) let _items_opt = tokio::task::spawn_blocking(|| trash::delete(path))
.await .await
.map_err(err_str)? .map_err(OperationError::from_str)?
.map_err(err_str)?; .map_err(OperationError::from_str)?;
//TODO: items_opt allows for easy restore //TODO: items_opt allows for easy restore
} }
OperationSelection::default() Ok(OperationSelection::default())
} }
Self::EmptyTrash => { Self::EmptyTrash => {
#[cfg(any( #[cfg(any(
@ -760,29 +822,29 @@ impl Operation {
) )
))] ))]
{ {
tokio::task::spawn_blocking(move || -> Result<(), String> { tokio::task::spawn_blocking(move || -> Result<(), OperationError> {
let items = trash::os_limited::list().map_err(err_str)?; let items = trash::os_limited::list().map_err(OperationError::from_str)?;
let count = items.len(); let count = items.len();
for (i, item) in items.into_iter().enumerate() { for (i, item) in items.into_iter().enumerate() {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
controller.set_progress(i as f32 / count as f32); controller.set_progress(i as f32 / count as f32);
trash::os_limited::purge_all([item]).map_err(err_str)?; trash::os_limited::purge_all([item]).map_err(OperationError::from_str)?;
} }
Ok(()) Ok(())
}) })
.await .await
.map_err(err_str)??; .map_err(OperationError::from_str)??;
} }
OperationSelection::default() Ok(OperationSelection::default())
} }
Self::Extract { paths, to } => { Self::Extract { paths, to, password } => {
tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, OperationError> {
let total_paths = paths.len(); let total_paths = paths.len();
let mut op_sel = OperationSelection::default(); let mut op_sel = OperationSelection::default();
for (i, path) in paths.iter().enumerate() { for (i, path) in paths.iter().enumerate() {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
controller.set_progress((i as f32) / total_paths as f32); controller.set_progress((i as f32) / total_paths as f32);
@ -801,6 +863,7 @@ impl Operation {
let controller = controller.clone(); let controller = controller.clone();
let mime = mime_for_path(path); let mime = mime_for_path(path);
let password = password.clone();
match mime.essence_str() { match mime.essence_str() {
"application/gzip" | "application/x-compressed-tar" => { "application/gzip" | "application/x-compressed-tar" => {
OpReader::new(path, controller) OpReader::new(path, controller)
@ -808,21 +871,29 @@ impl Operation {
.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(err_str)? .map_err(OperationError::from_str)?
} }
"application/x-tar" => OpReader::new(path, controller) "application/x-tar" => OpReader::new(path, controller)
.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(err_str)?, .map_err(OperationError::from_str)?,
"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)
.map_err(err_str)? .map_err(OperationError::from_str)?
.and_then(move |mut archive| { .and_then(move |mut archive| {
zip_extract(&mut archive, &new_dir, controller) zip_extract(&mut archive, &new_dir, controller, password)
}) })
.map_err(err_str)?, .map_err(|e| match e {
ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED) |
ZipError::InvalidPassword => {
OperationError {
kind: OperationErrorType::PasswordRequired,
}
},
_ => OperationError::from_str(e)
})?,
#[cfg(feature = "bzip2")] #[cfg(feature = "bzip2")]
"application/x-bzip" | "application/x-bzip-compressed-tar" => { "application/x-bzip" | "application/x-bzip-compressed-tar" => {
OpReader::new(path, controller) OpReader::new(path, controller)
@ -830,7 +901,7 @@ impl Operation {
.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(err_str)? .map_err(OperationError::from_str)?
} }
#[cfg(feature = "liblzma")] #[cfg(feature = "liblzma")]
"application/x-xz" | "application/x-xz-compressed-tar" => { "application/x-xz" | "application/x-xz-compressed-tar" => {
@ -839,9 +910,9 @@ impl Operation {
.map(liblzma::read::XzDecoder::new) .map(liblzma::read::XzDecoder::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(err_str)? .map_err(OperationError::from_str)?
} }
_ => Err(format!("unsupported mime type {:?}", mime))?, _ => Err(OperationError::from_str(format!("unsupported mime type {:?}", mime)))?,
} }
} }
} }
@ -849,45 +920,45 @@ impl Operation {
Ok(op_sel) Ok(op_sel)
}) })
.await .await
.map_err(err_str)? .map_err(OperationError::from_str)?
.map_err(err_str)? //.map_err(OperationError::from_str)?
} }
Self::Move { paths, to } => copy_or_move(paths, to, true, msg_tx, controller).await?, Self::Move { paths, to } => copy_or_move(paths, to, true, msg_tx, controller).await,
Self::NewFolder { path } => { Self::NewFolder { path } => {
tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, OperationError> {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
fs::create_dir(&path).map_err(err_str)?; fs::create_dir(&path).map_err(OperationError::from_str)?;
Ok(OperationSelection { Ok(OperationSelection {
ignored: Vec::new(), ignored: Vec::new(),
selected: vec![path], selected: vec![path],
}) })
}) })
.await .await
.map_err(err_str)?? .map_err(OperationError::from_str)?
} }
Self::NewFile { path } => { Self::NewFile { path } => {
tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, OperationError> {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
fs::File::create(&path).map_err(err_str)?; fs::File::create(&path).map_err(OperationError::from_str)?;
Ok(OperationSelection { Ok(OperationSelection {
ignored: Vec::new(), ignored: Vec::new(),
selected: vec![path], selected: vec![path],
}) })
}) })
.await .await
.map_err(err_str)?? .map_err(OperationError::from_str)?
} }
Self::Rename { from, to } => { Self::Rename { from, to } => {
tokio::task::spawn_blocking(move || -> Result<OperationSelection, String> { tokio::task::spawn_blocking(move || -> Result<OperationSelection, OperationError> {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
fs::rename(&from, &to).map_err(err_str)?; fs::rename(&from, &to).map_err(OperationError::from_str)?;
Ok(OperationSelection { Ok(OperationSelection {
ignored: vec![from], ignored: vec![from],
selected: vec![to], selected: vec![to],
}) })
}) })
.await .await
.map_err(err_str)?? .map_err(OperationError::from_str)?
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Self::Restore { .. } => { Self::Restore { .. } => {
@ -899,7 +970,7 @@ impl Operation {
let total = items.len(); let total = items.len();
let mut paths = Vec::with_capacity(total); let mut paths = Vec::with_capacity(total);
for (i, item) in items.into_iter().enumerate() { for (i, item) in items.into_iter().enumerate() {
controller.check()?; controller.check().map_err(OperationError::from_str)?;
controller.set_progress((i as f32) / (total as f32)); controller.set_progress((i as f32) / (total as f32));
@ -907,47 +978,47 @@ impl Operation {
tokio::task::spawn_blocking(|| trash::os_limited::restore_all([item])) tokio::task::spawn_blocking(|| trash::os_limited::restore_all([item]))
.await .await
.map_err(err_str)? .map_err(OperationError::from_str)?
.map_err(err_str)?; .map_err(OperationError::from_str)?;
} }
OperationSelection { Ok(OperationSelection {
ignored: Vec::new(), ignored: Vec::new(),
selected: paths, selected: paths,
} })
} }
Self::SetExecutableAndLaunch { path } => { Self::SetExecutableAndLaunch { path } => {
tokio::task::spawn_blocking(move || -> Result<(), String> { tokio::task::spawn_blocking(move || -> Result<(), OperationError> {
//TODO: what to do on non-Unix systems? //TODO: what to do on non-Unix systems?
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
controller.check()?; controller.check().map_err(OperationError::from_str)?;
let mut perms = fs::metadata(&path).map_err(err_str)?.permissions(); let mut perms = fs::metadata(&path).map_err(OperationError::from_str)?.permissions();
let current_mode = perms.mode(); let current_mode = perms.mode();
let new_mode = current_mode | 0o111; let new_mode = current_mode | 0o111;
perms.set_mode(new_mode); perms.set_mode(new_mode);
fs::set_permissions(&path, perms).map_err(err_str)?; fs::set_permissions(&path, perms).map_err(OperationError::from_str)?;
} }
controller.check()?; controller.check().map_err(OperationError::from_str)?;
let mut command = std::process::Command::new(path); let mut command = std::process::Command::new(path);
spawn_detached(&mut command).map_err(err_str)?; spawn_detached(&mut command).map_err(OperationError::from_str)?;
Ok(()) Ok(())
}) })
.await .await
.map_err(err_str)? .map_err(OperationError::from_str)?
.map_err(err_str)?; .map_err(|e| e)?;
OperationSelection::default() Ok(OperationSelection::default())
} }
}; };
controller_clone.set_progress(100.0); controller_clone.set_progress(100.0);
Ok(paths) paths
} }
} }