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:
commit
2f668b0bd6
3 changed files with 249 additions and 96 deletions
|
|
@ -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?
|
||||||
|
|
|
||||||
93
src/app.rs
93
src/app.rs
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue