mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-05-17 22:46:13 +08:00
Feat/macos clipboard file (#10939)
* feat: macos, clipboard file Signed-off-by: fufesou <linlong1266@gmail.com> * Can't reuse file transfer Signed-off-by: fufesou <linlong1266@gmail.com> * handle paste task Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
37
Cargo.lock
generated
37
Cargo.lock
generated
@ -978,10 +978,15 @@ dependencies = [
|
||||
"cacao",
|
||||
"cc",
|
||||
"dashmap",
|
||||
"dirs 5.0.1",
|
||||
"fsevent",
|
||||
"fuser",
|
||||
"hbb_common",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
@ -990,8 +995,10 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"thiserror",
|
||||
"utf16string",
|
||||
"uuid",
|
||||
"x11-clipboard 0.8.1",
|
||||
"x11rb 0.12.0",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2218,6 +2225,25 @@ dependencies = [
|
||||
"time 0.1.45",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8836d1f147a0a195bf517a5fd211ea7023d19ced903135faf6c4504f2cf8775f"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"fsevent-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
@ -7999,6 +8025,17 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"rustix 0.38.34",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.2.0"
|
||||
|
@ -47,3 +47,11 @@ fuser = {version = "0.15", default-features = false, optional = true}
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true}
|
||||
# Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef`
|
||||
objc2 = { version = "0.5.1", features = ["relax-void-encoding"] }
|
||||
objc2-foundation = { version = "0.2.0", features = ["NSArray", "NSString", "NSEnumerator", "NSGeometry", "NSProgress"] }
|
||||
objc2-app-kit = { version = "0.2.0", features = ["NSPasteboard", "NSPasteboardItem", "NSImage", "NSFilePromiseProvider"] }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
fsevent = "2.1.2"
|
||||
dirs = "5.0"
|
||||
xattr = "1.4.0"
|
||||
|
@ -1,22 +1,29 @@
|
||||
use hbb_common::{log, ResultType};
|
||||
use std::sync::Mutex;
|
||||
use std::{ops::Deref, sync::Mutex};
|
||||
|
||||
use crate::CliprdrServiceContext;
|
||||
|
||||
const CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS: u32 = 30;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CONTEXT_SEND: ContextSend = ContextSend{addr: Mutex::new(None)};
|
||||
static ref CONTEXT_SEND: ContextSend = ContextSend::default();
|
||||
}
|
||||
|
||||
pub struct ContextSend {
|
||||
addr: Mutex<Option<Box<dyn CliprdrServiceContext>>>,
|
||||
#[derive(Default)]
|
||||
pub struct ContextSend(Mutex<Option<Box<dyn CliprdrServiceContext>>>);
|
||||
|
||||
impl Deref for ContextSend {
|
||||
type Target = Mutex<Option<Box<dyn CliprdrServiceContext>>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextSend {
|
||||
#[inline]
|
||||
pub fn is_enabled() -> bool {
|
||||
CONTEXT_SEND.addr.lock().unwrap().is_some()
|
||||
CONTEXT_SEND.lock().unwrap().is_some()
|
||||
}
|
||||
|
||||
pub fn set_is_stopped() {
|
||||
@ -24,7 +31,7 @@ impl ContextSend {
|
||||
}
|
||||
|
||||
pub fn enable(enabled: bool) {
|
||||
let mut lock = CONTEXT_SEND.addr.lock().unwrap();
|
||||
let mut lock = CONTEXT_SEND.lock().unwrap();
|
||||
if enabled {
|
||||
if lock.is_some() {
|
||||
return;
|
||||
@ -49,7 +56,7 @@ impl ContextSend {
|
||||
|
||||
/// make sure the clipboard context is enabled.
|
||||
pub fn make_sure_enabled() -> ResultType<()> {
|
||||
let mut lock = CONTEXT_SEND.addr.lock().unwrap();
|
||||
let mut lock = CONTEXT_SEND.lock().unwrap();
|
||||
if lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
@ -63,7 +70,7 @@ impl ContextSend {
|
||||
pub fn proc<F: FnOnce(&mut Box<dyn CliprdrServiceContext>) -> ResultType<()>>(
|
||||
f: F,
|
||||
) -> ResultType<()> {
|
||||
let mut lock = CONTEXT_SEND.addr.lock().unwrap();
|
||||
let mut lock = CONTEXT_SEND.lock().unwrap();
|
||||
match lock.as_mut() {
|
||||
Some(context) => f(context),
|
||||
None => Ok(()),
|
||||
|
@ -1,6 +1,9 @@
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "macos", feature = "unix-file-copy-paste")
|
||||
))]
|
||||
use hbb_common::ResultType;
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
use hbb_common::{allow_err, log};
|
||||
@ -14,10 +17,16 @@ use hbb_common::{
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "macos", feature = "unix-file-copy-paste")
|
||||
))]
|
||||
pub mod context_send;
|
||||
pub mod platform;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "macos", feature = "unix-file-copy-paste")
|
||||
))]
|
||||
pub use context_send::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@ -27,9 +36,18 @@ const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "macos", feature = "unix-file-copy-paste")
|
||||
))]
|
||||
pub(crate) use platform::create_cliprdr_context;
|
||||
|
||||
pub struct ProgressPercent {
|
||||
pub percent: f64,
|
||||
pub is_canceled: bool,
|
||||
pub is_failed: bool,
|
||||
}
|
||||
|
||||
// to-do: This trait may be removed, because unix file copy paste does not need it.
|
||||
/// Ability to handle Clipboard File from remote rustdesk client
|
||||
///
|
||||
@ -44,6 +62,10 @@ pub trait CliprdrServiceContext: Send + Sync {
|
||||
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError>;
|
||||
/// run as a server for clipboard RPC
|
||||
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>;
|
||||
/// get the progress of the paste task.
|
||||
fn get_progress_percent(&self) -> Option<ProgressPercent>;
|
||||
/// cancel the paste task.
|
||||
fn cancel(&mut self);
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@ -62,11 +84,11 @@ pub enum CliprdrError {
|
||||
ConversionFailure,
|
||||
#[error("failure to read clipboard")]
|
||||
OpenClipboard,
|
||||
#[error("failure to read file metadata or content")]
|
||||
#[error("failure to read file metadata or content, path: {path}, err: {err}")]
|
||||
FileError { path: String, err: std::io::Error },
|
||||
#[error("invalid request")]
|
||||
#[error("invalid request: {description}")]
|
||||
InvalidRequest { description: String },
|
||||
#[error("common request")]
|
||||
#[error("common request: {description}")]
|
||||
CommonError { description: String },
|
||||
#[error("unknown cliprdr error")]
|
||||
Unknown(u32),
|
||||
|
@ -14,3 +14,13 @@ pub fn create_cliprdr_context(
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub mod unix;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn create_cliprdr_context(
|
||||
_enable_files: bool,
|
||||
_enable_others: bool,
|
||||
_response_wait_timeout_secs: u32,
|
||||
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
|
||||
let boxed = unix::macos::pasteboard_context::create_pasteboard_context()? as Box<_>;
|
||||
Ok(boxed)
|
||||
}
|
||||
|
@ -4,15 +4,17 @@ use hbb_common::{
|
||||
bytes::{Buf, Bytes},
|
||||
log,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use utf16string::WStr;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub type Inode = u64;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
@ -28,10 +30,11 @@ pub const PERM_RW: u16 = 0o644;
|
||||
pub const PERM_SELF_RO: u16 = 0o400;
|
||||
/// rwx
|
||||
pub const PERM_RWX: u16 = 0o755;
|
||||
#[allow(dead_code)]
|
||||
/// max length of file name
|
||||
pub const MAX_NAME_LEN: usize = 255;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct FileDescription {
|
||||
pub conn_id: i32,
|
||||
pub name: PathBuf,
|
||||
@ -40,9 +43,7 @@ pub struct FileDescription {
|
||||
pub last_modified: SystemTime,
|
||||
pub last_metadata_changed: SystemTime,
|
||||
pub creation_time: SystemTime,
|
||||
|
||||
pub size: u64,
|
||||
|
||||
pub perm: u16,
|
||||
}
|
||||
|
||||
@ -144,7 +145,6 @@ impl FileDescription {
|
||||
atime: last_modified,
|
||||
last_modified,
|
||||
last_metadata_changed: last_modified,
|
||||
|
||||
creation_time: last_modified,
|
||||
size,
|
||||
perm,
|
||||
|
25
libs/clipboard/src/platform/unix/macos/README.md
Normal file
25
libs/clipboard/src/platform/unix/macos/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
# File pate on macOS
|
||||
|
||||
MacOS cannot use `fuse` because of [macfuse is not supported by default](https://github.com/macfuse/macfuse/wiki/Getting-Started#enabling-support-for-third-party-kernel-extensions-apple-silicon-macs).
|
||||
|
||||
1. Use a temporary file `/tmp/rustdesk_<uuid>` as a placeholder in the pasteboard.
|
||||
2. Uses `fsevent` to observe files paste operation. Then perform pasting files.
|
||||
|
||||
## Files
|
||||
|
||||
### `pasteboard_context.rs`
|
||||
|
||||
The context manager of the paste operations.
|
||||
|
||||
### `item_data_provider.rs`
|
||||
|
||||
1. Set pasteboard item.
|
||||
2. Create temp file in `/tmp/.rustdesk_*`.
|
||||
|
||||
### `paste_observer.rs`
|
||||
|
||||
Use `fsevent` to observe the paste operation with the source file `/tmp/.rustdesk_*`.
|
||||
|
||||
### `paste_task.rs`
|
||||
|
||||
Perform the paste.
|
77
libs/clipboard/src/platform/unix/macos/item_data_provider.rs
Normal file
77
libs/clipboard/src/platform/unix/macos/item_data_provider.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use super::pasteboard_context::{PasteObserverInfo, TEMP_FILE_PREFIX};
|
||||
use objc2::{
|
||||
declare_class, msg_send_id, mutability,
|
||||
rc::Id,
|
||||
runtime::{NSObject, NSObjectProtocol},
|
||||
ClassType, DeclaredClass,
|
||||
};
|
||||
use objc2_app_kit::{
|
||||
NSPasteboard, NSPasteboardItem, NSPasteboardItemDataProvider, NSPasteboardType,
|
||||
NSPasteboardTypeFileURL,
|
||||
};
|
||||
use objc2_foundation::NSString;
|
||||
use std::{io::Result, sync::mpsc::Sender};
|
||||
|
||||
pub(super) struct Ivars {
|
||||
task_info: PasteObserverInfo,
|
||||
tx: Sender<Result<PasteObserverInfo>>,
|
||||
}
|
||||
|
||||
declare_class!(
|
||||
pub(super) struct PasteboardFileUrlProvider;
|
||||
|
||||
unsafe impl ClassType for PasteboardFileUrlProvider {
|
||||
type Super = NSObject;
|
||||
type Mutability = mutability::InteriorMutable;
|
||||
const NAME: &'static str = "PasteboardFileUrlProvider";
|
||||
}
|
||||
|
||||
impl DeclaredClass for PasteboardFileUrlProvider {
|
||||
type Ivars = Ivars;
|
||||
}
|
||||
|
||||
unsafe impl NSObjectProtocol for PasteboardFileUrlProvider {}
|
||||
|
||||
unsafe impl NSPasteboardItemDataProvider for PasteboardFileUrlProvider {
|
||||
#[method(pasteboard:item:provideDataForType:)]
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn pasteboard_item_provideDataForType(
|
||||
&self,
|
||||
_pasteboard: Option<&NSPasteboard>,
|
||||
item: &NSPasteboardItem,
|
||||
r#type: &NSPasteboardType,
|
||||
) {
|
||||
if r#type == NSPasteboardTypeFileURL {
|
||||
let path = format!("/tmp/{}{}", TEMP_FILE_PREFIX, uuid::Uuid::new_v4().to_string());
|
||||
match std::fs::File::create(&path) {
|
||||
Ok(_) => {
|
||||
let url = format!("file:///{}", &path);
|
||||
item.setString_forType(&NSString::from_str(&url), &NSPasteboardTypeFileURL);
|
||||
let mut task_info = self.ivars().task_info.clone();
|
||||
task_info.source_path = path;
|
||||
self.ivars().tx.send(Ok(task_info)).ok();
|
||||
}
|
||||
Err(e) => {
|
||||
self.ivars().tx.send(Err(e)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[method(pasteboardFinishedWithDataProvider:)]
|
||||
// unsafe fn pasteboardFinishedWithDataProvider(&self, pasteboard: &NSPasteboard) {
|
||||
// }
|
||||
}
|
||||
|
||||
unsafe impl PasteboardFileUrlProvider {}
|
||||
);
|
||||
|
||||
pub(super) fn create_pasteboard_file_url_provider(
|
||||
task_info: PasteObserverInfo,
|
||||
tx: Sender<Result<PasteObserverInfo>>,
|
||||
) -> Id<PasteboardFileUrlProvider> {
|
||||
let provider = PasteboardFileUrlProvider::alloc();
|
||||
let provider = provider.set_ivars(Ivars { task_info, tx });
|
||||
let provider: Id<PasteboardFileUrlProvider> = unsafe { msg_send_id![super(provider), init] };
|
||||
provider
|
||||
}
|
14
libs/clipboard/src/platform/unix/macos/mod.rs
Normal file
14
libs/clipboard/src/platform/unix/macos/mod.rs
Normal file
@ -0,0 +1,14 @@
|
||||
mod item_data_provider;
|
||||
mod paste_observer;
|
||||
mod paste_task;
|
||||
pub mod pasteboard_context;
|
||||
|
||||
pub fn should_handle_msg(msg: &crate::ClipboardFile) -> bool {
|
||||
matches!(
|
||||
msg,
|
||||
crate::ClipboardFile::FormatList { .. }
|
||||
| crate::ClipboardFile::FormatDataResponse { .. }
|
||||
| crate::ClipboardFile::FileContentsResponse { .. }
|
||||
| crate::ClipboardFile::TryEmpty
|
||||
)
|
||||
}
|
BIN
libs/clipboard/src/platform/unix/macos/paste-files-macos.png
Normal file
BIN
libs/clipboard/src/platform/unix/macos/paste-files-macos.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
179
libs/clipboard/src/platform/unix/macos/paste_observer.rs
Normal file
179
libs/clipboard/src/platform/unix/macos/paste_observer.rs
Normal file
@ -0,0 +1,179 @@
|
||||
use super::pasteboard_context::PasteObserverInfo;
|
||||
use fsevent::{self, StreamFlags};
|
||||
use hbb_common::{bail, log, ResultType};
|
||||
use std::{
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, RecvTimeoutError, Sender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
enum FseventControl {
|
||||
Start,
|
||||
Stop,
|
||||
Exit,
|
||||
}
|
||||
|
||||
struct FseventThreadInfo {
|
||||
tx: Sender<FseventControl>,
|
||||
handle: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
pub struct PasteObserver {
|
||||
exit: Arc<Mutex<bool>>,
|
||||
observer_info: Arc<Mutex<Option<PasteObserverInfo>>>,
|
||||
tx_handle_fsevent_thread: Option<FseventThreadInfo>,
|
||||
handle_observer_thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Drop for PasteObserver {
|
||||
fn drop(&mut self) {
|
||||
*self.exit.lock().unwrap() = true;
|
||||
if let Some(handle_observer_thread) = self.handle_observer_thread.take() {
|
||||
handle_observer_thread.join().ok();
|
||||
}
|
||||
if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.take() {
|
||||
tx_handle_fsevent_thread.tx.send(FseventControl::Exit).ok();
|
||||
tx_handle_fsevent_thread.handle.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PasteObserver {
|
||||
const OBSERVE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
exit: Arc::new(Mutex::new(false)),
|
||||
observer_info: Default::default(),
|
||||
tx_handle_fsevent_thread: None,
|
||||
handle_observer_thread: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self, cb_pasted: fn(&PasteObserverInfo) -> ()) -> ResultType<()> {
|
||||
let Some(home_dir) = dirs::home_dir() else {
|
||||
bail!("No home dir is set, do not observe.");
|
||||
};
|
||||
|
||||
let (tx_observer, rx_observer) = channel::<fsevent::Event>();
|
||||
let handle_observer = Self::init_thread_observer(
|
||||
self.exit.clone(),
|
||||
self.observer_info.clone(),
|
||||
rx_observer,
|
||||
cb_pasted,
|
||||
);
|
||||
self.handle_observer_thread = Some(handle_observer);
|
||||
let (tx_control, rx_control) = channel::<FseventControl>();
|
||||
let handle_fsevent = Self::init_thread_fsevent(
|
||||
home_dir.to_string_lossy().to_string(),
|
||||
tx_observer,
|
||||
rx_control,
|
||||
);
|
||||
self.tx_handle_fsevent_thread = Some(FseventThreadInfo {
|
||||
tx: tx_control,
|
||||
handle: handle_fsevent,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_file_from_path(path: &String) -> String {
|
||||
let last_slash = path.rfind('/').or_else(|| path.rfind('\\'));
|
||||
match last_slash {
|
||||
Some(index) => path[index + 1..].to_string(),
|
||||
None => path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_thread_observer(
|
||||
exit: Arc<Mutex<bool>>,
|
||||
observer_info: Arc<Mutex<Option<PasteObserverInfo>>>,
|
||||
rx_observer: Receiver<fsevent::Event>,
|
||||
cb_pasted: fn(&PasteObserverInfo) -> (),
|
||||
) -> thread::JoinHandle<()> {
|
||||
thread::spawn(move || loop {
|
||||
match rx_observer.recv_timeout(Duration::from_millis(300)) {
|
||||
Ok(event) => {
|
||||
if (event.flag & StreamFlags::ITEM_CREATED) != StreamFlags::NONE
|
||||
&& (event.flag & StreamFlags::ITEM_REMOVED) == StreamFlags::NONE
|
||||
&& (event.flag & StreamFlags::IS_FILE) != StreamFlags::NONE
|
||||
{
|
||||
let source_file = observer_info
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|x| Self::get_file_from_path(&x.source_path));
|
||||
if let Some(source_file) = source_file {
|
||||
let file = Self::get_file_from_path(&event.path);
|
||||
if source_file == file {
|
||||
if let Some(observer_info) = observer_info.lock().unwrap().as_mut()
|
||||
{
|
||||
observer_info.target_path = event.path.clone();
|
||||
cb_pasted(observer_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if *(exit.lock().unwrap()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn new_fsevent(home_dir: String, tx_observer: Sender<fsevent::Event>) -> fsevent::FsEvent {
|
||||
let mut evt = fsevent::FsEvent::new(vec![home_dir.to_string()]);
|
||||
evt.observe_async(tx_observer).ok();
|
||||
evt
|
||||
}
|
||||
|
||||
fn init_thread_fsevent(
|
||||
home_dir: String,
|
||||
tx_observer: Sender<fsevent::Event>,
|
||||
rx_control: Receiver<FseventControl>,
|
||||
) -> thread::JoinHandle<()> {
|
||||
log::debug!("fsevent observe dir: {}", &home_dir);
|
||||
thread::spawn(move || {
|
||||
let mut fsevent = None;
|
||||
loop {
|
||||
match rx_control.recv_timeout(Self::OBSERVE_TIMEOUT) {
|
||||
Ok(FseventControl::Start) => {
|
||||
if fsevent.is_none() {
|
||||
fsevent =
|
||||
Some(Self::new_fsevent(home_dir.clone(), tx_observer.clone()));
|
||||
}
|
||||
}
|
||||
Ok(FseventControl::Stop) | Err(RecvTimeoutError::Timeout) => {
|
||||
let _ = fsevent.as_mut().map(|e| e.shutdown_observe());
|
||||
fsevent = None;
|
||||
}
|
||||
Ok(FseventControl::Exit) | Err(RecvTimeoutError::Disconnected) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("fsevent thread exit");
|
||||
let _ = fsevent.as_mut().map(|e| e.shutdown_observe());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start(&mut self, observer_info: PasteObserverInfo) {
|
||||
if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.as_ref() {
|
||||
self.observer_info.lock().unwrap().replace(observer_info);
|
||||
tx_handle_fsevent_thread.tx.send(FseventControl::Start).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
if let Some(tx_handle_fsevent_thread) = &self.tx_handle_fsevent_thread {
|
||||
self.observer_info = Default::default();
|
||||
tx_handle_fsevent_thread.tx.send(FseventControl::Stop).ok();
|
||||
}
|
||||
}
|
||||
}
|
639
libs/clipboard/src/platform/unix/macos/paste_task.rs
Normal file
639
libs/clipboard/src/platform/unix/macos/paste_task.rs
Normal file
@ -0,0 +1,639 @@
|
||||
use crate::{
|
||||
platform::unix::{FileDescription, FileType, BLOCK_SIZE},
|
||||
send_data, ClipboardFile, CliprdrError, ProgressPercent,
|
||||
};
|
||||
use hbb_common::{allow_err, log, tokio::time::Instant};
|
||||
use std::{
|
||||
cmp::min,
|
||||
fs::{File, FileTimes},
|
||||
io::{BufWriter, Write},
|
||||
os::macos::fs::FileTimesExt,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
mpsc::{Receiver, RecvTimeoutError},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
const RECV_RETRY_TIMES: usize = 3;
|
||||
|
||||
const DOWNLOAD_EXTENSION: &str = "rddownload";
|
||||
const RECEIVE_WAIT_TIMEOUT: Duration = Duration::from_millis(5_000);
|
||||
|
||||
// https://stackoverflow.com/a/15112784/1926020
|
||||
// "1984-01-24 08:00:00 +0000"
|
||||
const TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED: u64 = 443779200;
|
||||
const ATTR_PROGRESS_FRACTION_COMPLETED: &str = "com.apple.progress.fractionCompleted";
|
||||
|
||||
pub struct FileContentsResponse {
|
||||
pub conn_id: i32,
|
||||
pub msg_flags: i32,
|
||||
pub stream_id: i32,
|
||||
pub requested_data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PasteTaskProgress {
|
||||
// Use list index to identify the file
|
||||
// `list_index` is also used as the stream id
|
||||
list_index: i32,
|
||||
offset: u64,
|
||||
total_size: u64,
|
||||
current_size: u64,
|
||||
last_sent_time: Instant,
|
||||
download_file_index: i32,
|
||||
download_file_size: u64,
|
||||
download_file_path: String,
|
||||
download_file_current_size: u64,
|
||||
file_handle: Option<BufWriter<File>>,
|
||||
error: Option<CliprdrError>,
|
||||
is_canceled: bool,
|
||||
}
|
||||
|
||||
struct PasteTaskHandle {
|
||||
progress: PasteTaskProgress,
|
||||
target_dir: PathBuf,
|
||||
files: Vec<FileDescription>,
|
||||
}
|
||||
|
||||
pub struct PasteTask {
|
||||
exit: Arc<Mutex<bool>>,
|
||||
handle: Arc<Mutex<Option<PasteTaskHandle>>>,
|
||||
handle_worker: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Drop for PasteTask {
|
||||
fn drop(&mut self) {
|
||||
*self.exit.lock().unwrap() = true;
|
||||
if let Some(handle_worker) = self.handle_worker.take() {
|
||||
handle_worker.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PasteTask {
|
||||
const INVALID_FILE_INDEX: i32 = -1;
|
||||
|
||||
pub fn new(rx_file_contents: Receiver<FileContentsResponse>) -> Self {
|
||||
let exit = Arc::new(Mutex::new(false));
|
||||
let handle = Arc::new(Mutex::new(None));
|
||||
let handle_worker =
|
||||
Self::init_worker_thread(exit.clone(), handle.clone(), rx_file_contents);
|
||||
Self {
|
||||
handle,
|
||||
exit,
|
||||
handle_worker: Some(handle_worker),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, target_dir: PathBuf, files: Vec<FileDescription>) {
|
||||
let mut task_lock = self.handle.lock().unwrap();
|
||||
if task_lock
|
||||
.as_ref()
|
||||
.map(|x| !x.is_finished())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
log::error!("Previous paste task is not finished, ignore new request.");
|
||||
return;
|
||||
}
|
||||
let total_size = files.iter().map(|f| f.size).sum();
|
||||
let mut task_handle = PasteTaskHandle {
|
||||
progress: PasteTaskProgress {
|
||||
list_index: -1,
|
||||
offset: 0,
|
||||
total_size,
|
||||
current_size: 0,
|
||||
last_sent_time: Instant::now(),
|
||||
download_file_index: Self::INVALID_FILE_INDEX,
|
||||
download_file_size: 0,
|
||||
download_file_path: "".to_owned(),
|
||||
download_file_current_size: 0,
|
||||
file_handle: None,
|
||||
error: None,
|
||||
is_canceled: false,
|
||||
},
|
||||
target_dir,
|
||||
files,
|
||||
};
|
||||
task_handle.update_next(0).ok();
|
||||
if task_handle.is_finished() {
|
||||
task_handle.on_finished();
|
||||
} else {
|
||||
if let Err(e) = task_handle.send_file_contents_request() {
|
||||
log::error!("Failed to send file contents request, error: {}", &e);
|
||||
task_handle.on_error(e);
|
||||
}
|
||||
}
|
||||
*task_lock = Some(task_handle);
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
let mut task_handle = self.handle.lock().unwrap();
|
||||
if let Some(task_handle) = task_handle.as_mut() {
|
||||
task_handle.progress.is_canceled = true;
|
||||
task_handle.on_cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
fn init_worker_thread(
|
||||
exit: Arc<Mutex<bool>>,
|
||||
handle: Arc<Mutex<Option<PasteTaskHandle>>>,
|
||||
rx_file_contents: Receiver<FileContentsResponse>,
|
||||
) -> thread::JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
let mut retry_count = 0;
|
||||
loop {
|
||||
if *exit.lock().unwrap() {
|
||||
break;
|
||||
}
|
||||
|
||||
match rx_file_contents.recv_timeout(Duration::from_millis(300)) {
|
||||
Ok(file_contents) => {
|
||||
let mut task_lock = handle.lock().unwrap();
|
||||
let Some(task_handle) = task_lock.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
if task_handle.is_finished() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if file_contents.stream_id != task_handle.progress.list_index {
|
||||
// ignore invalid stream id
|
||||
continue;
|
||||
} else if file_contents.msg_flags != 0x01 {
|
||||
retry_count += 1;
|
||||
if retry_count > RECV_RETRY_TIMES {
|
||||
task_handle.progress.error = Some(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"Failed to read file contents, stream id: {}, msg_flags: {}",
|
||||
file_contents.stream_id,
|
||||
file_contents.msg_flags
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let resp_list_index = file_contents.stream_id;
|
||||
let Some(file) = &task_handle.files.get(resp_list_index as usize)
|
||||
else {
|
||||
// unreachable
|
||||
// Because `task_handle.progress.list_index >= task_handle.files.len()` should always be false
|
||||
log::warn!(
|
||||
"Invalid response list index: {}, file length: {}",
|
||||
resp_list_index,
|
||||
task_handle.files.len()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if file.conn_id != file_contents.conn_id {
|
||||
// unreachable
|
||||
// We still add log here to make sure we can see the error message when it happens.
|
||||
log::error!(
|
||||
"Invalid response conn id: {}, expected: {}",
|
||||
file_contents.conn_id,
|
||||
file.conn_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = task_handle.handle_file_contents_response(file_contents)
|
||||
{
|
||||
log::error!("Failed to handle file contents response: {}", &e);
|
||||
task_handle.on_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if !task_handle.is_finished() {
|
||||
if let Err(e) = task_handle.send_file_contents_request() {
|
||||
log::error!("Failed to send file contents request: {}", &e);
|
||||
task_handle.on_error(e);
|
||||
}
|
||||
} else {
|
||||
retry_count = 0;
|
||||
task_handle.on_finished();
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
let mut task_lock = handle.lock().unwrap();
|
||||
if let Some(task_handle) = task_lock.as_mut() {
|
||||
if task_handle.check_receive_timemout() {
|
||||
retry_count = 0;
|
||||
task_handle.on_finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.handle
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|handle| handle.is_finished())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn progress_percent(&self) -> Option<ProgressPercent> {
|
||||
self.handle
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|handle| handle.progress_percent())
|
||||
}
|
||||
}
|
||||
|
||||
impl PasteTaskHandle {
|
||||
fn update_next(&mut self, size: u64) -> Result<(), CliprdrError> {
|
||||
if self.is_finished() {
|
||||
return Ok(());
|
||||
}
|
||||
self.progress.current_size += size;
|
||||
|
||||
let is_start = self.progress.list_index == -1;
|
||||
if is_start || (self.progress.offset + size) >= self.progress.download_file_size {
|
||||
if !is_start {
|
||||
self.on_done();
|
||||
}
|
||||
for i in (self.progress.list_index + 1)..self.files.len() as i32 {
|
||||
let Some(file_desc) = self.files.get(i as usize) else {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!("Invalid file index: {}", i),
|
||||
});
|
||||
};
|
||||
match file_desc.kind {
|
||||
FileType::File => {
|
||||
if file_desc.size == 0 {
|
||||
if let Some(new_file_path) =
|
||||
Self::get_new_filename(&self.target_dir, file_desc)
|
||||
{
|
||||
if let Ok(f) = std::fs::File::create(&new_file_path) {
|
||||
f.set_len(0).ok();
|
||||
Self::set_file_metadata(&f, file_desc);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
self.progress.list_index = i;
|
||||
self.progress.offset = 0;
|
||||
self.open_new_writer()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
FileType::Directory => {
|
||||
let path = self.target_dir.join(&file_desc.name);
|
||||
if !path.exists() {
|
||||
std::fs::create_dir_all(path).ok();
|
||||
}
|
||||
}
|
||||
FileType::Symlink => {
|
||||
// to-do: handle symlink
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.progress.offset += size;
|
||||
self.progress.download_file_current_size += size;
|
||||
self.update_progress_completed(None);
|
||||
}
|
||||
if self.progress.file_handle.is_none() {
|
||||
self.progress.list_index = self.files.len() as i32;
|
||||
self.progress.offset = 0;
|
||||
self.progress.download_file_size = 0;
|
||||
self.progress.download_file_current_size = 0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_progress_completed(&self) {
|
||||
if let Some(file) = self.progress.file_handle.as_ref() {
|
||||
let creation_time =
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED);
|
||||
file.get_ref()
|
||||
.set_times(FileTimes::new().set_created(creation_time))
|
||||
.ok();
|
||||
xattr::set(
|
||||
&self.progress.download_file_path,
|
||||
ATTR_PROGRESS_FRACTION_COMPLETED,
|
||||
"0.0".as_bytes(),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_progress_completed(&mut self, fraction_completed: Option<f64>) {
|
||||
let fraction_completed = fraction_completed.unwrap_or_else(|| {
|
||||
let current_size = self.progress.download_file_current_size as f64;
|
||||
let total_size = self.progress.download_file_size as f64;
|
||||
if total_size > 0.0 {
|
||||
current_size / total_size
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
});
|
||||
xattr::set(
|
||||
&self.progress.download_file_path,
|
||||
ATTR_PROGRESS_FRACTION_COMPLETED,
|
||||
&fraction_completed.to_string().as_bytes(),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn remove_progress_completed(path: &str) {
|
||||
if !path.is_empty() {
|
||||
xattr::remove(path, ATTR_PROGRESS_FRACTION_COMPLETED).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn open_new_writer(&mut self) -> Result<(), CliprdrError> {
|
||||
let Some(file) = &self.files.get(self.progress.list_index as usize) else {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"Invalid file index: {}, file count: {}",
|
||||
self.progress.list_index,
|
||||
self.files.len()
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
let original_file_path = self
|
||||
.target_dir
|
||||
.join(&file.name)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let Some(download_file_path) = Self::get_first_filename(
|
||||
format!("{}.{}", original_file_path, DOWNLOAD_EXTENSION),
|
||||
file.kind,
|
||||
) else {
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: format!("Failed to get download file path: {}", original_file_path),
|
||||
});
|
||||
};
|
||||
let Some(download_path_parent) = Path::new(&download_file_path).parent() else {
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: format!(
|
||||
"Failed to get parent of the download file path: {}",
|
||||
original_file_path
|
||||
),
|
||||
});
|
||||
};
|
||||
if !download_path_parent.exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(download_path_parent) {
|
||||
return Err(CliprdrError::FileError {
|
||||
path: download_path_parent.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
match std::fs::File::create(&download_file_path) {
|
||||
Ok(handle) => {
|
||||
let writer = BufWriter::with_capacity(BLOCK_SIZE as usize * 2, handle);
|
||||
self.progress.download_file_index = self.progress.list_index;
|
||||
self.progress.download_file_size = file.size;
|
||||
self.progress.download_file_path = download_file_path;
|
||||
self.progress.download_file_current_size = 0;
|
||||
self.progress.file_handle = Some(writer);
|
||||
self.start_progress_completed();
|
||||
}
|
||||
Err(e) => {
|
||||
self.progress.error = Some(CliprdrError::FileError {
|
||||
path: download_file_path,
|
||||
err: e,
|
||||
});
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_first_filename(path: String, r#type: FileType) -> Option<String> {
|
||||
let p = Path::new(&path);
|
||||
if !p.exists() {
|
||||
return Some(path);
|
||||
} else {
|
||||
for i in 1..9999999 {
|
||||
let new_path = match r#type {
|
||||
FileType::File => {
|
||||
if let Some(ext) = p.extension() {
|
||||
let new_name = format!(
|
||||
"{}-{}.{}",
|
||||
p.file_stem().unwrap_or_default().to_string_lossy(),
|
||||
i,
|
||||
ext.to_string_lossy()
|
||||
);
|
||||
p.with_file_name(new_name).to_string_lossy().to_string()
|
||||
} else {
|
||||
format!("{} ({})", path, i)
|
||||
}
|
||||
}
|
||||
FileType::Directory => format!("{} ({})", path, i),
|
||||
FileType::Symlink => {
|
||||
// to-do: handle symlink
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !Path::new(&new_path).exists() {
|
||||
return Some(new_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
// unreachable
|
||||
None
|
||||
}
|
||||
|
||||
fn progress_percent(&self) -> ProgressPercent {
|
||||
let percent = self.progress.current_size as f64 / self.progress.total_size as f64;
|
||||
ProgressPercent {
|
||||
percent,
|
||||
is_canceled: self.progress.is_canceled,
|
||||
is_failed: self.progress.error.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_finished(&self) -> bool {
|
||||
self.progress.is_canceled
|
||||
|| self.progress.error.is_some()
|
||||
|| self.progress.list_index >= self.files.len() as i32
|
||||
}
|
||||
|
||||
fn check_receive_timemout(&mut self) -> bool {
|
||||
if !self.is_finished() {
|
||||
if self.progress.last_sent_time.elapsed() > RECEIVE_WAIT_TIMEOUT {
|
||||
self.progress.error = Some(CliprdrError::InvalidRequest {
|
||||
description: "Failed to read file contents".to_string(),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn on_finished(&mut self) {
|
||||
if self.progress.error.is_some() {
|
||||
self.on_cancelled();
|
||||
} else {
|
||||
self.on_done();
|
||||
}
|
||||
if self.progress.current_size != self.progress.total_size {
|
||||
self.progress.error = Some(CliprdrError::InvalidRequest {
|
||||
description: "Failed to download all files".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn on_error(&mut self, error: CliprdrError) {
|
||||
self.progress.error = Some(error);
|
||||
self.on_cancelled();
|
||||
}
|
||||
|
||||
fn on_cancelled(&mut self) {
|
||||
self.progress.file_handle = None;
|
||||
std::fs::remove_file(&self.progress.download_file_path).ok();
|
||||
}
|
||||
|
||||
fn on_done(&mut self) {
|
||||
self.update_progress_completed(Some(1.0));
|
||||
Self::remove_progress_completed(&self.progress.download_file_path);
|
||||
|
||||
let Some(file) = self.progress.file_handle.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if self.progress.download_file_index == PasteTask::INVALID_FILE_INDEX {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = file.flush() {
|
||||
log::error!("Failed to flush file: {:?}", e);
|
||||
}
|
||||
self.progress.file_handle = None;
|
||||
|
||||
let Some(file_desc) = self.files.get(self.progress.download_file_index as usize) else {
|
||||
// unreachable
|
||||
log::error!(
|
||||
"Failed to get file description: {}",
|
||||
self.progress.download_file_index
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(rename_to_path) = Self::get_new_filename(&self.target_dir, file_desc) else {
|
||||
return;
|
||||
};
|
||||
match std::fs::rename(&self.progress.download_file_path, &rename_to_path) {
|
||||
Ok(_) => Self::set_file_metadata2(&rename_to_path, file_desc),
|
||||
Err(e) => {
|
||||
log::error!("Failed to rename file: {:?}", e);
|
||||
}
|
||||
}
|
||||
self.progress.download_file_path = "".to_owned();
|
||||
self.progress.download_file_index = PasteTask::INVALID_FILE_INDEX;
|
||||
}
|
||||
|
||||
fn get_new_filename(target_dir: &PathBuf, file_desc: &FileDescription) -> Option<String> {
|
||||
let mut rename_to_path = target_dir
|
||||
.join(&file_desc.name)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
if Path::new(&rename_to_path).exists() {
|
||||
let Some(new_path) = Self::get_first_filename(rename_to_path.clone(), file_desc.kind)
|
||||
else {
|
||||
log::error!("Failed to get new file name: {}", &rename_to_path);
|
||||
return None;
|
||||
};
|
||||
rename_to_path = new_path;
|
||||
}
|
||||
Some(rename_to_path)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_file_metadata(f: &File, file_desc: &FileDescription) {
|
||||
let times = FileTimes::new()
|
||||
.set_accessed(file_desc.atime)
|
||||
.set_modified(file_desc.last_modified)
|
||||
.set_created(file_desc.creation_time);
|
||||
f.set_times(times).ok();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_file_metadata2(path: &str, file_desc: &FileDescription) {
|
||||
let times = FileTimes::new()
|
||||
.set_accessed(file_desc.atime)
|
||||
.set_modified(file_desc.last_modified)
|
||||
.set_created(file_desc.creation_time);
|
||||
File::options()
|
||||
.write(true)
|
||||
.open(path)
|
||||
.map(|f| f.set_times(times))
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn send_file_contents_request(&mut self) -> Result<(), CliprdrError> {
|
||||
if self.is_finished() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let stream_id = self.progress.list_index;
|
||||
let list_index = self.progress.list_index;
|
||||
let Some(file) = &self.files.get(list_index as usize) else {
|
||||
// unreachable
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!("Invalid file index: {}", list_index),
|
||||
});
|
||||
};
|
||||
let cb_requested = min(BLOCK_SIZE as u64, file.size - self.progress.offset);
|
||||
let conn_id = file.conn_id;
|
||||
|
||||
let (n_position_high, n_position_low) = (
|
||||
(self.progress.offset >> 32) as i32,
|
||||
(self.progress.offset & (u32::MAX as u64)) as i32,
|
||||
);
|
||||
let request = ClipboardFile::FileContentsRequest {
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags: 2,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested: cb_requested as _,
|
||||
have_clip_data_id: false,
|
||||
clip_data_id: 0,
|
||||
};
|
||||
allow_err!(send_data(conn_id, request));
|
||||
self.progress.last_sent_time = Instant::now();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_file_contents_response(
|
||||
&mut self,
|
||||
file_contents: FileContentsResponse,
|
||||
) -> Result<(), CliprdrError> {
|
||||
if let Some(file) = self.progress.file_handle.as_mut() {
|
||||
let data = file_contents.requested_data.as_slice();
|
||||
let mut write_len = 0;
|
||||
while write_len < data.len() {
|
||||
match file.write(&data[write_len..]) {
|
||||
Ok(len) => {
|
||||
write_len += len;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(CliprdrError::FileError {
|
||||
path: self.progress.download_file_path.clone(),
|
||||
err: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
self.update_next(write_len as _)?;
|
||||
} else {
|
||||
return Err(CliprdrError::FileError {
|
||||
path: self.progress.download_file_path.clone(),
|
||||
err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle is not opened"),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
443
libs/clipboard/src/platform/unix/macos/pasteboard_context.rs
Normal file
443
libs/clipboard/src/platform/unix/macos/pasteboard_context.rs
Normal file
@ -0,0 +1,443 @@
|
||||
use super::{
|
||||
item_data_provider::create_pasteboard_file_url_provider,
|
||||
paste_observer::PasteObserver,
|
||||
paste_task::{FileContentsResponse, PasteTask},
|
||||
};
|
||||
use crate::{
|
||||
platform::unix::{
|
||||
filetype::FileDescription, FILECONTENTS_FORMAT_NAME, FILEDESCRIPTORW_FORMAT_NAME,
|
||||
},
|
||||
send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ProgressPercent,
|
||||
};
|
||||
use hbb_common::{allow_err, bail, log, ResultType};
|
||||
use objc2::{msg_send_id, rc::Id, runtime::ProtocolObject, ClassType};
|
||||
use objc2_app_kit::{NSPasteboard, NSPasteboardTypeFileURL};
|
||||
use objc2_foundation::{NSArray, NSString};
|
||||
use std::{
|
||||
io,
|
||||
path::Path,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, RecvTimeoutError, Sender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PASTE_OBSERVER_INFO: Arc<Mutex<Option<PasteObserverInfo>>> = Default::default();
|
||||
}
|
||||
|
||||
pub const TEMP_FILE_PREFIX: &str = ".rustdesk_";
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub(super) struct PasteObserverInfo {
|
||||
pub file_descriptor_id: i32,
|
||||
pub conn_id: i32,
|
||||
pub source_path: String,
|
||||
pub target_path: String,
|
||||
}
|
||||
|
||||
impl PasteObserverInfo {
|
||||
fn exit_msg() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextInfo {
|
||||
tx: Sender<io::Result<PasteObserverInfo>>,
|
||||
handle: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
pub struct PasteboardContext {
|
||||
pasteboard: Id<NSPasteboard>,
|
||||
observer: Arc<Mutex<PasteObserver>>,
|
||||
tx_handle: Option<ContextInfo>,
|
||||
tx_remove_file: Option<Sender<String>>,
|
||||
remove_file_handle: Option<thread::JoinHandle<()>>,
|
||||
tx_paste_task: Sender<FileContentsResponse>,
|
||||
paste_task: Arc<Mutex<PasteTask>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for PasteboardContext {}
|
||||
unsafe impl Sync for PasteboardContext {}
|
||||
|
||||
impl Drop for PasteboardContext {
|
||||
fn drop(&mut self) {
|
||||
self.observer.lock().unwrap().stop();
|
||||
if let Some(tx_handle) = self.tx_handle.take() {
|
||||
if tx_handle.tx.send(Ok(PasteObserverInfo::exit_msg())).is_ok() {
|
||||
tx_handle.handle.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CliprdrServiceContext for PasteboardContext {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError> {
|
||||
Ok(self.empty_clipboard_(conn_id))
|
||||
}
|
||||
|
||||
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
self.server_clip_file_(conn_id, msg)
|
||||
}
|
||||
|
||||
fn get_progress_percent(&self) -> Option<ProgressPercent> {
|
||||
self.paste_task.lock().unwrap().progress_percent()
|
||||
}
|
||||
|
||||
fn cancel(&mut self) {
|
||||
self.paste_task.lock().unwrap().cancel();
|
||||
}
|
||||
}
|
||||
|
||||
impl PasteboardContext {
|
||||
fn init(&mut self) {
|
||||
let (tx_remove_file, rx_remove_file) = channel();
|
||||
let handle_remove_file = Self::init_thread_remove_file(rx_remove_file);
|
||||
self.tx_remove_file = Some(tx_remove_file.clone());
|
||||
self.remove_file_handle = Some(handle_remove_file);
|
||||
|
||||
let (tx, rx) = channel();
|
||||
let observer: Arc<Mutex<PasteObserver>> = self.observer.clone();
|
||||
let handle = Self::init_thread_observer(tx_remove_file, rx, observer);
|
||||
self.tx_handle = Some(ContextInfo { tx, handle });
|
||||
}
|
||||
|
||||
fn init_thread_observer(
|
||||
tx_remove_file: Sender<String>,
|
||||
rx: Receiver<io::Result<PasteObserverInfo>>,
|
||||
observer: Arc<Mutex<PasteObserver>>,
|
||||
) -> thread::JoinHandle<()> {
|
||||
let exit_msg = PasteObserverInfo::exit_msg();
|
||||
thread::spawn(move || loop {
|
||||
match rx.recv() {
|
||||
Ok(Ok(task_info)) => {
|
||||
if task_info == exit_msg {
|
||||
log::debug!("pasteboard item data provider: exit");
|
||||
break;
|
||||
}
|
||||
tx_remove_file.send(task_info.source_path.clone()).ok();
|
||||
observer.lock().unwrap().start(task_info);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::error!("pasteboard item data provider, inner error: {e}");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("pasteboard item data provider, error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn init_thread_remove_file(rx: Receiver<String>) -> thread::JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
let mut cur_file: Option<String> = None;
|
||||
loop {
|
||||
match rx.recv_timeout(Duration::from_secs(30)) {
|
||||
Ok(path) => {
|
||||
if let Some(file) = cur_file.take() {
|
||||
if !file.is_empty() {
|
||||
std::fs::remove_file(&file).ok();
|
||||
}
|
||||
}
|
||||
if !path.is_empty() {
|
||||
cur_file = Some(path);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(file) = cur_file.take() {
|
||||
if !file.is_empty() {
|
||||
std::fs::remove_file(&file).ok();
|
||||
}
|
||||
}
|
||||
if e == RecvTimeoutError::Disconnected {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Just removing the file can also make paste option in the context menu disappear.
|
||||
fn empty_clipboard_(&mut self, _conn_id: i32) -> bool {
|
||||
self.tx_remove_file
|
||||
.as_ref()
|
||||
.map(|tx| tx.send("".to_string()).ok());
|
||||
true
|
||||
}
|
||||
|
||||
fn temp_files_count() -> usize {
|
||||
let mut count = 0;
|
||||
if let Ok(entries) = std::fs::read_dir("/tmp") {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if let Some(file_name_str) = file_name.to_str() {
|
||||
if file_name_str.starts_with(TEMP_FILE_PREFIX) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn server_clip_file_(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
match msg {
|
||||
ClipboardFile::FormatList { format_list } => {
|
||||
let temp_files = Self::temp_files_count();
|
||||
if temp_files >= 3 {
|
||||
// The temp files should be 0 or 1 in normal case.
|
||||
// We should not continue to paste files if there are more than 3 temp files.
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: format!(
|
||||
"too many temp files, current: {}, limit: {}",
|
||||
temp_files, 3
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let task_lock = self.paste_task.lock().unwrap();
|
||||
if !task_lock.is_finished() {
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: "previous file paste task is not finished".to_string(),
|
||||
});
|
||||
}
|
||||
self.handle_format_list(conn_id, format_list)?;
|
||||
}
|
||||
ClipboardFile::FormatDataResponse {
|
||||
msg_flags,
|
||||
format_data,
|
||||
} => {
|
||||
self.handle_format_data_response(conn_id, msg_flags, format_data)?;
|
||||
}
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
} => {
|
||||
self.handle_file_contents_response(conn_id, msg_flags, stream_id, requested_data)?;
|
||||
}
|
||||
ClipboardFile::TryEmpty => self.handle_try_empty(conn_id),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_format_list(
|
||||
&self,
|
||||
conn_id: i32,
|
||||
format_list: Vec<(i32, String)>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
if let Some(tx_handle) = self.tx_handle.as_ref() {
|
||||
if !format_list
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)
|
||||
.is_some()
|
||||
{
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: "no file contents format found".to_string(),
|
||||
});
|
||||
};
|
||||
let Some(file_descriptor_id) = format_list
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)
|
||||
else {
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: "no file descriptor format found".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let tx = tx_handle.tx.clone();
|
||||
let provider = create_pasteboard_file_url_provider(
|
||||
PasteObserverInfo {
|
||||
file_descriptor_id,
|
||||
conn_id,
|
||||
source_path: "".to_string(),
|
||||
target_path: "".to_string(),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
unsafe {
|
||||
let types = NSArray::from_vec(vec![NSString::from_str(
|
||||
&NSPasteboardTypeFileURL.to_string(),
|
||||
)]);
|
||||
let item = objc2_app_kit::NSPasteboardItem::new();
|
||||
item.setDataProvider_forTypes(&ProtocolObject::from_id(provider), &types);
|
||||
self.pasteboard.clearContents();
|
||||
if !self
|
||||
.pasteboard
|
||||
.writeObjects(&Id::cast(NSArray::from_vec(vec![item])))
|
||||
{
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: "failed to write objects".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: "pasteboard context is not inited".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_format_data_response(
|
||||
&self,
|
||||
conn_id: i32,
|
||||
msg_flags: i32,
|
||||
format_data: Vec<u8>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
log::debug!("handle format data response, msg_flags: {msg_flags}");
|
||||
if msg_flags != 0x1 {
|
||||
// return failure message?
|
||||
}
|
||||
|
||||
let mut task_lock = self.paste_task.lock().unwrap();
|
||||
let target_dir = PASTE_OBSERVER_INFO
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|task| task.target_path.clone());
|
||||
// unreachable in normal case
|
||||
let Some(target_dir) = target_dir.as_ref().map(|d| Path::new(d).parent()).flatten() else {
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: "failed to get parent path".to_string(),
|
||||
});
|
||||
};
|
||||
// unreachable in normal case
|
||||
if !target_dir.exists() {
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: "target path does not exist".to_string(),
|
||||
});
|
||||
}
|
||||
let target_dir = target_dir.to_owned();
|
||||
match FileDescription::parse_file_descriptors(format_data, conn_id) {
|
||||
Ok(files) => {
|
||||
task_lock.start(target_dir, files);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
PASTE_OBSERVER_INFO
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(PasteObserverInfo::default());
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_contents_response(
|
||||
&self,
|
||||
conn_id: i32,
|
||||
msg_flags: i32,
|
||||
stream_id: i32,
|
||||
requested_data: Vec<u8>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
log::debug!("handle file contents response");
|
||||
self.tx_paste_task
|
||||
.send(FileContentsResponse {
|
||||
conn_id,
|
||||
msg_flags,
|
||||
stream_id,
|
||||
requested_data,
|
||||
})
|
||||
.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_try_empty(&mut self, conn_id: i32) {
|
||||
log::debug!("empty_clipboard called");
|
||||
let ret = self.empty_clipboard_(conn_id);
|
||||
log::debug!(
|
||||
"empty_clipboard called, conn_id {}, return {}",
|
||||
conn_id,
|
||||
ret
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_paste_result(task_info: &PasteObserverInfo) {
|
||||
log::info!(
|
||||
"file {} is pasted to {}",
|
||||
&task_info.source_path,
|
||||
&task_info.target_path
|
||||
);
|
||||
if Path::new(&task_info.target_path).parent().is_none() {
|
||||
log::error!(
|
||||
"failed to get parent path of {}, no need to perform pasting",
|
||||
&task_info.target_path
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
PASTE_OBSERVER_INFO
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(task_info.clone());
|
||||
// to-do: add a timeout to clear data in `PASTE_OBSERVER_INFO`.
|
||||
std::fs::remove_file(&task_info.source_path).ok();
|
||||
std::fs::remove_file(&task_info.target_path).ok();
|
||||
let data = ClipboardFile::FormatDataRequest {
|
||||
requested_format_id: task_info.file_descriptor_id,
|
||||
};
|
||||
allow_err!(send_data(task_info.conn_id as _, data));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn create_pasteboard_context() -> ResultType<Box<PasteboardContext>> {
|
||||
let pasteboard: Option<Id<NSPasteboard>> =
|
||||
unsafe { msg_send_id![NSPasteboard::class(), generalPasteboard] };
|
||||
let Some(pasteboard) = pasteboard else {
|
||||
bail!("failed to get general pasteboard");
|
||||
};
|
||||
let mut observer = PasteObserver::new();
|
||||
observer.init(handle_paste_result)?;
|
||||
let (tx, rx) = channel();
|
||||
let mut context = Box::new(PasteboardContext {
|
||||
pasteboard,
|
||||
observer: Arc::new(Mutex::new(observer)),
|
||||
tx_handle: None,
|
||||
tx_remove_file: None,
|
||||
remove_file_handle: None,
|
||||
tx_paste_task: tx,
|
||||
paste_task: Arc::new(Mutex::new(PasteTask::new(rx))),
|
||||
});
|
||||
context.init();
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_temp_files_count() {
|
||||
let mut c = super::PasteboardContext::temp_files_count();
|
||||
|
||||
for _ in 0..10 {
|
||||
let path = format!(
|
||||
"/tmp/{}{}",
|
||||
super::TEMP_FILE_PREFIX,
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
);
|
||||
if std::fs::File::create(&path).is_ok() {
|
||||
c += 1;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(c, super::PasteboardContext::temp_files_count());
|
||||
}
|
||||
}
|
@ -2,9 +2,13 @@ use dashmap::DashMap;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
mod filetype;
|
||||
pub use filetype::{FileDescription, FileType};
|
||||
/// use FUSE for file pasting on these platforms
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod fuse;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
pub mod local_file;
|
||||
pub mod serv_files;
|
||||
|
||||
|
@ -6,8 +6,9 @@
|
||||
#![allow(deref_nullptr)]
|
||||
|
||||
use crate::{
|
||||
send_data, send_data_exclude, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
||||
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
||||
send_data, send_data_exclude, ClipboardFile, CliprdrError, CliprdrServiceContext,
|
||||
ProgressPercent, ResultType, ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG,
|
||||
ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
||||
};
|
||||
use hbb_common::{allow_err, log};
|
||||
use std::{
|
||||
@ -602,6 +603,12 @@ impl CliprdrServiceContext for CliprdrClientContext {
|
||||
let ret = server_clip_file(self, conn_id, msg);
|
||||
ret_to_result(ret)
|
||||
}
|
||||
|
||||
fn get_progress_percent(&self) -> Option<ProgressPercent> {
|
||||
None
|
||||
}
|
||||
|
||||
fn cancel(&mut self) {}
|
||||
}
|
||||
|
||||
fn ret_to_result(ret: u32) -> Result<(), CliprdrError> {
|
||||
@ -745,7 +752,11 @@ pub fn server_clip_file(
|
||||
ClipboardFile::TryEmpty => {
|
||||
log::debug!("empty_clipboard called");
|
||||
let ret = empty_clipboard(context, conn_id);
|
||||
log::debug!("empty_clipboard called, conn_id {}, return {}", conn_id, ret);
|
||||
log::debug!(
|
||||
"empty_clipboard called, conn_id {}, return {}",
|
||||
conn_id,
|
||||
ret
|
||||
);
|
||||
}
|
||||
}
|
||||
ret
|
||||
|
@ -848,6 +848,10 @@ impl ClientClipboardHandler {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) {
|
||||
if !urls.is_empty() {
|
||||
#[cfg(target_os = "macos")]
|
||||
if crate::clipboard::is_file_url_set_by_rustdesk(&urls) {
|
||||
return;
|
||||
}
|
||||
if self.is_file_required() {
|
||||
match clipboard::platform::unix::serv_files::sync_files(&urls) {
|
||||
Ok(()) => {
|
||||
|
@ -12,7 +12,10 @@ use crate::{
|
||||
};
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "macos", feature = "unix-file-copy-paste")
|
||||
))]
|
||||
use clipboard::ContextSend;
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
@ -1956,9 +1959,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
async fn handle_cliprdr_msg(
|
||||
&self,
|
||||
&mut self,
|
||||
clip: hbb_common::message_proto::Cliprdr,
|
||||
_peer: &mut Stream,
|
||||
peer: &mut Stream,
|
||||
) {
|
||||
log::debug!("handling cliprdr msg from server peer");
|
||||
#[cfg(feature = "flutter")]
|
||||
@ -1982,7 +1985,10 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
"Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}",
|
||||
stop, is_stopping_allowed, file_transfer_enabled);
|
||||
if !stop {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "macos", feature = "unix-file-copy-paste")
|
||||
))]
|
||||
if let Err(e) = ContextSend::make_sure_enabled() {
|
||||
log::error!("failed to restart clipboard context: {}", e);
|
||||
};
|
||||
@ -1996,12 +2002,36 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if crate::is_support_file_copy_paste_num(self.handler.lc.read().unwrap().version) {
|
||||
if let Some(msg) = unix_file_clip::serve_clip_messages(
|
||||
let mut out_msg = None;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if clipboard::platform::unix::macos::should_handle_msg(&clip) {
|
||||
if let Err(e) = ContextSend::proc(|context| -> ResultType<()> {
|
||||
context
|
||||
.server_clip_file(self.client_conn_id, clip)
|
||||
.map_err(|e| e.into())
|
||||
}) {
|
||||
log::error!("failed to handle cliprdr msg: {}", e);
|
||||
}
|
||||
} else {
|
||||
out_msg = unix_file_clip::serve_clip_messages(
|
||||
ClipboardSide::Client,
|
||||
clip,
|
||||
self.client_conn_id,
|
||||
) {
|
||||
allow_err!(_peer.send(&msg).await);
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
out_msg = unix_file_clip::serve_clip_messages(
|
||||
ClipboardSide::Client,
|
||||
clip,
|
||||
self.client_conn_id,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(msg) = out_msg {
|
||||
allow_err!(peer.send(&msg).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,24 @@ pub fn check_clipboard(
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
|
||||
pub fn is_file_url_set_by_rustdesk(url: &Vec<String>) -> bool {
|
||||
if url.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
url.iter()
|
||||
.next()
|
||||
.map(|s| {
|
||||
for prefix in &["file:///tmp/.rustdesk_", "//tmp/.rustdesk_"] {
|
||||
if s.starts_with(prefix) {
|
||||
return s[prefix.len()..].parse::<uuid::Uuid>().is_ok();
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn check_clipboard_files(
|
||||
ctx: &mut Option<ClipboardContext>,
|
||||
@ -110,7 +128,6 @@ pub fn update_clipboard_files(files: Vec<String>, side: ClipboardSide) {
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
|
||||
#[cfg(target_os = "linux")]
|
||||
std::thread::spawn(move || {
|
||||
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
||||
if ctx.is_none() {
|
||||
@ -125,11 +142,24 @@ pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
|
||||
}
|
||||
}
|
||||
if let Some(mut ctx) = ctx.as_mut() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use clipboard::platform::unix;
|
||||
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
// No need to make sure the context is enabled.
|
||||
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
||||
context.empty_clipboard(_conn_id).ok();
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -351,17 +381,26 @@ impl ClipboardContext {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
fn try_empty_clipboard_files(&mut self, side: ClipboardSide) {
|
||||
let _lock = ARBOARD_MTX.lock().unwrap();
|
||||
if let Ok(data) = self.get_formats(&[ClipboardFormat::FileUrl]) {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
|
||||
fn get_file_urls_set_by_rustdesk(
|
||||
data: Vec<ClipboardData>,
|
||||
_side: ClipboardSide,
|
||||
) -> Vec<String> {
|
||||
for item in data.into_iter() {
|
||||
if let ClipboardData::FileUrl(urls) = item {
|
||||
if is_file_url_set_by_rustdesk(&urls) {
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
||||
fn get_file_urls_set_by_rustdesk(data: Vec<ClipboardData>, side: ClipboardSide) -> Vec<String> {
|
||||
let exclude_path =
|
||||
clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client);
|
||||
#[cfg(target_os = "macos")]
|
||||
let exclude_path: Arc<String> = Default::default();
|
||||
let urls = data
|
||||
.into_iter()
|
||||
data.into_iter()
|
||||
.filter_map(|c| match c {
|
||||
ClipboardData::FileUrl(urls) => Some(
|
||||
urls.into_iter()
|
||||
@ -371,7 +410,14 @@ impl ClipboardContext {
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
fn try_empty_clipboard_files(&mut self, side: ClipboardSide) {
|
||||
let _lock = ARBOARD_MTX.lock().unwrap();
|
||||
if let Ok(data) = self.get_formats(&[ClipboardFormat::FileUrl]) {
|
||||
let urls = Self::get_file_urls_set_by_rustdesk(data, side);
|
||||
if !urls.is_empty() {
|
||||
// FIXME:
|
||||
// The host-side clear file clipboard `let _ = self.inner.clear();`,
|
||||
|
@ -139,6 +139,10 @@ pub fn is_support_file_copy_paste_num(ver: i64) -> bool {
|
||||
ver >= hbb_common::get_version_number("1.3.8")
|
||||
}
|
||||
|
||||
pub fn is_support_file_paste_if_macos(ver: &str) -> bool {
|
||||
hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9")
|
||||
}
|
||||
|
||||
// is server process, with "--server" args
|
||||
#[inline]
|
||||
pub fn is_server() -> bool {
|
||||
|
@ -115,6 +115,10 @@ impl Handler {
|
||||
fn check_clipboard_file(&mut self) {
|
||||
if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Host, false) {
|
||||
if !urls.is_empty() {
|
||||
#[cfg(target_os = "macos")]
|
||||
if crate::clipboard::is_file_url_set_by_rustdesk(&urls) {
|
||||
return;
|
||||
}
|
||||
match clipboard::platform::unix::serv_files::sync_files(&urls) {
|
||||
Ok(()) => {
|
||||
// Use `send_data()` here to reuse `handle_file_clip()` in `connection.rs`.
|
||||
|
@ -1267,11 +1267,13 @@ impl Connection {
|
||||
let is_unix_and_peer_supported = crate::is_support_file_copy_paste(&self.lr.version);
|
||||
#[cfg(not(feature = "unix-file-copy-paste"))]
|
||||
let is_unix_and_peer_supported = false;
|
||||
// to-do: add file clipboard support for macos
|
||||
let is_both_macos = cfg!(target_os = "macos")
|
||||
&& self.lr.my_platform == whoami::Platform::MacOS.to_string();
|
||||
let has_file_clipboard =
|
||||
is_both_windows || (is_unix_and_peer_supported && !is_both_macos);
|
||||
let is_peer_support_paste_if_macos =
|
||||
crate::is_support_file_paste_if_macos(&self.lr.version);
|
||||
let has_file_clipboard = is_both_windows
|
||||
|| (is_unix_and_peer_supported
|
||||
&& (!is_both_macos || is_peer_support_paste_if_macos));
|
||||
platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard));
|
||||
}
|
||||
|
||||
@ -2195,11 +2197,38 @@ impl Connection {
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if crate::is_support_file_copy_paste(&self.lr.version) {
|
||||
if let Some(msg) = unix_file_clip::serve_clip_messages(
|
||||
let mut out_msg = None;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if clipboard::platform::unix::macos::should_handle_msg(&clip) {
|
||||
if let Err(e) = clipboard::ContextSend::make_sure_enabled() {
|
||||
log::error!("failed to restart clipboard context: {}", e);
|
||||
} else {
|
||||
let _ =
|
||||
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
||||
context
|
||||
.server_clip_file(self.inner.id(), clip)
|
||||
.map_err(|e| e.into())
|
||||
});
|
||||
}
|
||||
} else {
|
||||
out_msg = unix_file_clip::serve_clip_messages(
|
||||
ClipboardSide::Host,
|
||||
clip,
|
||||
self.inner.id(),
|
||||
) {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
out_msg = unix_file_clip::serve_clip_messages(
|
||||
ClipboardSide::Host,
|
||||
clip,
|
||||
self.inner.id(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(msg) = out_msg {
|
||||
self.send(msg).await;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user