2024-07-08 11:51:07 -06:00
|
|
|
use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt};
|
2024-05-09 22:34:16 -04:00
|
|
|
use std::{
|
2024-07-11 15:31:32 -06:00
|
|
|
borrow::Cow,
|
2024-05-09 22:34:16 -04:00
|
|
|
fs,
|
2024-09-05 17:34:03 -05:00
|
|
|
io::{self, Read, Write},
|
2024-07-08 14:35:20 -06:00
|
|
|
path::{Path, PathBuf},
|
2024-11-15 09:47:03 -07:00
|
|
|
sync::{Arc, Condvar, Mutex},
|
2024-05-09 22:34:16 -04:00
|
|
|
};
|
2024-11-15 09:47:03 -07:00
|
|
|
use tokio::sync::{mpsc, Mutex as TokioMutex};
|
2024-09-05 17:34:03 -05:00
|
|
|
use walkdir::WalkDir;
|
2024-01-29 11:58:50 -07:00
|
|
|
|
2024-11-14 16:00:35 -07:00
|
|
|
use self::reader::OpReader;
|
2024-11-13 14:36:11 -07:00
|
|
|
use self::recursive::Context;
|
2024-07-08 11:51:07 -06:00
|
|
|
use crate::{
|
2024-09-05 17:34:03 -05:00
|
|
|
app::{ArchiveType, DialogPage, Message},
|
2024-07-08 11:51:07 -06:00
|
|
|
config::IconSizes,
|
2024-09-13 15:13:37 -06:00
|
|
|
err_str, fl,
|
2024-09-10 11:50:14 -06:00
|
|
|
mime_icon::mime_for_path,
|
2024-10-03 11:53:01 -06:00
|
|
|
spawn_detached::spawn_detached,
|
2024-09-10 11:50:14 -06:00
|
|
|
tab,
|
2024-07-08 11:51:07 -06:00
|
|
|
};
|
2024-01-30 10:47:41 -07:00
|
|
|
|
2024-11-14 16:00:35 -07:00
|
|
|
pub mod reader;
|
2024-11-13 14:36:11 -07:00
|
|
|
pub mod recursive;
|
|
|
|
|
|
2024-07-08 13:43:11 -06:00
|
|
|
fn handle_replace(
|
2024-11-15 09:47:03 -07:00
|
|
|
msg_tx: &Arc<TokioMutex<Sender<Message>>>,
|
2024-07-08 13:43:11 -06:00
|
|
|
file_from: PathBuf,
|
|
|
|
|
file_to: PathBuf,
|
2024-07-08 14:35:20 -06:00
|
|
|
multiple: bool,
|
2024-07-08 13:43:11 -06:00
|
|
|
) -> ReplaceResult {
|
|
|
|
|
let item_from = match tab::item_from_path(file_from, IconSizes::default()) {
|
|
|
|
|
Ok(ok) => ok,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("{}", err);
|
|
|
|
|
return ReplaceResult::Cancel;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let item_to = match tab::item_from_path(file_to, IconSizes::default()) {
|
|
|
|
|
Ok(ok) => ok,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("{}", err);
|
|
|
|
|
return ReplaceResult::Cancel;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
executor::block_on(async {
|
|
|
|
|
let (tx, mut rx) = mpsc::channel(1);
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::DialogPush(DialogPage::Replace {
|
|
|
|
|
from: item_from,
|
|
|
|
|
to: item_to,
|
2024-07-08 14:35:20 -06:00
|
|
|
multiple,
|
|
|
|
|
apply_to_all: false,
|
2024-07-08 13:43:11 -06:00
|
|
|
tx,
|
|
|
|
|
}))
|
|
|
|
|
.await;
|
|
|
|
|
rx.recv().await.unwrap_or(ReplaceResult::Cancel)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 19:09:27 +05:30
|
|
|
fn get_directory_name(file_name: &str) -> &str {
|
|
|
|
|
const SUPPORTED_EXTENSIONS: [&str; 4] = [".tar.gz", ".tgz", ".tar", ".zip"];
|
|
|
|
|
|
|
|
|
|
for ext in &SUPPORTED_EXTENSIONS {
|
|
|
|
|
if file_name.ends_with(ext) {
|
|
|
|
|
return &file_name[..file_name.len() - ext.len()];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
file_name
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-14 16:00:35 -07:00
|
|
|
// From https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract, with cancellation and progress added
|
|
|
|
|
fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
|
|
|
|
archive: &mut zip::ZipArchive<R>,
|
|
|
|
|
directory: P,
|
|
|
|
|
id: u64,
|
2024-11-15 09:47:03 -07:00
|
|
|
msg_tx: Arc<TokioMutex<Sender<Message>>>,
|
|
|
|
|
controller: Controller,
|
2024-11-14 16:00:35 -07:00
|
|
|
) -> zip::result::ZipResult<()> {
|
|
|
|
|
use std::{ffi::OsString, fs};
|
|
|
|
|
use zip::result::ZipError;
|
|
|
|
|
|
|
|
|
|
fn make_writable_dir_all<T: AsRef<Path>>(outpath: T) -> Result<(), ZipError> {
|
|
|
|
|
fs::create_dir_all(outpath.as_ref())?;
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
// Dirs must be writable until all normal files are extracted
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
std::fs::set_permissions(
|
|
|
|
|
outpath.as_ref(),
|
|
|
|
|
std::fs::Permissions::from_mode(
|
|
|
|
|
0o700 | std::fs::metadata(outpath.as_ref())?.permissions().mode(),
|
|
|
|
|
),
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
let mut files_by_unix_mode = Vec::new();
|
|
|
|
|
let mut buffer = vec![0; 4 * 1024 * 1024];
|
|
|
|
|
let total_files = archive.len();
|
|
|
|
|
for i in 0..total_files {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller
|
|
|
|
|
.check()
|
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
2024-11-14 16:00:35 -07:00
|
|
|
|
|
|
|
|
executor::block_on(async {
|
|
|
|
|
let total_progress = (i as f32) / total_files as f32;
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut file = archive.by_index(i)?;
|
|
|
|
|
let filepath = file
|
|
|
|
|
.enclosed_name()
|
|
|
|
|
.ok_or(ZipError::InvalidArchive("Invalid file path"))?;
|
|
|
|
|
|
|
|
|
|
let outpath = directory.as_ref().join(filepath);
|
|
|
|
|
|
|
|
|
|
if file.is_dir() {
|
|
|
|
|
make_writable_dir_all(&outpath)?;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
|
|
|
|
|
let mut target = Vec::with_capacity(file.size() as usize);
|
|
|
|
|
file.read_to_end(&mut target)?;
|
|
|
|
|
Some(target)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
drop(file);
|
|
|
|
|
if let Some(p) = outpath.parent() {
|
|
|
|
|
make_writable_dir_all(p)?;
|
|
|
|
|
}
|
|
|
|
|
if let Some(target) = symlink_target {
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::ffi::OsStringExt;
|
|
|
|
|
let target = OsString::from_vec(target);
|
|
|
|
|
std::os::unix::fs::symlink(&target, outpath.as_path())?;
|
|
|
|
|
}
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
{
|
|
|
|
|
let Ok(target) = String::from_utf8(target) else {
|
|
|
|
|
return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink target"));
|
|
|
|
|
};
|
|
|
|
|
let target = target.into_boxed_str();
|
|
|
|
|
let target_is_dir_from_archive =
|
|
|
|
|
archive.shared.files.contains_key(&target) && is_dir(&target);
|
|
|
|
|
let target_path = directory.as_ref().join(OsString::from(target.to_string()));
|
|
|
|
|
let target_is_dir = if target_is_dir_from_archive {
|
|
|
|
|
true
|
|
|
|
|
} else if let Ok(meta) = std::fs::metadata(&target_path) {
|
|
|
|
|
meta.is_dir()
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
};
|
|
|
|
|
if target_is_dir {
|
|
|
|
|
std::os::windows::fs::symlink_dir(target_path, outpath.as_path())?;
|
|
|
|
|
} else {
|
|
|
|
|
std::os::windows::fs::symlink_file(target_path, outpath.as_path())?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let mut file = archive.by_index(i)?;
|
|
|
|
|
let total = file.size();
|
|
|
|
|
let mut outfile = fs::File::create(&outpath)?;
|
|
|
|
|
let mut current = 0;
|
|
|
|
|
loop {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller
|
|
|
|
|
.check()
|
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
2024-11-14 16:00:35 -07:00
|
|
|
|
|
|
|
|
let count = file.read(&mut buffer)?;
|
|
|
|
|
if count == 0 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
outfile.write_all(&buffer[..count])?;
|
|
|
|
|
current += count as u64;
|
|
|
|
|
|
|
|
|
|
if current < total {
|
|
|
|
|
executor::block_on(async {
|
|
|
|
|
let file_progress = current as f32 / total as f32;
|
|
|
|
|
let total_progress = (i as f32 + file_progress) / total_files as f32;
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
outfile.sync_all()?;
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
// Check for real permissions, which we'll set in a second pass
|
|
|
|
|
if let Some(mode) = file.unix_mode() {
|
|
|
|
|
files_by_unix_mode.push((outpath.clone(), mode));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::cmp::Reverse;
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
|
|
|
|
|
if files_by_unix_mode.len() > 1 {
|
|
|
|
|
// Ensure we update children's permissions before making a parent unwritable
|
|
|
|
|
files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.clone()));
|
|
|
|
|
}
|
|
|
|
|
for (path, mode) in files_by_unix_mode.into_iter() {
|
|
|
|
|
fs::set_permissions(&path, fs::Permissions::from_mode(mode))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 11:07:26 -07:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
pub enum ControllerState {
|
|
|
|
|
Cancelled,
|
|
|
|
|
Paused,
|
|
|
|
|
Running,
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:30:25 -07:00
|
|
|
#[derive(Debug)]
|
2024-11-15 09:47:03 -07:00
|
|
|
struct ControllerInner {
|
2024-11-15 11:07:26 -07:00
|
|
|
state: Mutex<ControllerState>,
|
2024-11-15 09:47:03 -07:00
|
|
|
condvar: Condvar,
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:30:25 -07:00
|
|
|
#[derive(Debug)]
|
2024-11-15 09:47:03 -07:00
|
|
|
pub struct Controller {
|
2024-11-15 17:30:25 -07:00
|
|
|
primary: bool,
|
2024-11-15 09:47:03 -07:00
|
|
|
inner: Arc<ControllerInner>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Controller {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
2024-11-15 17:30:25 -07:00
|
|
|
primary: true,
|
2024-11-15 09:47:03 -07:00
|
|
|
inner: Arc::new(ControllerInner {
|
2024-11-15 11:07:26 -07:00
|
|
|
state: Mutex::new(ControllerState::Running),
|
2024-11-15 09:47:03 -07:00
|
|
|
condvar: Condvar::new(),
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn check(&self) -> Result<(), String> {
|
|
|
|
|
let mut state = self.inner.state.lock().unwrap();
|
|
|
|
|
loop {
|
2024-11-15 11:07:26 -07:00
|
|
|
match *state {
|
|
|
|
|
ControllerState::Cancelled => return Err(fl!("cancelled")),
|
|
|
|
|
ControllerState::Paused => {
|
|
|
|
|
state = self.inner.condvar.wait(state).unwrap();
|
|
|
|
|
}
|
|
|
|
|
ControllerState::Running => return Ok(()),
|
2024-11-15 09:47:03 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 11:07:26 -07:00
|
|
|
pub fn state(&self) -> ControllerState {
|
|
|
|
|
*self.inner.state.lock().unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_state(&self, state: ControllerState) {
|
|
|
|
|
*self.inner.state.lock().unwrap() = state;
|
|
|
|
|
self.inner.condvar.notify_all();
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 09:47:03 -07:00
|
|
|
pub fn is_cancelled(&self) -> bool {
|
2024-11-15 11:07:26 -07:00
|
|
|
matches!(self.state(), ControllerState::Cancelled)
|
2024-11-15 09:47:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cancel(&self) {
|
2024-11-15 11:07:26 -07:00
|
|
|
self.set_state(ControllerState::Cancelled);
|
2024-11-15 09:47:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_paused(&self) -> bool {
|
2024-11-15 11:07:26 -07:00
|
|
|
matches!(self.state(), ControllerState::Paused)
|
2024-11-15 09:47:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn pause(&self) {
|
2024-11-15 11:07:26 -07:00
|
|
|
self.set_state(ControllerState::Paused);
|
2024-11-15 09:47:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn unpause(&self) {
|
2024-11-15 11:07:26 -07:00
|
|
|
//TODO: ensure this does not override Cancel?
|
|
|
|
|
self.set_state(ControllerState::Running);
|
2024-11-15 09:47:03 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:30:25 -07:00
|
|
|
impl Clone for Controller {
|
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
primary: false,
|
|
|
|
|
inner: self.inner.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for Controller {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
// Cancel operations if primary controller is dropped
|
|
|
|
|
if self.primary {
|
|
|
|
|
self.cancel();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 13:43:11 -06:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
|
|
|
pub enum ReplaceResult {
|
2024-07-08 14:35:20 -06:00
|
|
|
Replace(bool),
|
|
|
|
|
KeepBoth,
|
|
|
|
|
Skip(bool),
|
2024-07-08 13:43:11 -06:00
|
|
|
Cancel,
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-30 10:47:41 -07:00
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
2024-01-29 11:58:50 -07:00
|
|
|
pub enum Operation {
|
2024-09-05 17:34:03 -05:00
|
|
|
/// Compress files
|
|
|
|
|
Compress {
|
|
|
|
|
paths: Vec<PathBuf>,
|
|
|
|
|
to: PathBuf,
|
|
|
|
|
archive_type: ArchiveType,
|
|
|
|
|
},
|
2024-01-30 10:47:41 -07:00
|
|
|
/// Copy items
|
2024-02-27 13:25:50 -07:00
|
|
|
Copy {
|
|
|
|
|
paths: Vec<PathBuf>,
|
|
|
|
|
to: PathBuf,
|
|
|
|
|
},
|
2024-01-30 10:47:41 -07:00
|
|
|
/// Move items to the trash
|
2024-02-27 13:25:50 -07:00
|
|
|
Delete {
|
|
|
|
|
paths: Vec<PathBuf>,
|
|
|
|
|
},
|
2024-05-09 13:24:06 -06:00
|
|
|
/// Empty the trash
|
|
|
|
|
EmptyTrash,
|
2024-08-15 12:32:12 -05:00
|
|
|
/// Uncompress files
|
|
|
|
|
Extract {
|
|
|
|
|
paths: Vec<PathBuf>,
|
|
|
|
|
to: PathBuf,
|
|
|
|
|
},
|
2024-01-30 10:47:41 -07:00
|
|
|
/// Move items
|
2024-02-27 13:25:50 -07:00
|
|
|
Move {
|
|
|
|
|
paths: Vec<PathBuf>,
|
|
|
|
|
to: PathBuf,
|
|
|
|
|
},
|
|
|
|
|
NewFile {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
},
|
|
|
|
|
NewFolder {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
},
|
2024-02-28 15:07:50 -07:00
|
|
|
Rename {
|
|
|
|
|
from: PathBuf,
|
|
|
|
|
to: PathBuf,
|
|
|
|
|
},
|
2024-01-29 11:58:50 -07:00
|
|
|
/// Restore a path from the trash
|
2024-02-27 13:25:50 -07:00
|
|
|
Restore {
|
|
|
|
|
paths: Vec<trash::TrashItem>,
|
|
|
|
|
},
|
2024-10-03 11:53:01 -06:00
|
|
|
/// Set executable and launch
|
|
|
|
|
SetExecutableAndLaunch {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
},
|
2024-01-29 11:58:50 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-27 11:04:18 -06:00
|
|
|
async fn copy_or_move(
|
|
|
|
|
paths: Vec<PathBuf>,
|
|
|
|
|
to: PathBuf,
|
|
|
|
|
moving: bool,
|
|
|
|
|
id: u64,
|
2024-11-15 09:47:03 -07:00
|
|
|
msg_tx: &Arc<TokioMutex<Sender<Message>>>,
|
|
|
|
|
controller: Controller,
|
2024-09-27 11:04:18 -06:00
|
|
|
) -> Result<(), String> {
|
2024-11-13 14:36:11 -07:00
|
|
|
let msg_tx = msg_tx.clone();
|
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
|
|
|
|
log::info!(
|
|
|
|
|
"{} {:?} to {:?}",
|
|
|
|
|
if moving { "Move" } else { "Copy" },
|
|
|
|
|
paths,
|
|
|
|
|
to
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Handle duplicate file names by renaming paths
|
|
|
|
|
let from_to_pairs: Vec<(PathBuf, PathBuf)> = paths
|
2024-09-27 11:04:18 -06:00
|
|
|
.into_iter()
|
|
|
|
|
.zip(std::iter::repeat(to.as_path()))
|
2024-11-13 14:36:11 -07:00
|
|
|
.filter_map(|(from, to)| {
|
2024-09-27 11:04:18 -06:00
|
|
|
if matches!(from.parent(), Some(parent) if parent == to) && !moving {
|
|
|
|
|
// `from`'s parent is equal to `to` which means we're copying to the same
|
|
|
|
|
// directory (duplicating files)
|
|
|
|
|
let to = copy_unique_path(&from, &to);
|
2024-11-13 14:36:11 -07:00
|
|
|
Some((from, to))
|
|
|
|
|
} else if let Some(name) = from.file_name() {
|
2024-09-27 11:04:18 -06:00
|
|
|
let to = to.join(name);
|
2024-11-13 14:36:11 -07:00
|
|
|
Some((from, to))
|
2024-09-27 11:04:18 -06:00
|
|
|
} else {
|
2024-11-13 14:36:11 -07:00
|
|
|
//TODO: how to handle from missing file name?
|
|
|
|
|
None
|
2024-09-27 11:04:18 -06:00
|
|
|
}
|
|
|
|
|
})
|
2024-11-13 14:36:11 -07:00
|
|
|
.collect();
|
|
|
|
|
|
2024-11-15 09:47:03 -07:00
|
|
|
let mut context = Context::new(controller);
|
2024-11-13 14:36:11 -07:00
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let msg_tx = msg_tx.clone();
|
2024-11-14 16:00:35 -07:00
|
|
|
context = context.on_progress(move |_op, progress| {
|
2024-11-13 14:36:11 -07:00
|
|
|
let item_progress = match progress.total_bytes {
|
|
|
|
|
Some(total_bytes) => {
|
|
|
|
|
if total_bytes == 0 {
|
|
|
|
|
1.0
|
|
|
|
|
} else {
|
|
|
|
|
progress.current_bytes as f32 / total_bytes as f32
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => 0.0,
|
2024-09-27 11:04:18 -06:00
|
|
|
};
|
2024-11-13 14:36:11 -07:00
|
|
|
let total_progress =
|
|
|
|
|
(item_progress + progress.current_ops as f32) / progress.total_ops as f32;
|
2024-09-27 11:04:18 -06:00
|
|
|
executor::block_on(async {
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
|
|
|
|
.await;
|
|
|
|
|
})
|
2024-11-13 14:36:11 -07:00
|
|
|
});
|
|
|
|
|
}
|
2024-09-27 11:04:18 -06:00
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
{
|
|
|
|
|
let msg_tx = msg_tx.clone();
|
|
|
|
|
context = context.on_replace(move |op| {
|
|
|
|
|
handle_replace(&msg_tx, op.from.clone(), op.to.clone(), true)
|
|
|
|
|
});
|
2024-09-27 11:04:18 -06:00
|
|
|
}
|
2024-11-13 14:36:11 -07:00
|
|
|
|
|
|
|
|
context.recursive_copy_or_move(from_to_pairs, moving)?;
|
|
|
|
|
|
2024-09-27 11:04:18 -06:00
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
.map_err(err_str)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 14:35:20 -06:00
|
|
|
fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
|
2024-10-03 16:45:14 +05:30
|
|
|
// List of compound extensions to check
|
|
|
|
|
const COMPOUND_EXTENSIONS: &[&str] = &[
|
|
|
|
|
".tar.gz",
|
|
|
|
|
".tar.bz2",
|
|
|
|
|
".tar.xz",
|
|
|
|
|
".tar.zst",
|
|
|
|
|
".tar.lz",
|
|
|
|
|
".tar.lzma",
|
|
|
|
|
".tar.sz",
|
|
|
|
|
".tar.lzo",
|
|
|
|
|
".tar.br",
|
|
|
|
|
".tar.Z",
|
|
|
|
|
".tar.pz",
|
|
|
|
|
];
|
|
|
|
|
|
2024-07-08 14:35:20 -06:00
|
|
|
let mut to = to.to_owned();
|
2024-09-28 19:28:33 +05:30
|
|
|
if let Some(file_name) = from.file_name().and_then(|name| name.to_str()) {
|
2024-10-03 16:45:14 +05:30
|
|
|
let (stem, ext) = if from.is_dir() {
|
2024-09-29 04:00:18 +05:30
|
|
|
(file_name.to_string(), None)
|
2024-10-03 16:45:14 +05:30
|
|
|
} else {
|
|
|
|
|
let file_name = file_name.to_string();
|
|
|
|
|
COMPOUND_EXTENSIONS
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|&&ext| file_name.ends_with(ext))
|
|
|
|
|
.map(|&ext| {
|
|
|
|
|
(
|
|
|
|
|
file_name.strip_suffix(ext).unwrap().to_string(),
|
|
|
|
|
Some(ext[1..].to_string()),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
from.file_stem()
|
|
|
|
|
.and_then(|s| s.to_str())
|
|
|
|
|
.map(|stem| {
|
|
|
|
|
(
|
|
|
|
|
stem.to_string(),
|
|
|
|
|
from.extension()
|
|
|
|
|
.and_then(|e| e.to_str())
|
|
|
|
|
.map(|e| e.to_string()),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or((file_name, None))
|
|
|
|
|
})
|
2024-09-29 04:00:18 +05:30
|
|
|
};
|
|
|
|
|
|
2024-10-03 16:45:14 +05:30
|
|
|
for n in 0.. {
|
2024-09-28 19:28:33 +05:30
|
|
|
let new_name = if n == 0 {
|
|
|
|
|
file_name.to_string()
|
|
|
|
|
} else {
|
2024-10-03 16:45:14 +05:30
|
|
|
match ext {
|
|
|
|
|
Some(ref ext) => format!("{} ({} {}).{}", stem, fl!("copy_noun"), n, ext),
|
|
|
|
|
None => format!("{} ({} {})", stem, fl!("copy_noun"), n),
|
2024-09-29 04:00:18 +05:30
|
|
|
}
|
2024-09-28 19:28:33 +05:30
|
|
|
};
|
|
|
|
|
|
2024-10-03 16:45:14 +05:30
|
|
|
to = to.join(&new_name);
|
2024-07-08 14:35:20 -06:00
|
|
|
|
|
|
|
|
if !matches!(to.try_exists(), Ok(true)) {
|
2024-10-03 16:45:14 +05:30
|
|
|
break;
|
2024-07-08 14:35:20 -06:00
|
|
|
}
|
|
|
|
|
// Continue if a copy with index exists
|
|
|
|
|
to.pop();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-03 16:45:14 +05:30
|
|
|
to
|
2024-07-08 14:35:20 -06:00
|
|
|
}
|
|
|
|
|
|
2024-07-11 15:31:32 -06:00
|
|
|
fn file_name<'a>(path: &'a Path) -> Cow<'a, str> {
|
|
|
|
|
path.file_name()
|
|
|
|
|
.map_or_else(|| fl!("unknown-folder").into(), |x| x.to_string_lossy())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parent_name<'a>(path: &'a Path) -> Cow<'a, str> {
|
|
|
|
|
let Some(parent) = path.parent() else {
|
|
|
|
|
return fl!("unknown-folder").into();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
file_name(parent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn paths_parent_name<'a>(paths: &'a Vec<PathBuf>) -> Cow<'a, str> {
|
|
|
|
|
let Some(first_path) = paths.first() else {
|
|
|
|
|
return fl!("unknown-folder").into();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some(parent) = first_path.parent() else {
|
|
|
|
|
return fl!("unknown-folder").into();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for path in paths.iter() {
|
|
|
|
|
//TODO: is it possible to have different parents, and what should be returned?
|
|
|
|
|
if path.parent() != Some(parent) {
|
|
|
|
|
return fl!("unknown-folder").into();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_name(parent)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-29 11:58:50 -07:00
|
|
|
impl Operation {
|
2024-11-15 11:07:26 -07:00
|
|
|
pub fn pending_text(&self, percent: i32, state: ControllerState) -> String {
|
|
|
|
|
let progress = || match state {
|
|
|
|
|
ControllerState::Running => fl!("progress", percent = percent),
|
|
|
|
|
ControllerState::Paused => fl!("progress-paused", percent = percent),
|
|
|
|
|
ControllerState::Cancelled => fl!("progress-cancelled", percent = percent),
|
|
|
|
|
};
|
2024-07-11 15:31:32 -06:00
|
|
|
match self {
|
2024-09-05 17:34:03 -05:00
|
|
|
Self::Compress { paths, to, .. } => fl!(
|
|
|
|
|
"compressing",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
2024-11-14 14:43:45 -07:00
|
|
|
to = file_name(to),
|
2024-11-15 11:07:26 -07:00
|
|
|
progress = progress()
|
2024-09-05 17:34:03 -05:00
|
|
|
),
|
2024-07-11 15:31:32 -06:00
|
|
|
Self::Copy { paths, to } => fl!(
|
|
|
|
|
"copying",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
2024-11-14 14:43:45 -07:00
|
|
|
to = file_name(to),
|
2024-11-15 11:07:26 -07:00
|
|
|
progress = progress()
|
2024-07-11 15:31:32 -06:00
|
|
|
),
|
|
|
|
|
Self::Delete { paths } => fl!(
|
|
|
|
|
"moving",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
2024-11-14 14:43:45 -07:00
|
|
|
to = fl!("trash"),
|
2024-11-15 11:07:26 -07:00
|
|
|
progress = progress()
|
2024-07-11 15:31:32 -06:00
|
|
|
),
|
2024-11-15 11:07:26 -07:00
|
|
|
Self::EmptyTrash => fl!("emptying-trash", progress = progress()),
|
2024-08-15 12:32:12 -05:00
|
|
|
Self::Extract { paths, to } => fl!(
|
|
|
|
|
"extracting",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
2024-11-14 14:43:45 -07:00
|
|
|
to = file_name(to),
|
2024-11-15 11:07:26 -07:00
|
|
|
progress = progress()
|
2024-08-15 12:32:12 -05:00
|
|
|
),
|
2024-07-11 15:31:32 -06:00
|
|
|
Self::Move { paths, to } => fl!(
|
|
|
|
|
"moving",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
2024-11-14 14:43:45 -07:00
|
|
|
to = file_name(to),
|
2024-11-15 11:07:26 -07:00
|
|
|
progress = progress()
|
2024-07-11 15:31:32 -06:00
|
|
|
),
|
|
|
|
|
Self::NewFile { path } => fl!(
|
|
|
|
|
"creating",
|
|
|
|
|
name = file_name(path),
|
|
|
|
|
parent = parent_name(path)
|
|
|
|
|
),
|
|
|
|
|
Self::NewFolder { path } => fl!(
|
|
|
|
|
"creating",
|
|
|
|
|
name = file_name(path),
|
|
|
|
|
parent = parent_name(path)
|
|
|
|
|
),
|
|
|
|
|
Self::Rename { from, to } => {
|
|
|
|
|
fl!("renaming", from = file_name(from), to = file_name(to))
|
|
|
|
|
}
|
2024-11-15 11:07:26 -07:00
|
|
|
Self::Restore { paths } => fl!("restoring", items = paths.len(), progress = progress()),
|
2024-10-03 11:53:01 -06:00
|
|
|
Self::SetExecutableAndLaunch { path } => {
|
|
|
|
|
fl!("setting-executable-and-launching", name = file_name(path))
|
|
|
|
|
}
|
2024-07-11 15:31:32 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn completed_text(&self) -> String {
|
|
|
|
|
match self {
|
2024-09-05 17:34:03 -05:00
|
|
|
Self::Compress { paths, to, .. } => fl!(
|
|
|
|
|
"compressed",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
|
|
|
|
to = file_name(to)
|
|
|
|
|
),
|
2024-07-11 15:31:32 -06:00
|
|
|
Self::Copy { paths, to } => fl!(
|
|
|
|
|
"copied",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
|
|
|
|
to = file_name(to)
|
|
|
|
|
),
|
|
|
|
|
Self::Delete { paths } => fl!(
|
|
|
|
|
"moved",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
|
|
|
|
to = fl!("trash")
|
|
|
|
|
),
|
|
|
|
|
Self::EmptyTrash => fl!("emptied-trash"),
|
2024-08-15 12:32:12 -05:00
|
|
|
Self::Extract { paths, to } => fl!(
|
|
|
|
|
"extracted",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
|
|
|
|
to = file_name(to)
|
|
|
|
|
),
|
2024-07-11 15:31:32 -06:00
|
|
|
Self::Move { paths, to } => fl!(
|
|
|
|
|
"moved",
|
|
|
|
|
items = paths.len(),
|
|
|
|
|
from = paths_parent_name(paths),
|
|
|
|
|
to = file_name(to)
|
|
|
|
|
),
|
|
|
|
|
Self::NewFile { path } => fl!(
|
|
|
|
|
"created",
|
|
|
|
|
name = file_name(path),
|
|
|
|
|
parent = parent_name(path)
|
|
|
|
|
),
|
|
|
|
|
Self::NewFolder { path } => fl!(
|
|
|
|
|
"created",
|
|
|
|
|
name = file_name(path),
|
|
|
|
|
parent = parent_name(path)
|
|
|
|
|
),
|
|
|
|
|
Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)),
|
|
|
|
|
Self::Restore { paths } => fl!("restored", items = paths.len()),
|
2024-10-03 11:53:01 -06:00
|
|
|
Self::SetExecutableAndLaunch { path } => {
|
|
|
|
|
fl!("set-executable-and-launched", name = file_name(path))
|
|
|
|
|
}
|
2024-07-11 15:31:32 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-14 14:43:45 -07:00
|
|
|
pub fn show_progress_notification(&self) -> bool {
|
|
|
|
|
// Long running operations show a progress notification
|
|
|
|
|
match self {
|
|
|
|
|
Self::Compress { .. }
|
|
|
|
|
| Self::Copy { .. }
|
|
|
|
|
| Self::Delete { .. }
|
|
|
|
|
| Self::EmptyTrash
|
|
|
|
|
| Self::Extract { .. }
|
|
|
|
|
| Self::Move { .. }
|
|
|
|
|
| Self::Restore { .. } => true,
|
|
|
|
|
Self::NewFile { .. }
|
|
|
|
|
| Self::NewFolder { .. }
|
|
|
|
|
| Self::Rename { .. }
|
|
|
|
|
| Self::SetExecutableAndLaunch { .. } => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-11 14:11:43 -06:00
|
|
|
pub fn toast(&self) -> Option<String> {
|
|
|
|
|
match self {
|
2024-09-05 17:34:03 -05:00
|
|
|
Self::Compress { .. } => Some(self.completed_text()),
|
2024-07-11 16:20:59 -06:00
|
|
|
Self::Delete { .. } => Some(self.completed_text()),
|
2024-09-05 17:34:03 -05:00
|
|
|
Self::Extract { .. } => Some(self.completed_text()),
|
2024-07-11 14:11:43 -06:00
|
|
|
//TODO: more toasts
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-30 10:47:41 -07:00
|
|
|
/// Perform the operation
|
2024-03-20 11:54:37 -06:00
|
|
|
pub async fn perform(
|
|
|
|
|
self,
|
|
|
|
|
id: u64,
|
2024-11-15 09:47:03 -07:00
|
|
|
msg_tx: &Arc<TokioMutex<Sender<Message>>>,
|
|
|
|
|
controller: Controller,
|
2024-03-20 11:54:37 -06:00
|
|
|
) -> Result<(), String> {
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 0.0))
|
|
|
|
|
.await;
|
2024-01-29 11:58:50 -07:00
|
|
|
|
2024-01-30 10:47:41 -07:00
|
|
|
//TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE
|
2024-01-29 11:58:50 -07:00
|
|
|
match self {
|
2024-09-05 17:34:03 -05:00
|
|
|
Self::Compress {
|
|
|
|
|
paths,
|
|
|
|
|
to,
|
|
|
|
|
archive_type,
|
|
|
|
|
} => {
|
|
|
|
|
let msg_tx = msg_tx.clone();
|
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
2024-09-10 11:50:14 -06:00
|
|
|
let Some(relative_root) = to.parent() else {
|
|
|
|
|
return Err(format!("path {:?} has no parent directory", to));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut paths = paths;
|
|
|
|
|
for path in paths.clone().iter() {
|
|
|
|
|
if path.is_dir() {
|
|
|
|
|
let new_paths_it = WalkDir::new(path).into_iter();
|
|
|
|
|
for entry in new_paths_it.skip(1) {
|
|
|
|
|
let entry = entry.map_err(err_str)?;
|
2024-11-13 14:36:11 -07:00
|
|
|
paths.push(entry.into_path());
|
2024-09-10 11:50:14 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-05 17:34:03 -05:00
|
|
|
match archive_type {
|
2024-09-10 11:50:14 -06:00
|
|
|
ArchiveType::Tgz => {
|
2024-09-05 17:34:03 -05:00
|
|
|
let mut archive = fs::File::create(&to)
|
|
|
|
|
.map(io::BufWriter::new)
|
2024-09-10 11:50:14 -06:00
|
|
|
.map(|w| {
|
|
|
|
|
flate2::write::GzEncoder::new(w, flate2::Compression::default())
|
|
|
|
|
})
|
|
|
|
|
.map(tar::Builder::new)
|
2024-09-05 17:34:03 -05:00
|
|
|
.map_err(err_str)?;
|
|
|
|
|
|
2024-09-10 11:50:14 -06:00
|
|
|
let total_paths = paths.len();
|
|
|
|
|
for (i, path) in paths.iter().enumerate() {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-09-10 11:50:14 -06:00
|
|
|
executor::block_on(async {
|
|
|
|
|
let total_progress = (i as f32) / total_paths as f32;
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
|
|
|
|
.await;
|
|
|
|
|
});
|
2024-09-05 17:34:03 -05:00
|
|
|
|
2024-09-10 11:50:14 -06:00
|
|
|
if let Some(relative_path) =
|
|
|
|
|
path.strip_prefix(relative_root).map_err(err_str)?.to_str()
|
|
|
|
|
{
|
|
|
|
|
archive
|
|
|
|
|
.append_path_with_name(path, relative_path)
|
|
|
|
|
.map_err(err_str)?;
|
2024-09-05 17:34:03 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 11:50:14 -06:00
|
|
|
archive.finish().map_err(err_str)?;
|
|
|
|
|
}
|
|
|
|
|
ArchiveType::Zip => {
|
|
|
|
|
let mut archive = fs::File::create(&to)
|
2024-11-14 16:00:35 -07:00
|
|
|
.map(io::BufWriter::new)
|
2024-09-10 11:50:14 -06:00
|
|
|
.map(zip::ZipWriter::new)
|
|
|
|
|
.map_err(err_str)?;
|
|
|
|
|
|
2024-09-05 17:34:03 -05:00
|
|
|
let total_paths = paths.len();
|
2024-11-14 14:43:45 -07:00
|
|
|
let mut buffer = vec![0; 4 * 1024 * 1024];
|
2024-09-05 17:34:03 -05:00
|
|
|
for (i, path) in paths.iter().enumerate() {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-09-05 17:34:03 -05:00
|
|
|
executor::block_on(async {
|
|
|
|
|
let total_progress = (i as f32) / total_paths as f32;
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
2024-11-14 14:43:45 -07:00
|
|
|
let mut zip_options = zip::write::SimpleFileOptions::default();
|
2024-09-10 11:50:14 -06:00
|
|
|
if let Some(relative_path) =
|
|
|
|
|
path.strip_prefix(relative_root).map_err(err_str)?.to_str()
|
|
|
|
|
{
|
|
|
|
|
if path.is_file() {
|
2024-11-14 14:43:45 -07:00
|
|
|
let mut file = fs::File::open(&path).map_err(err_str)?;
|
|
|
|
|
let metadata = file.metadata().map_err(err_str)?;
|
|
|
|
|
let total = metadata.len();
|
|
|
|
|
if total >= 4 * 1024 * 1024 * 1024 {
|
|
|
|
|
// The large file option must be enabled for files above 4 GiB
|
|
|
|
|
zip_options = zip_options.large_file(true);
|
|
|
|
|
}
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::fs::MetadataExt;
|
|
|
|
|
let mode = metadata.mode();
|
|
|
|
|
zip_options = zip_options.unix_permissions(mode);
|
|
|
|
|
}
|
2024-09-10 11:50:14 -06:00
|
|
|
archive
|
|
|
|
|
.start_file(relative_path, zip_options)
|
|
|
|
|
.map_err(err_str)?;
|
2024-11-14 14:43:45 -07:00
|
|
|
let mut current = 0;
|
|
|
|
|
loop {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
|
|
|
|
let count = file.read(&mut buffer).map_err(err_str)?;
|
|
|
|
|
if count == 0 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
archive.write_all(&buffer[..count]).map_err(err_str)?;
|
|
|
|
|
current += count;
|
|
|
|
|
|
|
|
|
|
executor::block_on(async {
|
|
|
|
|
let file_progress = current as f32 / total as f32;
|
|
|
|
|
let total_progress =
|
|
|
|
|
(i as f32 + file_progress) / total_paths as f32;
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(
|
|
|
|
|
id,
|
|
|
|
|
100.0 * total_progress,
|
|
|
|
|
))
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-09-10 11:50:14 -06:00
|
|
|
} else {
|
|
|
|
|
archive
|
|
|
|
|
.add_directory(relative_path, zip_options)
|
|
|
|
|
.map_err(err_str)?;
|
2024-09-05 17:34:03 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
archive.finish().map_err(err_str)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
.map_err(err_str)?;
|
|
|
|
|
}
|
2024-03-20 11:54:37 -06:00
|
|
|
Self::Copy { paths, to } => {
|
2024-11-15 09:47:03 -07:00
|
|
|
copy_or_move(paths, to, false, id, msg_tx, controller).await?;
|
2024-03-20 11:54:37 -06:00
|
|
|
}
|
2024-01-30 10:47:41 -07:00
|
|
|
Self::Delete { paths } => {
|
2024-02-27 09:58:22 -07:00
|
|
|
let total = paths.len();
|
2024-11-14 14:43:45 -07:00
|
|
|
for (i, path) in paths.into_iter().enumerate() {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-02-27 09:58:22 -07:00
|
|
|
let _ = msg_tx
|
2024-03-20 11:54:37 -06:00
|
|
|
.lock()
|
|
|
|
|
.await
|
2024-01-30 10:47:41 -07:00
|
|
|
.send(Message::PendingProgress(
|
|
|
|
|
id,
|
2024-11-14 14:43:45 -07:00
|
|
|
100.0 * (i as f32) / (total as f32),
|
2024-01-30 10:47:41 -07:00
|
|
|
))
|
|
|
|
|
.await;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
|
|
|
|
let _items_opt = tokio::task::spawn_blocking(|| trash::delete(path))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
.map_err(err_str)?;
|
|
|
|
|
//TODO: items_opt allows for easy restore
|
2024-01-30 10:47:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-05-09 13:24:06 -06:00
|
|
|
Self::EmptyTrash => {
|
|
|
|
|
#[cfg(any(
|
|
|
|
|
target_os = "windows",
|
|
|
|
|
all(
|
|
|
|
|
unix,
|
|
|
|
|
not(target_os = "macos"),
|
|
|
|
|
not(target_os = "ios"),
|
|
|
|
|
not(target_os = "android")
|
|
|
|
|
)
|
|
|
|
|
))]
|
|
|
|
|
{
|
2024-11-14 14:43:45 -07:00
|
|
|
let msg_tx = msg_tx.clone();
|
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
|
|
|
|
let items = trash::os_limited::list().map_err(err_str)?;
|
|
|
|
|
let count = items.len();
|
|
|
|
|
for (i, item) in items.into_iter().enumerate() {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
|
|
|
|
executor::block_on(async {
|
|
|
|
|
let total_progress = i as f32 / count as f32;
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
trash::os_limited::purge_all([item]).map_err(err_str)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
2024-05-09 13:24:06 -06:00
|
|
|
})
|
|
|
|
|
.await
|
2024-11-14 14:43:45 -07:00
|
|
|
.map_err(err_str)??;
|
2024-05-09 13:24:06 -06:00
|
|
|
}
|
|
|
|
|
}
|
2024-08-15 12:32:12 -05:00
|
|
|
Self::Extract { paths, to } => {
|
2024-09-05 17:34:03 -05:00
|
|
|
let msg_tx = msg_tx.clone();
|
|
|
|
|
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
|
|
|
|
let total_paths = paths.len();
|
|
|
|
|
for (i, path) in paths.iter().enumerate() {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-09-05 17:34:03 -05:00
|
|
|
executor::block_on(async {
|
|
|
|
|
let total_progress = (i as f32) / total_paths as f32;
|
|
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 100.0 * total_progress))
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let to = to.to_owned();
|
2024-08-15 12:32:12 -05:00
|
|
|
|
2024-09-28 19:09:27 +05:30
|
|
|
if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
|
|
|
|
|
let dir_name = get_directory_name(file_name);
|
|
|
|
|
let mut new_dir = to.join(dir_name);
|
|
|
|
|
|
2024-08-15 12:32:12 -05:00
|
|
|
if new_dir.exists() {
|
|
|
|
|
if let Some(new_dir_parent) = new_dir.parent() {
|
2024-09-17 12:31:54 -06:00
|
|
|
new_dir = copy_unique_path(&new_dir, new_dir_parent);
|
2024-08-15 12:32:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-14 16:00:35 -07:00
|
|
|
let msg_tx = msg_tx.clone();
|
2024-11-15 09:47:03 -07:00
|
|
|
let controller = controller.clone();
|
2024-09-10 11:50:14 -06:00
|
|
|
let mime = mime_for_path(&path);
|
|
|
|
|
match mime.essence_str() {
|
2024-09-17 12:31:54 -06:00
|
|
|
"application/gzip" | "application/x-compressed-tar" => {
|
2024-11-15 09:47:03 -07:00
|
|
|
OpReader::new(path, id, msg_tx, controller)
|
2024-09-17 12:31:54 -06:00
|
|
|
.map(io::BufReader::new)
|
|
|
|
|
.map(flate2::read::GzDecoder::new)
|
|
|
|
|
.map(tar::Archive::new)
|
2024-09-28 19:09:27 +05:30
|
|
|
.and_then(|mut archive| archive.unpack(&new_dir))
|
2024-09-17 12:31:54 -06:00
|
|
|
.map_err(err_str)?
|
|
|
|
|
}
|
2024-11-15 09:47:03 -07:00
|
|
|
"application/x-tar" => OpReader::new(path, id, msg_tx, controller)
|
2024-09-10 11:50:14 -06:00
|
|
|
.map(io::BufReader::new)
|
|
|
|
|
.map(tar::Archive::new)
|
2024-09-28 19:09:27 +05:30
|
|
|
.and_then(|mut archive| archive.unpack(&new_dir))
|
2024-09-10 11:50:14 -06:00
|
|
|
.map_err(err_str)?,
|
|
|
|
|
"application/zip" => fs::File::open(path)
|
|
|
|
|
.map(io::BufReader::new)
|
|
|
|
|
.map(zip::ZipArchive::new)
|
|
|
|
|
.map_err(err_str)?
|
2024-11-14 16:00:35 -07:00
|
|
|
.and_then(move |mut archive| {
|
2024-11-15 09:47:03 -07:00
|
|
|
zip_extract(&mut archive, &new_dir, id, msg_tx, controller)
|
2024-11-14 16:00:35 -07:00
|
|
|
})
|
2024-09-10 11:50:14 -06:00
|
|
|
.map_err(err_str)?,
|
2024-09-28 19:28:33 +05:30
|
|
|
#[cfg(feature = "bzip2")]
|
2024-09-17 12:31:54 -06:00
|
|
|
"application/x-bzip" | "application/x-bzip-compressed-tar" => {
|
2024-11-15 09:47:03 -07:00
|
|
|
OpReader::new(path, id, msg_tx, controller)
|
2024-09-17 12:31:54 -06:00
|
|
|
.map(io::BufReader::new)
|
|
|
|
|
.map(bzip2::read::BzDecoder::new)
|
|
|
|
|
.map(tar::Archive::new)
|
|
|
|
|
.and_then(|mut archive| archive.unpack(new_dir))
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
}
|
|
|
|
|
#[cfg(feature = "liblzma")]
|
|
|
|
|
"application/x-xz" | "application/x-xz-compressed-tar" => {
|
2024-11-15 09:47:03 -07:00
|
|
|
OpReader::new(path, id, msg_tx, controller)
|
2024-09-17 12:31:54 -06:00
|
|
|
.map(io::BufReader::new)
|
|
|
|
|
.map(liblzma::read::XzDecoder::new)
|
|
|
|
|
.map(tar::Archive::new)
|
|
|
|
|
.and_then(|mut archive| archive.unpack(new_dir))
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
}
|
2024-09-10 11:50:14 -06:00
|
|
|
_ => Err(format!("unsupported mime type {:?}", mime))?,
|
2024-08-15 12:32:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-05 17:34:03 -05:00
|
|
|
}
|
2024-08-15 12:32:12 -05:00
|
|
|
|
2024-09-05 17:34:03 -05:00
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
.map_err(err_str)?;
|
2024-08-15 12:32:12 -05:00
|
|
|
}
|
2024-03-20 11:54:37 -06:00
|
|
|
Self::Move { paths, to } => {
|
2024-11-15 09:47:03 -07:00
|
|
|
copy_or_move(paths, to, true, id, msg_tx, controller).await?;
|
2024-03-20 11:54:37 -06:00
|
|
|
}
|
2024-02-27 13:25:50 -07:00
|
|
|
Self::NewFolder { path } => {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-02-27 13:25:50 -07:00
|
|
|
tokio::task::spawn_blocking(|| fs::create_dir(path))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
.map_err(err_str)?;
|
|
|
|
|
}
|
|
|
|
|
Self::NewFile { path } => {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-02-27 13:25:50 -07:00
|
|
|
tokio::task::spawn_blocking(|| fs::File::create(path))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
2024-02-28 15:07:50 -07:00
|
|
|
.map_err(err_str)?;
|
|
|
|
|
}
|
|
|
|
|
Self::Rename { from, to } => {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-02-28 15:07:50 -07:00
|
|
|
tokio::task::spawn_blocking(|| fs::rename(from, to))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
2024-02-27 13:25:50 -07:00
|
|
|
.map_err(err_str)?;
|
|
|
|
|
}
|
2024-06-14 12:54:13 +02:00
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
Self::Restore { .. } => {
|
|
|
|
|
// TODO: add support for macos
|
|
|
|
|
return Err("Restoring from trash is not supported on macos".to_string());
|
|
|
|
|
}
|
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
2024-01-30 10:47:41 -07:00
|
|
|
Self::Restore { paths } => {
|
2024-02-27 09:58:22 -07:00
|
|
|
let total = paths.len();
|
2024-11-14 14:43:45 -07:00
|
|
|
for (i, path) in paths.into_iter().enumerate() {
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-02-27 09:58:22 -07:00
|
|
|
let _ = msg_tx
|
2024-03-20 11:54:37 -06:00
|
|
|
.lock()
|
|
|
|
|
.await
|
2024-01-30 10:47:41 -07:00
|
|
|
.send(Message::PendingProgress(
|
|
|
|
|
id,
|
2024-11-14 14:43:45 -07:00
|
|
|
100.0 * (i as f32) / (total as f32),
|
2024-01-30 10:47:41 -07:00
|
|
|
))
|
|
|
|
|
.await;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
|
|
|
|
tokio::task::spawn_blocking(|| trash::os_limited::restore_all([path]))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
.map_err(err_str)?;
|
2024-01-30 10:47:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-10-03 11:53:01 -06:00
|
|
|
Self::SetExecutableAndLaunch { path } => {
|
2024-11-14 14:43:45 -07:00
|
|
|
tokio::task::spawn_blocking(move || -> Result<(), String> {
|
2024-10-03 11:53:01 -06:00
|
|
|
//TODO: what to do on non-Unix systems?
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
|
|
|
|
let mut perms = fs::metadata(&path).map_err(err_str)?.permissions();
|
2024-10-03 11:53:01 -06:00
|
|
|
let current_mode = perms.mode();
|
|
|
|
|
let new_mode = current_mode | 0o111;
|
|
|
|
|
perms.set_mode(new_mode);
|
2024-11-14 14:43:45 -07:00
|
|
|
fs::set_permissions(&path, perms).map_err(err_str)?;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 09:47:03 -07:00
|
|
|
controller.check()?;
|
2024-10-03 11:53:01 -06:00
|
|
|
|
|
|
|
|
let mut command = std::process::Command::new(path);
|
2024-11-14 14:43:45 -07:00
|
|
|
spawn_detached(&mut command).map_err(err_str)?;
|
2024-10-03 11:53:01 -06:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.map_err(err_str)?
|
|
|
|
|
.map_err(err_str)?;
|
|
|
|
|
}
|
2024-01-29 11:58:50 -07:00
|
|
|
}
|
|
|
|
|
|
2024-03-20 11:54:37 -06:00
|
|
|
let _ = msg_tx
|
|
|
|
|
.lock()
|
|
|
|
|
.await
|
|
|
|
|
.send(Message::PendingProgress(id, 100.0))
|
|
|
|
|
.await;
|
2024-01-29 11:58:50 -07:00
|
|
|
|
2024-01-30 10:47:41 -07:00
|
|
|
Ok(())
|
2024-01-29 11:58:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-05-13 22:32:47 -04:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2024-05-21 02:09:59 -04:00
|
|
|
use std::{
|
|
|
|
|
fs::{self, File},
|
|
|
|
|
io,
|
|
|
|
|
path::PathBuf,
|
|
|
|
|
};
|
2024-05-13 22:32:47 -04:00
|
|
|
|
2024-07-22 23:23:47 -04:00
|
|
|
use cosmic::iced::futures::{channel::mpsc, StreamExt};
|
2024-05-13 22:32:47 -04:00
|
|
|
use log::{debug, trace};
|
|
|
|
|
use test_log::test;
|
|
|
|
|
use tokio::sync;
|
|
|
|
|
|
2024-11-19 08:15:47 -07:00
|
|
|
use super::{Controller, Operation, ReplaceResult};
|
2024-05-13 22:32:47 -04:00
|
|
|
use crate::{
|
|
|
|
|
app::{
|
|
|
|
|
test_utils::{
|
|
|
|
|
empty_fs, filter_dirs, filter_files, read_dir_sorted, simple_fs, NAME_LEN,
|
|
|
|
|
NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED,
|
|
|
|
|
},
|
2024-07-22 23:23:47 -04:00
|
|
|
DialogPage, Message,
|
2024-05-13 22:32:47 -04:00
|
|
|
},
|
|
|
|
|
fl,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Tests hang with lower values
|
|
|
|
|
const BUF_SIZE: usize = 8;
|
|
|
|
|
|
|
|
|
|
/// Simple wrapper around `[Operation::Copy]`
|
|
|
|
|
pub async fn operation_copy(paths: Vec<PathBuf>, to: PathBuf) -> Result<(), String> {
|
|
|
|
|
let id = fastrand::u64(0..u64::MAX);
|
|
|
|
|
let (tx, mut rx) = mpsc::channel(BUF_SIZE);
|
2024-07-22 23:23:47 -04:00
|
|
|
let paths_clone = paths.clone();
|
|
|
|
|
let to_clone = to.clone();
|
|
|
|
|
let handle_copy = tokio::spawn(async move {
|
|
|
|
|
Operation::Copy {
|
|
|
|
|
paths: paths_clone,
|
|
|
|
|
to: to_clone,
|
|
|
|
|
}
|
2024-11-19 08:15:47 -07:00
|
|
|
.perform(id, &sync::Mutex::new(tx).into(), Controller::new())
|
2024-07-22 23:23:47 -04:00
|
|
|
.await
|
|
|
|
|
});
|
2024-05-13 22:32:47 -04:00
|
|
|
|
2024-07-22 23:23:47 -04:00
|
|
|
while let Some(msg) = rx.next().await {
|
|
|
|
|
match msg {
|
|
|
|
|
Message::PendingProgress(id, progress) => {
|
2024-05-13 22:32:47 -04:00
|
|
|
trace!("({id}) [ {paths:?} => {to:?} ] {progress}% complete)")
|
|
|
|
|
}
|
2024-07-22 23:23:47 -04:00
|
|
|
Message::DialogPush(DialogPage::Replace { tx, .. }) => {
|
|
|
|
|
debug!("[{id}] Replace request");
|
|
|
|
|
tx.send(ReplaceResult::Cancel).await.expect("Sending a response to a replace request should succeed")
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
_ => unreachable!("Only [ `Message::PendingProgress`, `Message::DialogPush(DialogPage::Replace)` ] are sent from operation"),
|
2024-05-13 22:32:47 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-22 23:23:47 -04:00
|
|
|
handle_copy.await.unwrap()
|
2024-05-13 22:32:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test(tokio::test)]
|
|
|
|
|
async fn copy_file_to_same_location() -> io::Result<()> {
|
|
|
|
|
let fs = simple_fs(NUM_FILES, 0, 1, 0, NAME_LEN)?;
|
|
|
|
|
let path = fs.path();
|
|
|
|
|
|
|
|
|
|
// Get the first file from the first directory
|
|
|
|
|
let first_dir = filter_dirs(path)?
|
|
|
|
|
.next()
|
|
|
|
|
.expect("Should have at least one directory");
|
|
|
|
|
let first_file = filter_files(&first_dir)?
|
|
|
|
|
.next()
|
|
|
|
|
.expect("Should have at least one file");
|
|
|
|
|
|
|
|
|
|
// Duplicate that file
|
|
|
|
|
let base_name = first_file
|
|
|
|
|
.file_name()
|
|
|
|
|
.and_then(|name| name.to_str())
|
|
|
|
|
.expect("File name exists and is valid");
|
|
|
|
|
debug!(
|
|
|
|
|
"Duplicating {} in {}",
|
|
|
|
|
first_file.display(),
|
|
|
|
|
first_dir.display()
|
|
|
|
|
);
|
|
|
|
|
operation_copy(vec![first_file.clone()], first_dir.clone())
|
|
|
|
|
.await
|
|
|
|
|
.expect("Copy operation should have succeeded");
|
|
|
|
|
|
|
|
|
|
assert!(first_file.exists(), "Original file should still exist");
|
|
|
|
|
let expected = first_dir.join(format!("{base_name} ({} 1)", fl!("copy_noun")));
|
|
|
|
|
assert!(expected.exists(), "File should have been duplicated");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test(tokio::test)]
|
|
|
|
|
async fn copy_file_with_extension_to_same_loc() -> io::Result<()> {
|
|
|
|
|
let fs = empty_fs()?;
|
|
|
|
|
let path = fs.path();
|
|
|
|
|
|
|
|
|
|
let base_name = "foo.txt";
|
|
|
|
|
let base_path = path.join(base_name);
|
|
|
|
|
File::create(&base_path)?;
|
|
|
|
|
debug!("Duplicating {}", base_path.display());
|
|
|
|
|
operation_copy(vec![base_path.clone()], path.to_owned())
|
|
|
|
|
.await
|
|
|
|
|
.expect("Copy operation should have succeeded");
|
|
|
|
|
|
|
|
|
|
assert!(base_path.exists(), "Original file should still exist");
|
|
|
|
|
let expected = path.join(format!("foo ({} 1).txt", fl!("copy_noun")));
|
|
|
|
|
assert!(expected.exists(), "File should have been duplicated");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test(tokio::test)]
|
|
|
|
|
async fn copy_dir_to_same_location() -> io::Result<()> {
|
|
|
|
|
let fs = simple_fs(NUM_FILES, 0, NUM_DIRS, NUM_NESTED, NAME_LEN)?;
|
|
|
|
|
let path = fs.path();
|
|
|
|
|
|
|
|
|
|
// First directory path
|
|
|
|
|
let first_dir = filter_dirs(path)?
|
|
|
|
|
.next()
|
|
|
|
|
.expect("Should have at least one directory");
|
|
|
|
|
let base_name = first_dir
|
|
|
|
|
.file_name()
|
|
|
|
|
.and_then(|name| name.to_str())
|
|
|
|
|
.expect("First directory exists and has a valid name");
|
|
|
|
|
debug!("Duplicating directory {}", first_dir.display());
|
|
|
|
|
operation_copy(vec![first_dir.clone()], path.to_owned())
|
|
|
|
|
.await
|
|
|
|
|
.expect("Copy operation should have succeeded");
|
|
|
|
|
|
|
|
|
|
assert!(first_dir.exists(), "Original directory should still exist");
|
|
|
|
|
let expected = path.join(format!("{base_name} ({} 1)", fl!("copy_noun")));
|
|
|
|
|
assert!(expected.exists(), "Directory should have been duplicated");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test(tokio::test)]
|
|
|
|
|
async fn copying_file_multiple_times_to_same_location() -> io::Result<()> {
|
|
|
|
|
let fs = empty_fs()?;
|
|
|
|
|
let path = fs.path();
|
|
|
|
|
|
|
|
|
|
let base_name = "cosmic";
|
|
|
|
|
let base_path = path.join(base_name);
|
|
|
|
|
File::create(&base_path)?;
|
|
|
|
|
|
|
|
|
|
for i in 1..5 {
|
|
|
|
|
debug!("Duplicating {}", base_path.display());
|
|
|
|
|
operation_copy(vec![base_path.clone()], path.to_owned())
|
|
|
|
|
.await
|
|
|
|
|
.expect("Copy operation should have succeeded");
|
|
|
|
|
assert!(base_path.exists(), "Original file should still exist");
|
|
|
|
|
assert!(
|
|
|
|
|
path.join(format!("{base_name} ({} {i})", fl!("copy_noun")))
|
|
|
|
|
.exists(),
|
|
|
|
|
"File should have been duplicated (copy #{i})"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test(tokio::test)]
|
|
|
|
|
async fn copy_to_diff_dir_doesnt_dupe_files() -> io::Result<()> {
|
|
|
|
|
let fs = simple_fs(NUM_FILES, NUM_HIDDEN, NUM_DIRS, NUM_NESTED, NAME_LEN)?;
|
|
|
|
|
let path = fs.path();
|
|
|
|
|
|
|
|
|
|
let (first_dir, second_dir) = {
|
|
|
|
|
let mut dirs = filter_dirs(path)?;
|
|
|
|
|
(
|
|
|
|
|
dirs.next().expect("Should have at least two dirs"),
|
|
|
|
|
dirs.next().expect("Should have at least two dirs"),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
let first_file = filter_files(&first_dir)?
|
|
|
|
|
.next()
|
|
|
|
|
.expect("Should have at least one file");
|
|
|
|
|
// Both directories have a file with the same name.
|
|
|
|
|
let base_name = first_file
|
|
|
|
|
.file_name()
|
|
|
|
|
.and_then(|name| name.to_str())
|
|
|
|
|
.expect("File name exists and is valid");
|
|
|
|
|
|
|
|
|
|
debug!(
|
|
|
|
|
"Copying {} to {}",
|
|
|
|
|
first_file.display(),
|
|
|
|
|
second_dir.display()
|
|
|
|
|
);
|
|
|
|
|
operation_copy(vec![first_file.clone()], second_dir.clone())
|
|
|
|
|
.await
|
2024-07-22 23:23:47 -04:00
|
|
|
.expect(
|
|
|
|
|
"Copy operation should have been cancelled because we're copying to different directories without replacement",
|
2024-05-13 22:32:47 -04:00
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
first_dir.join(base_name).exists(),
|
|
|
|
|
"First file should still exist"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
second_dir.join(base_name).exists(),
|
|
|
|
|
"Second file should still exist"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2024-05-21 02:09:59 -04:00
|
|
|
|
|
|
|
|
#[test(tokio::test)]
|
|
|
|
|
async fn copy_file_with_diff_name_to_diff_dir() -> io::Result<()> {
|
|
|
|
|
let fs = empty_fs()?;
|
|
|
|
|
let path = fs.path();
|
|
|
|
|
|
|
|
|
|
let dir_path = path.join("cosmic");
|
|
|
|
|
fs::create_dir(&dir_path)?;
|
|
|
|
|
let file_path = path.join("ferris");
|
|
|
|
|
File::create(&file_path)?;
|
|
|
|
|
let expected = dir_path.join("ferris");
|
|
|
|
|
|
|
|
|
|
debug!("Copying {} to {}", file_path.display(), expected.display());
|
|
|
|
|
operation_copy(vec![file_path.clone()], dir_path.clone())
|
|
|
|
|
.await
|
|
|
|
|
.expect("Copy operation should have succeeded");
|
|
|
|
|
|
|
|
|
|
assert!(file_path.exists(), "Original file should still exist");
|
|
|
|
|
assert!(expected.exists(), "File should have been copied");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2024-05-13 22:32:47 -04:00
|
|
|
}
|