Copy to external drives when drag and dropping, part of #828

This commit is contained in:
Jeremy Soller 2025-04-29 18:07:57 -06:00
parent dd98622cfa
commit 5573e36400
No known key found for this signature in database
GPG key ID: 670FDFB5428E05CA
5 changed files with 82 additions and 34 deletions

View file

@ -2263,7 +2263,7 @@ impl Application for App {
Message::Cut(entity_opt) => {
self.set_cut(entity_opt);
let paths = self.selected_paths(entity_opt);
let contents = ClipboardCopy::new(ClipboardKind::Cut, &paths);
let contents = ClipboardCopy::new(ClipboardKind::Cut { is_dnd: false }, &paths);
return clipboard::write_data(contents);
}
Message::CloseToast(id) => {
@ -3010,9 +3010,10 @@ impl Application for App {
paths: contents.paths,
to,
}),
ClipboardKind::Cut => self.operation(Operation::Move {
ClipboardKind::Cut { is_dnd } => self.operation(Operation::Move {
paths: contents.paths,
to,
cross_device_copy: is_dnd,
}),
};
}
@ -3060,7 +3061,10 @@ impl Application for App {
if self.update_favorites(&[(from.clone(), to.clone())]) {
commands.push(self.update_config());
}
} else if let Operation::Move { ref paths, ref to } = op {
} else if let Operation::Move {
ref paths, ref to, ..
} = op
{
let path_changes: Vec<_> = paths
.iter()
.filter_map(|from| {
@ -3527,7 +3531,7 @@ impl Application for App {
cosmic::action::app(Message::CutPaths(match p {
Some(s) => match s.kind {
ClipboardKind::Copy => Vec::new(),
ClipboardKind::Cut => s.paths,
ClipboardKind::Cut { .. } => s.paths,
},
None => Vec::new(),
}))
@ -3712,7 +3716,7 @@ impl Application for App {
self.nav_dnd_hover = None;
if let Some((location, data)) = self.nav_model.data::<Location>(entity).zip(data) {
let kind = match action {
DndAction::Move => ClipboardKind::Cut,
DndAction::Move => ClipboardKind::Cut { is_dnd: true },
_ => ClipboardKind::Copy,
};
let ret = match location {
@ -3772,7 +3776,7 @@ impl Application for App {
self.nav_dnd_hover = None;
if let Some((tab, data)) = self.tab_model.data::<Tab>(entity).zip(data) {
let kind = match action {
DndAction::Move => ClipboardKind::Cut,
DndAction::Move => ClipboardKind::Cut { is_dnd: true },
_ => ClipboardKind::Copy,
};
let ret = match &tab.location {

View file

@ -13,7 +13,7 @@ use url::Url;
#[derive(Clone, Copy, Debug)]
pub enum ClipboardKind {
Copy,
Cut,
Cut { is_dnd: bool },
}
#[derive(Clone, Debug)]
@ -37,7 +37,7 @@ impl ClipboardCopy {
let mut text_uri_list = String::new();
let mut x_special_gnome_copied_files = match kind {
ClipboardKind::Copy => "copy",
ClipboardKind::Cut => "cut",
ClipboardKind::Cut { .. } => "cut",
}
.to_string();
//TODO: do we have to use \r\n?
@ -145,7 +145,7 @@ impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
if i == 0 {
kind = match line {
"copy" => ClipboardKind::Copy,
"cut" => ClipboardKind::Cut,
"cut" => ClipboardKind::Cut { is_dnd: false },
_ => Err(format!("unsupported clipboard operation {:?}", line))?,
};
} else {

View file

@ -27,7 +27,7 @@ pub mod controller;
use self::reader::OpReader;
pub mod reader;
use self::recursive::Context;
use self::recursive::{Context, Method};
pub mod recursive;
async fn handle_replace(
@ -267,7 +267,7 @@ pub enum ReplaceResult {
async fn copy_or_move(
paths: Vec<PathBuf>,
to: PathBuf,
moving: bool,
method: Method,
msg_tx: &Arc<TokioMutex<Sender<Message>>>,
controller: Controller,
) -> Result<OperationSelection, OperationError> {
@ -276,7 +276,10 @@ async fn copy_or_move(
compio::runtime::spawn(async move {
log::info!(
"{} {:?} to {:?}",
if moving { "Move" } else { "Copy" },
match method {
Method::Copy => "Copy",
Method::Move { .. } => "Move",
},
paths,
to
);
@ -286,7 +289,9 @@ async fn copy_or_move(
.into_iter()
.zip(std::iter::repeat(to.as_path()))
.filter_map(|(from, to)| {
if matches!(from.parent(), Some(parent) if parent == to) && !moving {
if matches!(from.parent(), Some(parent) if parent == to)
&& matches!(method, Method::Copy)
{
// `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);
@ -330,7 +335,7 @@ async fn copy_or_move(
}
context
.recursive_copy_or_move(from_to_pairs, moving)
.recursive_copy_or_move(from_to_pairs, method)
.await
.map_err(OperationError::from_str)?;
@ -483,6 +488,7 @@ pub enum Operation {
Move {
paths: Vec<PathBuf>,
to: PathBuf,
cross_device_copy: bool,
},
NewFile {
path: PathBuf,
@ -576,7 +582,7 @@ impl Operation {
to = file_name(to),
progress = progress()
),
Self::Move { paths, to } => fl!(
Self::Move { paths, to, .. } => fl!(
"moving",
items = paths.len(),
from = paths_parent_name(paths),
@ -635,7 +641,7 @@ impl Operation {
from = paths_parent_name(paths),
to = file_name(to)
),
Self::Move { paths, to } => fl!(
Self::Move { paths, to, .. } => fl!(
"moved",
items = paths.len(),
from = paths_parent_name(paths),
@ -853,7 +859,9 @@ impl Operation {
.map_err(wrap_compio_spawn_error)?
.map_err(OperationError::from_str)
}
Self::Copy { paths, to } => copy_or_move(paths, to, false, msg_tx, controller).await,
Self::Copy { paths, to } => {
copy_or_move(paths, to, Method::Copy, msg_tx, controller).await
}
Self::Delete { paths } => {
let total = paths.len();
for (i, path) in paths.into_iter().enumerate() {
@ -1027,7 +1035,20 @@ impl Operation {
.await
.map_err(wrap_compio_spawn_error)?
.map_err(OperationError::from_str),
Self::Move { paths, to } => copy_or_move(paths, to, true, msg_tx, controller).await,
Self::Move {
paths,
to,
cross_device_copy,
} => {
copy_or_move(
paths,
to,
Method::Move { cross_device_copy },
msg_tx,
controller,
)
.await
}
Self::NewFolder { path } => compio::runtime::spawn(async move {
controller.check().await.map_err(OperationError::from_str)?;
compio::fs::create_dir(&path)

View file

@ -9,6 +9,11 @@ use walkdir::WalkDir;
use super::{copy_unique_path, Controller, OperationSelection, ReplaceResult};
pub enum Method {
Copy,
Move { cross_device_copy: bool },
}
pub struct Context {
buf: Vec<u8>,
controller: Controller,
@ -46,7 +51,7 @@ impl Context {
pub async fn recursive_copy_or_move(
&mut self,
from_to_pairs: Vec<(PathBuf, PathBuf)>,
moving: bool,
method: Method,
) -> Result<bool, String> {
let mut ops = Vec::new();
let mut cleanup_ops = Vec::new();
@ -69,10 +74,9 @@ impl Context {
let kind = if file_type.is_dir() {
OpKind::Mkdir
} else if file_type.is_file() {
if moving {
OpKind::Move
} else {
OpKind::Copy
match method {
Method::Copy => OpKind::Copy,
Method::Move { cross_device_copy } => OpKind::Move { cross_device_copy },
}
} else if file_type.is_symlink() {
let target = fs::read_link(&from)
@ -99,9 +103,13 @@ impl Context {
kind,
from,
to,
skipped: Rc::new(Cell::new(false)),
skipped: Rc::new(Skip {
normal: Cell::new(false),
cleanup: Cell::new(false),
}),
is_cleanup: false,
};
if moving {
if matches!(method, Method::Move { .. }) {
if let Some(cleanup_op) = op.move_cleanup_op() {
cleanup_ops.push(cleanup_op);
}
@ -180,7 +188,7 @@ impl Context {
if apply_to_all {
self.replace_result_opt = Some(replace_result);
}
op.skipped.set(true);
op.skipped.normal.set(true);
Ok(ControlFlow::Break(true))
}
ReplaceResult::Cancel => Ok(ControlFlow::Break(false)),
@ -199,25 +207,34 @@ pub struct Progress {
#[derive(Debug)]
pub enum OpKind {
Copy,
Move,
Move { cross_device_copy: bool },
Mkdir,
Remove,
Rmdir,
Symlink { target: PathBuf },
}
#[derive(Debug)]
pub struct Skip {
/// Normal operation should be skipped
pub normal: Cell<bool>,
/// Cleanup operation should be skipped
pub cleanup: Cell<bool>,
}
#[derive(Debug)]
pub struct Op {
pub kind: OpKind,
pub from: PathBuf,
pub to: PathBuf,
pub skipped: Rc<Cell<bool>>,
pub skipped: Rc<Skip>,
pub is_cleanup: bool,
}
impl Op {
fn move_cleanup_op(&self) -> Option<Self> {
let kind = match self.kind {
OpKind::Copy | OpKind::Move | OpKind::Symlink { .. } => OpKind::Remove,
OpKind::Copy | OpKind::Move { .. } | OpKind::Symlink { .. } => OpKind::Remove,
OpKind::Mkdir => OpKind::Rmdir,
OpKind::Remove | OpKind::Rmdir => return None,
};
@ -227,6 +244,7 @@ impl Op {
//TODO: it is strange to have `to` here
to: self.to.clone(),
skipped: self.skipped.clone(),
is_cleanup: true,
})
}
@ -235,7 +253,7 @@ impl Op {
ctx: &mut Context,
mut progress: Progress,
) -> Result<bool, Box<dyn Error>> {
if self.skipped.get() {
if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) {
return Ok(true);
}
match self.kind {
@ -326,7 +344,7 @@ impl Op {
to_file.sync_all().await?;
}
OpKind::Move => {
OpKind::Move { cross_device_copy } => {
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(self).await? {
@ -349,12 +367,17 @@ impl Op {
const EXDEV: i32 = libc::EXDEV as _;
if err.raw_os_error() == Some(EXDEV) {
if cross_device_copy {
// Do not clean up if cross_device_copy is set
self.skipped.cleanup.set(true);
}
// Try standard copy if hard link fails with cross device error
let mut copy_op = Op {
kind: OpKind::Copy,
from: self.from.clone(),
to: self.to.clone(),
skipped: self.skipped.clone(),
is_cleanup: self.is_cleanup,
};
return Box::pin(copy_op.run(ctx, progress)).await;
} else {

View file

@ -3425,7 +3425,7 @@ impl Tab {
}
commands.push(Command::DropFiles(to, from))
}
Location::Trash if matches!(from.kind, ClipboardKind::Cut) => {
Location::Trash if matches!(from.kind, ClipboardKind::Cut { .. }) => {
commands.push(Command::Delete(from.paths))
}
_ => {
@ -3676,7 +3676,7 @@ impl Tab {
if action == DndAction::Copy {
Message::Drop(Some((location1.clone(), data)))
} else if action == DndAction::Move {
data.kind = ClipboardKind::Cut;
data.kind = ClipboardKind::Cut { is_dnd: true };
Message::Drop(Some((location1.clone(), data)))
} else {
log::warn!("unsupported action: {:?}", action);
@ -5009,7 +5009,7 @@ impl Tab {
if action == DndAction::Copy {
Message::Drop(Some((tab_location.clone(), data)))
} else if action == DndAction::Move {
data.kind = ClipboardKind::Cut;
data.kind = ClipboardKind::Cut { is_dnd: true };
Message::Drop(Some((tab_location.clone(), data)))
} else {
log::warn!("unsupported action: {:?}", action);