2026-04-10 06:03:16 +02:00
|
|
|
// Copyright 2023 System76 <info@system76.com>
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
|
|
|
|
use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path};
|
|
|
|
|
use crate::operation::{OperationError, sync_to_disk};
|
|
|
|
|
use anyhow::Context as AnyhowContext;
|
2025-09-03 23:24:38 +02:00
|
|
|
use compio::BufResult;
|
2025-04-10 18:14:24 -06:00
|
|
|
use compio::buf::{IntoInner, IoBuf};
|
2026-04-28 15:05:08 +02:00
|
|
|
use compio::driver::ToSharedFd;
|
|
|
|
|
use compio::driver::op::AsyncifyFd;
|
2025-04-09 23:15:07 +02:00
|
|
|
use compio::io::{AsyncReadAt, AsyncWriteAt};
|
2026-04-10 06:03:16 +02:00
|
|
|
use cosmic::iced::futures;
|
|
|
|
|
use futures::{FutureExt, StreamExt};
|
2026-04-28 15:05:08 +02:00
|
|
|
use std::cell::Cell;
|
|
|
|
|
use std::error::Error;
|
|
|
|
|
use std::fs;
|
2025-04-09 23:15:07 +02:00
|
|
|
use std::future::Future;
|
2026-04-28 15:05:08 +02:00
|
|
|
use std::ops::ControlFlow;
|
|
|
|
|
use std::path::PathBuf;
|
2025-04-09 23:15:07 +02:00
|
|
|
use std::pin::Pin;
|
2026-04-10 06:03:16 +02:00
|
|
|
use std::rc::Rc;
|
2025-04-09 23:15:07 +02:00
|
|
|
use std::time::Instant;
|
2024-11-13 14:36:11 -07:00
|
|
|
use walkdir::WalkDir;
|
|
|
|
|
|
2026-04-14 17:15:19 +02:00
|
|
|
#[cfg(feature = "gvfs")]
|
|
|
|
|
use gio::prelude::FileExtManual;
|
|
|
|
|
|
2026-04-10 06:03:16 +02:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
|
pub enum GioCopyError {
|
|
|
|
|
#[error("controller state")]
|
|
|
|
|
Controller(OperationError),
|
2026-04-14 17:15:19 +02:00
|
|
|
#[cfg(feature = "gvfs")]
|
2026-04-10 06:03:16 +02:00
|
|
|
#[error("gio copy failed")]
|
|
|
|
|
GLib(#[from] glib::Error),
|
|
|
|
|
}
|
2024-11-13 14:36:11 -07:00
|
|
|
|
2025-04-29 18:07:57 -06:00
|
|
|
pub enum Method {
|
|
|
|
|
Copy,
|
|
|
|
|
Move { cross_device_copy: bool },
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
pub struct Context {
|
|
|
|
|
buf: Vec<u8>,
|
2024-11-15 09:47:03 -07:00
|
|
|
controller: Controller,
|
2025-01-17 22:29:57 -05:00
|
|
|
on_progress: Box<dyn OnProgress>,
|
2025-04-09 23:15:07 +02:00
|
|
|
on_replace: Pin<Box<dyn OnReplace>>,
|
2024-11-19 20:17:58 -07:00
|
|
|
pub(crate) op_sel: OperationSelection,
|
2024-11-13 14:36:11 -07:00
|
|
|
replace_result_opt: Option<ReplaceResult>,
|
2026-02-11 21:55:44 +01:00
|
|
|
remaining_conflicts: usize,
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
|
2025-01-17 22:29:57 -05:00
|
|
|
pub trait OnProgress: Fn(&Op, &Progress) + 'static {}
|
|
|
|
|
impl<F> OnProgress for F where F: Fn(&Op, &Progress) + 'static {}
|
|
|
|
|
|
2025-04-09 23:15:07 +02:00
|
|
|
pub trait OnReplace:
|
2026-02-11 21:55:44 +01:00
|
|
|
for<'a> Fn(&'a Op, usize) -> Pin<Box<dyn Future<Output = ReplaceResult> + 'a>> + 'static
|
2025-04-09 23:15:07 +02:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
impl<F> OnReplace for F where
|
2026-02-11 21:55:44 +01:00
|
|
|
F: for<'a> Fn(&'a Op, usize) -> Pin<Box<dyn Future<Output = ReplaceResult> + 'a>> + 'static
|
2025-04-09 23:15:07 +02:00
|
|
|
{
|
|
|
|
|
}
|
2025-01-17 22:29:57 -05:00
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
impl Context {
|
2024-11-15 09:47:03 -07:00
|
|
|
pub fn new(controller: Controller) -> Self {
|
2024-11-13 14:36:11 -07:00
|
|
|
Self {
|
2025-04-09 23:15:07 +02:00
|
|
|
// 128K is the optimal upper size of a buffer.
|
|
|
|
|
buf: vec![0u8; 128 * 1024],
|
2024-11-15 09:47:03 -07:00
|
|
|
controller,
|
2024-11-13 14:36:11 -07:00
|
|
|
on_progress: Box::new(|_op, _progress| {}),
|
2026-02-11 21:55:44 +01:00
|
|
|
on_replace: Box::pin(|_op, _count| Box::pin(async { ReplaceResult::Cancel })),
|
2024-11-19 20:17:58 -07:00
|
|
|
op_sel: OperationSelection::default(),
|
2024-11-13 14:36:11 -07:00
|
|
|
replace_result_opt: None,
|
2026-02-11 21:55:44 +01:00
|
|
|
remaining_conflicts: 0,
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 23:15:07 +02:00
|
|
|
pub async fn recursive_copy_or_move(
|
2024-11-13 14:36:11 -07:00
|
|
|
&mut self,
|
2025-10-28 13:10:40 +10:00
|
|
|
from_to_pairs: impl IntoIterator<Item = (PathBuf, PathBuf)>,
|
2025-04-29 18:07:57 -06:00
|
|
|
method: Method,
|
2025-09-01 01:28:26 -04:00
|
|
|
) -> Result<bool, OperationError> {
|
2024-11-13 14:36:11 -07:00
|
|
|
let mut ops = Vec::new();
|
|
|
|
|
let mut cleanup_ops = Vec::new();
|
2026-01-14 21:32:26 +01:00
|
|
|
let mut written_files = Vec::new();
|
|
|
|
|
let mut target_dirs = std::collections::HashSet::new();
|
2024-11-13 14:36:11 -07:00
|
|
|
for (from_parent, to_parent) in from_to_pairs {
|
2025-09-01 01:28:26 -04:00
|
|
|
self.controller
|
|
|
|
|
.check()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|s| OperationError::from_state(s, &self.controller))?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
if from_parent == to_parent {
|
|
|
|
|
// Skip matching source and destination
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-26 16:20:51 +10:00
|
|
|
for entry in WalkDir::new(&from_parent) {
|
2025-09-01 01:28:26 -04:00
|
|
|
self.controller
|
|
|
|
|
.check()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|s| OperationError::from_state(s, &self.controller))?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
let entry = entry.map_err(|err| {
|
2025-09-01 01:28:26 -04:00
|
|
|
OperationError::from_err(
|
2025-10-26 16:20:51 +10:00
|
|
|
format!(
|
|
|
|
|
"failed to walk directory {}: {}",
|
|
|
|
|
from_parent.display(),
|
|
|
|
|
err
|
|
|
|
|
),
|
2025-09-01 01:28:26 -04:00
|
|
|
&self.controller,
|
|
|
|
|
)
|
2024-11-13 14:36:11 -07:00
|
|
|
})?;
|
|
|
|
|
let file_type = entry.file_type();
|
|
|
|
|
let from = entry.into_path();
|
|
|
|
|
let kind = if file_type.is_dir() {
|
|
|
|
|
OpKind::Mkdir
|
|
|
|
|
} else if file_type.is_file() {
|
2025-04-29 18:07:57 -06:00
|
|
|
match method {
|
|
|
|
|
Method::Copy => OpKind::Copy,
|
|
|
|
|
Method::Move { cross_device_copy } => OpKind::Move { cross_device_copy },
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
} else if file_type.is_symlink() {
|
2025-09-01 01:28:26 -04:00
|
|
|
let target = fs::read_link(&from).map_err(|err| {
|
|
|
|
|
OperationError::from_err(
|
2025-10-26 16:20:51 +10:00
|
|
|
format!("failed to read link {}: {}", from_parent.display(), err),
|
2025-09-01 01:28:26 -04:00
|
|
|
&self.controller,
|
|
|
|
|
)
|
|
|
|
|
})?;
|
2024-11-13 14:36:11 -07:00
|
|
|
OpKind::Symlink { target }
|
|
|
|
|
} else {
|
|
|
|
|
//TODO: present dialog and allow continue
|
2025-09-01 01:28:26 -04:00
|
|
|
return Err(OperationError::from_err(
|
|
|
|
|
format!("{} is not a known file type", from.display()),
|
|
|
|
|
&self.controller,
|
|
|
|
|
));
|
2024-11-13 14:36:11 -07:00
|
|
|
};
|
|
|
|
|
let to = if from == from_parent {
|
|
|
|
|
// When copying a file, from matches from_parent, and to_parent must be used
|
|
|
|
|
to_parent.clone()
|
|
|
|
|
} else {
|
|
|
|
|
let relative = from.strip_prefix(&from_parent).map_err(|err| {
|
2025-09-01 01:28:26 -04:00
|
|
|
OperationError::from_err(
|
|
|
|
|
format!(
|
2025-10-26 16:20:51 +10:00
|
|
|
"failed to remove prefix {} from {}: {}",
|
|
|
|
|
from_parent.display(),
|
|
|
|
|
from.display(),
|
|
|
|
|
err
|
2025-09-01 01:28:26 -04:00
|
|
|
),
|
|
|
|
|
&self.controller,
|
2024-11-13 14:36:11 -07:00
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
//TODO: ensure to is inside of to_parent?
|
|
|
|
|
to_parent.join(relative)
|
|
|
|
|
};
|
2025-03-07 09:29:48 -07:00
|
|
|
let op = Op {
|
|
|
|
|
kind,
|
|
|
|
|
from,
|
|
|
|
|
to,
|
2025-04-29 18:07:57 -06:00
|
|
|
skipped: Rc::new(Skip {
|
|
|
|
|
normal: Cell::new(false),
|
|
|
|
|
cleanup: Cell::new(false),
|
|
|
|
|
}),
|
|
|
|
|
is_cleanup: false,
|
2025-03-07 09:29:48 -07:00
|
|
|
};
|
2026-01-24 17:03:31 +01:00
|
|
|
if matches!(method, Method::Move { .. })
|
|
|
|
|
&& let Some(cleanup_op) = op.move_cleanup_op()
|
|
|
|
|
{
|
|
|
|
|
cleanup_ops.push(cleanup_op);
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
2026-01-14 21:32:26 +01:00
|
|
|
if let Some(parent) = op.to.parent() {
|
|
|
|
|
target_dirs.insert(parent.to_path_buf());
|
|
|
|
|
}
|
2024-11-13 14:36:11 -07:00
|
|
|
ops.push(op);
|
|
|
|
|
}
|
2024-11-19 20:17:58 -07:00
|
|
|
|
|
|
|
|
self.op_sel.ignored.push(from_parent);
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add cleanup ops after standard ops, in reverse
|
2025-10-28 13:10:40 +10:00
|
|
|
cleanup_ops.reverse();
|
|
|
|
|
ops.append(&mut cleanup_ops);
|
2024-11-13 14:36:11 -07:00
|
|
|
|
2026-02-11 21:55:44 +01:00
|
|
|
// Count potential conflicts (files that would need replacement)
|
|
|
|
|
self.remaining_conflicts = ops
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|op| {
|
|
|
|
|
matches!(
|
|
|
|
|
op.kind,
|
|
|
|
|
OpKind::Copy | OpKind::Move { .. } | OpKind::Symlink { .. }
|
|
|
|
|
) && op.to.is_file()
|
|
|
|
|
})
|
|
|
|
|
.count();
|
|
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
let total_ops = ops.len();
|
|
|
|
|
for (current_ops, mut op) in ops.into_iter().enumerate() {
|
2025-09-01 01:28:26 -04:00
|
|
|
self.controller
|
|
|
|
|
.check()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|s| OperationError::from_state(s, &self.controller))?;
|
2024-11-14 14:43:45 -07:00
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
let progress = Progress {
|
|
|
|
|
current_ops,
|
|
|
|
|
total_ops,
|
|
|
|
|
current_bytes: 0,
|
|
|
|
|
total_bytes: None,
|
|
|
|
|
};
|
|
|
|
|
(self.on_progress)(&op, &progress);
|
2025-04-09 23:15:07 +02:00
|
|
|
if op.run(self, progress).await.map_err(|err| {
|
2025-09-01 01:28:26 -04:00
|
|
|
OperationError::from_err(
|
|
|
|
|
format!(
|
2025-10-26 16:20:51 +10:00
|
|
|
"failed to {:?} {} to {}: {}",
|
|
|
|
|
op.kind,
|
|
|
|
|
op.from.display(),
|
|
|
|
|
op.to.display(),
|
|
|
|
|
err
|
2025-09-01 01:28:26 -04:00
|
|
|
),
|
|
|
|
|
&self.controller,
|
2024-11-13 14:36:11 -07:00
|
|
|
)
|
|
|
|
|
})? {
|
2026-01-14 21:32:26 +01:00
|
|
|
if matches!(
|
|
|
|
|
op.kind,
|
|
|
|
|
OpKind::Copy
|
|
|
|
|
| OpKind::Move {
|
|
|
|
|
cross_device_copy: true
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
written_files.push(op.to.clone());
|
|
|
|
|
}
|
2024-11-19 20:17:58 -07:00
|
|
|
// The from path is ignored in the operation selection if it is a top level item
|
|
|
|
|
if self.op_sel.ignored.contains(&op.from) {
|
|
|
|
|
// So add the to path to the selection
|
2026-01-14 21:32:26 +01:00
|
|
|
self.op_sel.selected.push(op.to);
|
2024-11-19 20:17:58 -07:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Cancelled
|
2024-11-13 14:36:11 -07:00
|
|
|
return Ok(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 13:48:31 +01:00
|
|
|
// Flush files to disk
|
|
|
|
|
sync_to_disk(written_files, target_dirs).await;
|
2026-01-14 21:32:26 +01:00
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-17 22:29:57 -05:00
|
|
|
pub fn on_progress<F: OnProgress>(mut self, f: F) -> Self {
|
2024-11-13 14:36:11 -07:00
|
|
|
self.on_progress = Box::new(f);
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 23:15:07 +02:00
|
|
|
pub fn on_replace(mut self, f: impl OnReplace + 'static) -> Self {
|
|
|
|
|
self.on_replace = Box::pin(f);
|
2024-11-13 14:36:11 -07:00
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 23:15:07 +02:00
|
|
|
async fn replace(&mut self, op: &Op) -> Result<ControlFlow<bool, PathBuf>, Box<dyn Error>> {
|
|
|
|
|
let replace_result = match self.replace_result_opt {
|
|
|
|
|
Some(result) => result,
|
2026-02-11 21:55:44 +01:00
|
|
|
None => (self.on_replace)(op, self.remaining_conflicts).await,
|
2025-04-09 23:15:07 +02:00
|
|
|
};
|
|
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
match replace_result {
|
|
|
|
|
ReplaceResult::Replace(apply_to_all) => {
|
|
|
|
|
if apply_to_all {
|
|
|
|
|
self.replace_result_opt = Some(replace_result);
|
|
|
|
|
}
|
2025-04-09 23:15:07 +02:00
|
|
|
compio::fs::remove_file(&op.to).await?;
|
2024-11-13 14:36:11 -07:00
|
|
|
Ok(ControlFlow::Continue(op.to.clone()))
|
|
|
|
|
}
|
|
|
|
|
ReplaceResult::KeepBoth => match op.to.parent() {
|
2025-01-17 22:29:57 -05:00
|
|
|
Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(&op.from, to_parent))),
|
2025-10-26 16:20:51 +10:00
|
|
|
None => Err(format!("failed to get parent of {}", op.to.display()).into()),
|
2024-11-13 14:36:11 -07:00
|
|
|
},
|
|
|
|
|
ReplaceResult::Skip(apply_to_all) => {
|
|
|
|
|
if apply_to_all {
|
|
|
|
|
self.replace_result_opt = Some(replace_result);
|
|
|
|
|
}
|
2025-04-29 18:07:57 -06:00
|
|
|
op.skipped.normal.set(true);
|
2024-11-13 14:36:11 -07:00
|
|
|
Ok(ControlFlow::Break(true))
|
|
|
|
|
}
|
|
|
|
|
ReplaceResult::Cancel => Ok(ControlFlow::Break(false)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Progress {
|
|
|
|
|
pub current_ops: usize,
|
|
|
|
|
pub total_ops: usize,
|
|
|
|
|
pub current_bytes: u64,
|
|
|
|
|
pub total_bytes: Option<u64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum OpKind {
|
|
|
|
|
Copy,
|
2025-04-29 18:07:57 -06:00
|
|
|
Move { cross_device_copy: bool },
|
2024-11-13 14:36:11 -07:00
|
|
|
Mkdir,
|
|
|
|
|
Remove,
|
|
|
|
|
Rmdir,
|
|
|
|
|
Symlink { target: PathBuf },
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-29 18:07:57 -06:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Skip {
|
|
|
|
|
/// Normal operation should be skipped
|
|
|
|
|
pub normal: Cell<bool>,
|
|
|
|
|
/// Cleanup operation should be skipped
|
|
|
|
|
pub cleanup: Cell<bool>,
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Op {
|
|
|
|
|
pub kind: OpKind,
|
|
|
|
|
pub from: PathBuf,
|
|
|
|
|
pub to: PathBuf,
|
2025-04-29 18:07:57 -06:00
|
|
|
pub skipped: Rc<Skip>,
|
|
|
|
|
pub is_cleanup: bool,
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Op {
|
|
|
|
|
fn move_cleanup_op(&self) -> Option<Self> {
|
|
|
|
|
let kind = match self.kind {
|
2025-04-29 18:07:57 -06:00
|
|
|
OpKind::Copy | OpKind::Move { .. } | OpKind::Symlink { .. } => OpKind::Remove,
|
2024-11-13 14:36:11 -07:00
|
|
|
OpKind::Mkdir => OpKind::Rmdir,
|
|
|
|
|
OpKind::Remove | OpKind::Rmdir => return None,
|
|
|
|
|
};
|
|
|
|
|
Some(Self {
|
|
|
|
|
kind,
|
|
|
|
|
from: self.from.clone(),
|
|
|
|
|
//TODO: it is strange to have `to` here
|
|
|
|
|
to: self.to.clone(),
|
2025-03-07 09:29:48 -07:00
|
|
|
skipped: self.skipped.clone(),
|
2025-04-29 18:07:57 -06:00
|
|
|
is_cleanup: true,
|
2024-11-13 14:36:11 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 06:03:16 +02:00
|
|
|
async fn run(&mut self, ctx: &mut Context, progress: Progress) -> Result<bool, Box<dyn Error>> {
|
2025-04-29 18:07:57 -06:00
|
|
|
if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) {
|
2025-03-07 09:29:48 -07:00
|
|
|
return Ok(true);
|
|
|
|
|
}
|
2024-11-13 14:36:11 -07:00
|
|
|
match self.kind {
|
|
|
|
|
OpKind::Copy => {
|
2026-04-10 06:08:21 +02:00
|
|
|
crate::operation::actively_writing_add(self.to.clone());
|
2026-04-10 06:03:16 +02:00
|
|
|
let result = self.copy(ctx, progress).await;
|
2025-04-09 23:15:07 +02:00
|
|
|
|
2026-04-10 06:03:16 +02:00
|
|
|
if result.is_err() {
|
|
|
|
|
_ = compio::fs::remove_file(&self.to).await;
|
2025-05-15 11:40:59 -06:00
|
|
|
}
|
2025-04-09 23:15:07 +02:00
|
|
|
|
2026-04-10 06:08:21 +02:00
|
|
|
crate::operation::actively_writing_remove(&self.to);
|
2026-04-10 06:03:16 +02:00
|
|
|
return result;
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
2025-04-29 18:07:57 -06:00
|
|
|
OpKind::Move { cross_device_copy } => {
|
2026-04-10 06:03:16 +02:00
|
|
|
// Do not clean up if cross_device_copy is set
|
|
|
|
|
if cross_device_copy {
|
|
|
|
|
self.skipped.cleanup.set(true);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-13 14:36:11 -07:00
|
|
|
// Remove `to` if overwriting and it is an existing file
|
|
|
|
|
if self.to.is_file() {
|
2025-04-09 23:15:07 +02:00
|
|
|
match ctx.replace(self).await? {
|
2024-11-13 14:36:11 -07:00
|
|
|
ControlFlow::Continue(to) => {
|
|
|
|
|
self.to = to;
|
|
|
|
|
}
|
|
|
|
|
ControlFlow::Break(ret) => {
|
|
|
|
|
return Ok(ret);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// This is atomic and ensures `to` is not created by any other process
|
2025-04-09 23:15:07 +02:00
|
|
|
match compio::fs::hard_link(&self.from, &self.to).await {
|
2024-11-13 14:36:11 -07:00
|
|
|
Ok(()) => {}
|
|
|
|
|
Err(err) => {
|
2025-04-09 23:15:07 +02:00
|
|
|
// https://docs.rs/windows-sys/latest/windows_sys/Win32/Foundation/constant.ERROR_NOT_SAME_DEVICE.html
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
const EXDEV: i32 = 17;
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
const EXDEV: i32 = libc::EXDEV as _;
|
|
|
|
|
|
|
|
|
|
if err.raw_os_error() == Some(EXDEV) {
|
2025-04-29 18:07:57 -06:00
|
|
|
if cross_device_copy {
|
|
|
|
|
// Do not clean up if cross_device_copy is set
|
|
|
|
|
self.skipped.cleanup.set(true);
|
|
|
|
|
}
|
2024-11-13 14:36:11 -07:00
|
|
|
// Try standard copy if hard link fails with cross device error
|
2025-10-26 16:20:51 +10:00
|
|
|
let mut copy_op = Self {
|
2024-11-13 14:36:11 -07:00
|
|
|
kind: OpKind::Copy,
|
|
|
|
|
from: self.from.clone(),
|
|
|
|
|
to: self.to.clone(),
|
2025-03-07 09:29:48 -07:00
|
|
|
skipped: self.skipped.clone(),
|
2025-04-29 18:07:57 -06:00
|
|
|
is_cleanup: self.is_cleanup,
|
2024-11-13 14:36:11 -07:00
|
|
|
};
|
2025-04-09 23:15:07 +02:00
|
|
|
return Box::pin(copy_op.run(ctx, progress)).await;
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
2025-10-26 16:20:51 +10:00
|
|
|
return Err(err.into());
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
OpKind::Mkdir => {
|
2025-04-09 23:15:07 +02:00
|
|
|
compio::fs::create_dir_all(&self.to).await?;
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
OpKind::Remove => {
|
2025-04-09 23:15:07 +02:00
|
|
|
compio::fs::remove_file(&self.from).await?;
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
OpKind::Rmdir => {
|
2025-04-09 23:15:07 +02:00
|
|
|
compio::fs::remove_dir(&self.from).await?;
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
OpKind::Symlink { ref target } => {
|
|
|
|
|
// Remove `to` if overwriting and it is an existing file
|
|
|
|
|
if self.to.is_file() {
|
2025-04-09 23:15:07 +02:00
|
|
|
match ctx.replace(self).await? {
|
2024-11-13 14:36:11 -07:00
|
|
|
ControlFlow::Continue(to) => {
|
|
|
|
|
self.to = to;
|
|
|
|
|
}
|
|
|
|
|
ControlFlow::Break(ret) => {
|
|
|
|
|
return Ok(ret);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-17 22:29:57 -05:00
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
std::os::unix::fs::symlink(target, &self.to)?;
|
|
|
|
|
}
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
{
|
|
|
|
|
if target.is_dir() {
|
|
|
|
|
std::os::windows::fs::symlink_dir(target, &self.to)?;
|
|
|
|
|
} else {
|
|
|
|
|
std::os::windows::fs::symlink_file(target, &self.to)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
2026-04-10 06:03:16 +02:00
|
|
|
|
|
|
|
|
async fn copy(
|
|
|
|
|
&mut self,
|
|
|
|
|
ctx: &mut Context,
|
|
|
|
|
mut progress: Progress,
|
|
|
|
|
) -> Result<bool, Box<dyn Error>> {
|
|
|
|
|
// Remove `to` if overwriting and it is an existing file
|
|
|
|
|
if self.to.is_file() {
|
|
|
|
|
match ctx.replace(self).await? {
|
|
|
|
|
ControlFlow::Continue(to) => {
|
|
|
|
|
self.to = to;
|
|
|
|
|
}
|
|
|
|
|
ControlFlow::Break(ret) => {
|
|
|
|
|
return Ok(ret);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (from_file, metadata, to_file) = cosmic::iced::futures::join!(
|
|
|
|
|
async {
|
|
|
|
|
compio::fs::OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.open(&self.from)
|
|
|
|
|
.await
|
|
|
|
|
.with_context(|| format!("failed to open {} for reading", self.from.display(),))
|
|
|
|
|
},
|
|
|
|
|
async { compio::fs::metadata(&self.from).await.ok() },
|
|
|
|
|
// This is atomic and ensures `to` is not created by any other process
|
|
|
|
|
async {
|
|
|
|
|
compio::fs::OpenOptions::new()
|
|
|
|
|
.create_new(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(&self.to)
|
|
|
|
|
.await
|
|
|
|
|
.with_context(|| format!("failed to open {} for writing", self.to.display()))
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let from_file = from_file?;
|
|
|
|
|
let mut to_file = to_file?;
|
|
|
|
|
progress.total_bytes = metadata.as_ref().map(|m| m.len());
|
|
|
|
|
(ctx.on_progress)(self, &progress);
|
|
|
|
|
|
2026-04-28 14:45:01 +02:00
|
|
|
if let Some(metadata) = metadata.as_ref()
|
|
|
|
|
&& let Err(why) = to_file.set_permissions(metadata.permissions()).await
|
|
|
|
|
{
|
|
|
|
|
// This error is not propagated upwards as some filesystems do not support setting permissions
|
|
|
|
|
if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {
|
|
|
|
|
tracing::warn!(?why, "failed to set permissions for {}", self.to.display(),);
|
2026-04-10 06:03:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prevent spamming the progress callbacks.
|
|
|
|
|
let mut last_progress_update = Instant::now();
|
|
|
|
|
// io_uring/IOCP requires transferring ownership of the buffer to the kernel.
|
|
|
|
|
let mut buf_in = std::mem::take(&mut ctx.buf);
|
|
|
|
|
// Track where the current read/write position is at.
|
|
|
|
|
let mut pos = 0;
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let BufResult(result, buf_out) = from_file.read_at(buf_in, pos).await;
|
|
|
|
|
|
|
|
|
|
let count = match result {
|
|
|
|
|
Ok(0) => {
|
|
|
|
|
buf_in = buf_out;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
Ok(count) => count,
|
|
|
|
|
Err(why) => {
|
|
|
|
|
ctx.buf = buf_out;
|
|
|
|
|
tracing::error!("failed to read: {:?}", why);
|
|
|
|
|
_ = futures::future::join(from_file.close(), to_file.close()).await;
|
|
|
|
|
return Err(why).context("failed to read")?;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let BufResult(result, buf_out_slice) =
|
|
|
|
|
to_file.write_at(buf_out.slice(..count), pos).await;
|
|
|
|
|
let buf_out = buf_out_slice.into_inner();
|
|
|
|
|
|
|
|
|
|
if let Err(why) = result {
|
2026-04-14 17:15:19 +02:00
|
|
|
#[cfg(feature = "gvfs")]
|
2026-04-10 06:03:16 +02:00
|
|
|
if let std::io::ErrorKind::Unsupported = why.kind() {
|
|
|
|
|
ctx.buf = buf_out;
|
|
|
|
|
_ = futures::future::join(from_file.close(), to_file.close()).await;
|
|
|
|
|
return self
|
|
|
|
|
.gio_file_copy(ctx, progress)
|
|
|
|
|
.await
|
|
|
|
|
.map(|_| true)
|
|
|
|
|
.map_err(Into::into);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::error!("failed to write: {:?}", why);
|
|
|
|
|
ctx.buf = buf_out;
|
|
|
|
|
_ = futures::future::join(from_file.close(), to_file.close()).await;
|
|
|
|
|
return Err(why).context("failed to write")?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
progress.current_bytes += count as u64;
|
|
|
|
|
pos += count as u64;
|
|
|
|
|
|
|
|
|
|
// Avoid spamming progress messages too early.
|
|
|
|
|
let current = Instant::now();
|
|
|
|
|
if current.duration_since(last_progress_update).as_millis() > 49 {
|
|
|
|
|
last_progress_update = current;
|
|
|
|
|
(ctx.on_progress)(self, &progress);
|
|
|
|
|
|
|
|
|
|
// Also check if the progress was cancelled.
|
|
|
|
|
if let Err(state) = ctx.controller.check().await {
|
|
|
|
|
ctx.buf = buf_out;
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
"operation to copy from {:?} to {:?} cancelled",
|
|
|
|
|
self.from,
|
|
|
|
|
self.to
|
|
|
|
|
);
|
|
|
|
|
_ = futures::future::join(from_file.close(), to_file.close()).await;
|
|
|
|
|
return Err(OperationError::from_state(state, &ctx.controller).into());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buf_in = buf_out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.buf = buf_in;
|
|
|
|
|
|
|
|
|
|
if let Some(metadata) = metadata.as_ref() {
|
|
|
|
|
let mut times = fs::FileTimes::new();
|
|
|
|
|
if let Ok(time) = metadata.modified() {
|
|
|
|
|
times = times.set_modified(time);
|
|
|
|
|
}
|
|
|
|
|
if let Ok(time) = metadata.accessed() {
|
|
|
|
|
times = times.set_accessed(time);
|
|
|
|
|
}
|
|
|
|
|
//TODO: upstream set_times implementation to compio?
|
|
|
|
|
let op = AsyncifyFd::new(to_file.to_shared_fd(), move |file: &std::fs::File| {
|
|
|
|
|
BufResult(file.set_times(times).map(|_| 0), ())
|
|
|
|
|
});
|
|
|
|
|
match compio::runtime::submit(op).await.0.map(|_| ()) {
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
tracing::info!("set times for {} to {:?}", self.to.display(), times);
|
|
|
|
|
}
|
|
|
|
|
Err(why) => {
|
|
|
|
|
if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {
|
|
|
|
|
tracing::error!(?why, "failed to set times for {}", self.to.display());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = to_file.close().await;
|
|
|
|
|
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Fallback mechanism in the event that unsupported I/O error errors occur.
|
|
|
|
|
/// Fixes unsupported errors when copying large files over MTP.
|
|
|
|
|
/// TODO: Find what Gio.File does to work around this.
|
2026-04-14 17:15:19 +02:00
|
|
|
#[cfg(feature = "gvfs")]
|
2026-04-10 06:03:16 +02:00
|
|
|
async fn gio_file_copy(
|
|
|
|
|
&self,
|
|
|
|
|
ctx: &mut Context,
|
|
|
|
|
mut progress: Progress,
|
|
|
|
|
) -> Result<(), GioCopyError> {
|
|
|
|
|
_ = compio::fs::remove_file(&self.to).await;
|
|
|
|
|
|
|
|
|
|
let from = gio::File::for_path(&self.from);
|
|
|
|
|
let to = gio::File::for_path(&self.to);
|
|
|
|
|
let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
|
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
|
|
|
let (pause_tx, mut pause_rx) = tokio::sync::watch::channel(false);
|
|
|
|
|
|
|
|
|
|
let task = compio::runtime::spawn_blocking(move || {
|
|
|
|
|
let glib_context = glib::MainContext::new();
|
|
|
|
|
let glib_loop = glib::MainLoop::new(Some(&glib_context), false);
|
|
|
|
|
glib_context.with_thread_default(move || {
|
|
|
|
|
let glib_loop2 = glib_loop.clone();
|
|
|
|
|
glib::MainContext::ref_thread_default().spawn_local(async move {
|
|
|
|
|
// Create a future for copying the file with `gio::File`. This also creates a progress stream.
|
|
|
|
|
let (gio_copy_fut, mut progress_stream) = from.copy_future(
|
|
|
|
|
&to,
|
|
|
|
|
gio::FileCopyFlags::OVERWRITE | gio::FileCopyFlags::ALL_METADATA,
|
|
|
|
|
glib::Priority::LOW,
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-14 16:51:21 +02:00
|
|
|
let mut copy_fut = gio_copy_fut
|
|
|
|
|
.map(|result| result.map_err(GioCopyError::GLib))
|
|
|
|
|
.fuse();
|
2026-04-10 06:03:16 +02:00
|
|
|
|
2026-04-14 16:51:21 +02:00
|
|
|
let progress_fut = std::pin::pin!(async {
|
2026-04-10 06:03:16 +02:00
|
|
|
while let Some((current_bytes, _)) = progress_stream.next().await {
|
|
|
|
|
_ = progress_tx.send(current_bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drop(progress_tx);
|
|
|
|
|
futures::future::pending::<()>().await;
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-14 16:51:21 +02:00
|
|
|
let mut progress_fut = progress_fut.fuse();
|
2026-04-10 06:03:16 +02:00
|
|
|
let mut pause_rx2 = pause_rx.clone();
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let until_paused = std::pin::pin!(pause_rx.wait_for(|paused| *paused));
|
2026-04-14 16:51:21 +02:00
|
|
|
futures::select! {
|
|
|
|
|
_ = &mut progress_fut => {},
|
2026-04-10 06:03:16 +02:00
|
|
|
|
2026-04-14 16:51:21 +02:00
|
|
|
result = &mut copy_fut => {
|
2026-04-10 06:03:16 +02:00
|
|
|
_ = tx.send(result.map(|_| ()));
|
|
|
|
|
glib_loop2.quit();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-14 16:51:21 +02:00
|
|
|
|
|
|
|
|
_ = until_paused.fuse() => {
|
|
|
|
|
_ = pause_rx2.wait_for(|paused| !*paused).await;
|
|
|
|
|
}
|
2026-04-10 06:03:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
glib_loop.run();
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut last_progress_update = Instant::now();
|
|
|
|
|
let mut task = task.fuse();
|
|
|
|
|
let mut rx = rx.fuse();
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let until_paused = std::pin::pin!(ctx.controller.until_paused());
|
|
|
|
|
futures::select! {
|
|
|
|
|
value = progress_rx.recv().fuse() => {
|
|
|
|
|
if let Some(current_bytes) = value {
|
|
|
|
|
progress.current_bytes = current_bytes as u64;
|
|
|
|
|
let current = Instant::now();
|
|
|
|
|
if current.duration_since(last_progress_update).as_millis() > 49 {
|
|
|
|
|
last_progress_update = current;
|
|
|
|
|
(ctx.on_progress)(self, &progress);
|
|
|
|
|
// Also check if the progress was cancelled.
|
|
|
|
|
if let Err(state) = ctx.controller.check().await {
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
"operation to copy from {:?} to {:?} cancelled",
|
|
|
|
|
self.from,
|
|
|
|
|
self.to
|
|
|
|
|
);
|
|
|
|
|
return Err::<(), GioCopyError>(GioCopyError::Controller(
|
|
|
|
|
OperationError::from_state(state, &ctx.controller),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = rx => return result.unwrap(),
|
|
|
|
|
|
|
|
|
|
_ = task => (),
|
|
|
|
|
|
|
|
|
|
_ = until_paused.fuse() => {
|
|
|
|
|
// Pauses an active copy while the controller state is paused.
|
|
|
|
|
_ = pause_tx.send(true);
|
|
|
|
|
ctx.controller.until_unpaused().await;
|
|
|
|
|
_ = pause_tx.send(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-13 14:36:11 -07:00
|
|
|
}
|