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

View file

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

View file

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

View file

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

View file

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