Initial commit: RustDesk 1.4.7 macOS desktop port
- Filled empty libs/hbb_common/ submodule (cloned from rustdesk/hbb_common) - Patched Flutter 3.44 / Dart 3.12 compatibility: * flutter/lib/generated_bridge.dart: asTypedList with cast<>, DartPort=Int64 * flutter/lib/common.dart: DialogTheme->DialogThemeData, TabBarTheme->TabBarThemeData * flutter/pubspec.yaml: extended_text 14.0.0->15.0.2, google_fonts override 5.0.0 * flutter/macos/Runner/Configs/Release.xcconfig: EXCLUDED_ARCHS=x86_64 - Build verified: cargo check + cargo build --features flutter + cargo build --release --features flutter - Verified flutter build macos --debug and --release both produce working .app - Verified .dmg installer (27MB arm64) created via hdiutil - Build deps: Xcode 26.5, CocoaPods 1.16.2, Flutter 3.44, VCPKG arm64-osx
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
[package]
|
||||
name = "clipboard"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
unix-file-copy-paste = [
|
||||
"dep:x11rb",
|
||||
"dep:x11-clipboard",
|
||||
"dep:rand",
|
||||
"dep:fuser",
|
||||
"dep:libc",
|
||||
"dep:dashmap",
|
||||
"dep:percent-encoding",
|
||||
"dep:utf16string",
|
||||
"dep:once_cell",
|
||||
"dep:cacao"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
lazy_static = "1.4"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
hbb_common = { path = "../hbb_common" }
|
||||
parking_lot = {version = "0.12"}
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
|
||||
rand = {version = "0.8", optional = true}
|
||||
libc = {version = "0.2", optional = true}
|
||||
dashmap = {version ="5.5", optional = true}
|
||||
utf16string = {version = "0.2", optional = true}
|
||||
once_cell = {version = "1.18", optional = true}
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
percent-encoding = {version ="2.3", optional = true}
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
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"
|
||||
@@ -0,0 +1,161 @@
|
||||
# clipboard
|
||||
|
||||
Copy files and text through network.
|
||||
Main lowlevel logic from [FreeRDP](https://github.com/FreeRDP/FreeRDP).
|
||||
|
||||
To enjoy file copy and paste feature on Linux/OSX,
|
||||
please build with `unix-file-copy-paste` feature.
|
||||
|
||||
TODO: Move this lib to a separate project.
|
||||
|
||||
## How it works
|
||||
|
||||
Terminologies:
|
||||
|
||||
- cliprdr: this module
|
||||
- local: the endpoint which initiates a file copy events
|
||||
- remote: the endpoint which paste the file copied from `local`
|
||||
|
||||
The main algorithm of copying and pasting files is from
|
||||
[Remote Desktop Protocol: Clipboard Virtual Channel Extension](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPECLIP/%5bMS-RDPECLIP%5d.pdf),
|
||||
and could be concluded as:
|
||||
|
||||
0. local and remote notify each other that it's ready.
|
||||
1. local subscribes/listening to the system's clipboard for file copy
|
||||
2. local once got file copy event, notice the remote
|
||||
3. remote confirms receive and try pulls the file list
|
||||
4. local updates its file-list, the remote flushes pulled file list to the clipboard
|
||||
5. remote OS or desktop manager initiates a paste, making other programs reading
|
||||
clipboard files. Convert those reading requests to RPCs
|
||||
|
||||
- on Windows, all file reading will go through the stream file API
|
||||
- on Linux/OSX, FUSE is used for converting reading requests to RPCs
|
||||
- in case of local clipboard been transferred back
|
||||
and leading to a dead loop,
|
||||
all file copy event pointing at the FUSE directory will be ignored
|
||||
|
||||
6. finishing pasting all files one by one.
|
||||
|
||||
In a perspective of network data transferring:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant l as Local
|
||||
participant r as Remote
|
||||
note over l, r: Initialize
|
||||
l ->> r: Monitor Ready
|
||||
r ->> l: Monitor Ready
|
||||
loop Get clipboard update
|
||||
l ->> r: Format List (I got update)
|
||||
r ->> l: Format List Response (notified)
|
||||
r ->> l: Format Data Request (requests file list)
|
||||
activate l
|
||||
note left of l: Retrieve file list from system clipboard
|
||||
l ->> r: Format Data Response (containing file list)
|
||||
deactivate l
|
||||
note over r: Update system clipboard with received file list
|
||||
end
|
||||
loop Some application requests copied files
|
||||
note right of r: application reads file from x to x+y
|
||||
note over r: the file is the a-th file on list
|
||||
r ->> l: File Contents Request (read file a offset x size y)
|
||||
activate l
|
||||
note left of l: Find a-th file on list, read from x to x+y
|
||||
l ->> r: File Contents Response (contents of file a offset x size y)
|
||||
deactivate l
|
||||
end
|
||||
```
|
||||
|
||||
Note: In actual implementation, both sides could play send clipboard update
|
||||
and request file contents.
|
||||
There is no such limitation that only local can update clipboard
|
||||
and copy files to remote.
|
||||
|
||||
## impl
|
||||
|
||||
### windows
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
The protocol was originally designed as an extension of the Windows RDP,
|
||||
so the specific message packages fits windows well.
|
||||
|
||||
When starting cliprdr, a thread is spawned to create an invisible window
|
||||
and to subscribe to OLE clipboard events.
|
||||
The window's callback (see `cliprdr_proc` in `src/windows/wf_cliprdr.c`) was
|
||||
set to handle a variety of events.
|
||||
|
||||
Detailed implementation is shown in pictures above.
|
||||
|
||||
### Linux/OSX
|
||||
|
||||
The Cliprdr Server implementation has mainly 3 parts:
|
||||
|
||||
- Clipboard Client
|
||||
- Local File list
|
||||
- FUSE server
|
||||
|
||||
#### Clipboard Client
|
||||
|
||||
The clipboard client has a thread polling for file urls on clipboard.
|
||||
|
||||
If the client found any updates of file urls,
|
||||
after filtering out those pointing to our FUSE directory or duplicated,
|
||||
send format list directly to remote.
|
||||
|
||||
The cliprdr server also uses clipboard client for setting clipboard,
|
||||
or retrieve paths from system.
|
||||
|
||||
#### Local File List
|
||||
|
||||
The local file list is a temporary list of file metadata.
|
||||
When receiving file contents PDU from peer, the server picks
|
||||
out the file requested and open it for reading if necessary.
|
||||
|
||||
Also when receiving Format Data Request PDU from remote asking for file list,
|
||||
the local file list should be rebuilt from file list retrieved from Clipboard Client.
|
||||
|
||||
Some caching and preloading could be done on it since applications are likely to read
|
||||
on the list sequentially.
|
||||
|
||||
#### FUSE server
|
||||
|
||||
The FUSE server could convert POSIX file reading request to File Contents
|
||||
Request/Response RPCs.
|
||||
|
||||
When received file list from remote,
|
||||
the FUSE server will figure out the file system tree and rearrange its content.
|
||||
|
||||
#### Groceries
|
||||
|
||||
- The protocol was originally implemented for windows,
|
||||
so paths in PDU will all be converted to DOS formats in UTF-16 LE encoding,
|
||||
and datetimes will be converted to LDAP timestamp instead of
|
||||
unix timestamp
|
||||
|
||||
```text
|
||||
UNIX
|
||||
/usr/bin/rustdesk
|
||||
->
|
||||
DOS
|
||||
\usr\bin\rustdesk
|
||||
```
|
||||
|
||||
- To better fit for preserving permissions on unix-like platforms,
|
||||
a reserved area of FileDescriptor PDU
|
||||
|
||||
- you may notice
|
||||
the mountpoint is still occupied after the application quits.
|
||||
That's because the FUSE server was not mounted with `AUTO_UNMOUNT`.
|
||||
- It's hard to implement gressful shutdown for a multi-processed program
|
||||
- `AUTO_UNMOUNT` was not enabled by default and requires enable
|
||||
`user_allow_other` in configure. Letting users edit such global
|
||||
configuration to use this feature might not be a good idea.
|
||||
- use [`umount()`](https://man7.org/linux/man-pages/man2/umount.2.html)
|
||||
syscall to unmount will also require that option.
|
||||
- we currently directly call [`umount`](https://man7.org/linux/man-pages/man8/umount.8.html)
|
||||
program to unmount dangling FUSE server. It worked perfectly for now.
|
||||
@@ -0,0 +1,35 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
fn build_c_impl() {
|
||||
let mut build = cc::Build::new();
|
||||
|
||||
build.file("src/windows/wf_cliprdr.c");
|
||||
|
||||
{
|
||||
build.flag_if_supported("-Wno-c++0x-extensions");
|
||||
build.flag_if_supported("-Wno-return-type-c-linkage");
|
||||
build.flag_if_supported("-Wno-invalid-offsetof");
|
||||
build.flag_if_supported("-Wno-unused-parameter");
|
||||
|
||||
if build.get_compiler().is_like_msvc() {
|
||||
build.define("WIN32", "");
|
||||
// build.define("_AMD64_", "");
|
||||
build.flag("-Z7");
|
||||
build.flag("-GR-");
|
||||
// build.flag("-std:c++11");
|
||||
} else {
|
||||
build.flag("-fPIC");
|
||||
// build.flag("-std=c++11");
|
||||
// build.flag("-include");
|
||||
// build.flag(&confdefs_path.to_string_lossy());
|
||||
}
|
||||
|
||||
build.compile("mycliprdr");
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=src/windows/wf_cliprdr.c");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
build_c_impl();
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
#ifndef WF_CLIPRDR_H__
|
||||
#define WF_CLIPRDR_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
typedef signed char INT8, *PINT8;
|
||||
typedef signed short INT16, *PINT16;
|
||||
typedef signed int INT32, *PINT32;
|
||||
typedef unsigned char UINT8, *PUINT8;
|
||||
typedef unsigned short UINT16, *PUINT16;
|
||||
typedef unsigned int UINT32, *PUINT32;
|
||||
typedef unsigned int UINT;
|
||||
typedef int BOOL;
|
||||
typedef unsigned char BYTE;
|
||||
|
||||
/* Clipboard Messages */
|
||||
#define DEFINE_CLIPRDR_HEADER_COMMON() \
|
||||
UINT32 connID; \
|
||||
UINT16 msgType; \
|
||||
UINT16 msgFlags; \
|
||||
UINT32 dataLen
|
||||
|
||||
struct _CLIPRDR_HEADER
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
};
|
||||
typedef struct _CLIPRDR_HEADER CLIPRDR_HEADER;
|
||||
|
||||
struct _CLIPRDR_CAPABILITY_SET
|
||||
{
|
||||
UINT16 capabilitySetType;
|
||||
UINT16 capabilitySetLength;
|
||||
};
|
||||
typedef struct _CLIPRDR_CAPABILITY_SET CLIPRDR_CAPABILITY_SET;
|
||||
|
||||
struct _CLIPRDR_GENERAL_CAPABILITY_SET
|
||||
{
|
||||
UINT16 capabilitySetType;
|
||||
UINT16 capabilitySetLength;
|
||||
|
||||
UINT32 version;
|
||||
UINT32 generalFlags;
|
||||
};
|
||||
typedef struct _CLIPRDR_GENERAL_CAPABILITY_SET CLIPRDR_GENERAL_CAPABILITY_SET;
|
||||
|
||||
struct _CLIPRDR_CAPABILITIES
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
UINT32 cCapabilitiesSets;
|
||||
CLIPRDR_CAPABILITY_SET *capabilitySets;
|
||||
};
|
||||
typedef struct _CLIPRDR_CAPABILITIES CLIPRDR_CAPABILITIES;
|
||||
|
||||
struct _CLIPRDR_MONITOR_READY
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
};
|
||||
typedef struct _CLIPRDR_MONITOR_READY CLIPRDR_MONITOR_READY;
|
||||
|
||||
struct _CLIPRDR_TEMP_DIRECTORY
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
char szTempDir[520];
|
||||
};
|
||||
typedef struct _CLIPRDR_TEMP_DIRECTORY CLIPRDR_TEMP_DIRECTORY;
|
||||
|
||||
struct _CLIPRDR_FORMAT
|
||||
{
|
||||
UINT32 formatId;
|
||||
char *formatName;
|
||||
};
|
||||
typedef struct _CLIPRDR_FORMAT CLIPRDR_FORMAT;
|
||||
|
||||
struct _CLIPRDR_FORMAT_LIST
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
UINT32 numFormats;
|
||||
CLIPRDR_FORMAT *formats;
|
||||
};
|
||||
typedef struct _CLIPRDR_FORMAT_LIST CLIPRDR_FORMAT_LIST;
|
||||
|
||||
struct _CLIPRDR_FORMAT_LIST_RESPONSE
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
};
|
||||
typedef struct _CLIPRDR_FORMAT_LIST_RESPONSE CLIPRDR_FORMAT_LIST_RESPONSE;
|
||||
|
||||
struct _CLIPRDR_LOCK_CLIPBOARD_DATA
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
UINT32 clipDataId;
|
||||
};
|
||||
typedef struct _CLIPRDR_LOCK_CLIPBOARD_DATA CLIPRDR_LOCK_CLIPBOARD_DATA;
|
||||
|
||||
struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
UINT32 clipDataId;
|
||||
};
|
||||
typedef struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA CLIPRDR_UNLOCK_CLIPBOARD_DATA;
|
||||
|
||||
struct _CLIPRDR_FORMAT_DATA_REQUEST
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
UINT32 requestedFormatId;
|
||||
};
|
||||
typedef struct _CLIPRDR_FORMAT_DATA_REQUEST CLIPRDR_FORMAT_DATA_REQUEST;
|
||||
|
||||
struct _CLIPRDR_FORMAT_DATA_RESPONSE
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
const BYTE *requestedFormatData;
|
||||
};
|
||||
typedef struct _CLIPRDR_FORMAT_DATA_RESPONSE CLIPRDR_FORMAT_DATA_RESPONSE;
|
||||
|
||||
struct _CLIPRDR_FILE_CONTENTS_REQUEST
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
UINT32 streamId;
|
||||
UINT32 listIndex;
|
||||
UINT32 dwFlags;
|
||||
UINT32 nPositionLow;
|
||||
UINT32 nPositionHigh;
|
||||
UINT32 cbRequested;
|
||||
BOOL haveClipDataId;
|
||||
UINT32 clipDataId;
|
||||
};
|
||||
typedef struct _CLIPRDR_FILE_CONTENTS_REQUEST CLIPRDR_FILE_CONTENTS_REQUEST;
|
||||
|
||||
struct _CLIPRDR_FILE_CONTENTS_RESPONSE
|
||||
{
|
||||
DEFINE_CLIPRDR_HEADER_COMMON();
|
||||
|
||||
UINT32 streamId;
|
||||
UINT32 cbRequested;
|
||||
const BYTE *requestedData;
|
||||
};
|
||||
typedef struct _CLIPRDR_FILE_CONTENTS_RESPONSE CLIPRDR_FILE_CONTENTS_RESPONSE;
|
||||
|
||||
typedef struct _cliprdr_client_context CliprdrClientContext;
|
||||
|
||||
struct _NOTIFICATION_MESSAGE
|
||||
{
|
||||
// 0 - info, 1 - warning, 2 - error
|
||||
UINT32 type;
|
||||
char *msg;
|
||||
char *details;
|
||||
};
|
||||
typedef struct _NOTIFICATION_MESSAGE NOTIFICATION_MESSAGE;
|
||||
|
||||
typedef UINT (*pcCliprdrServerCapabilities)(CliprdrClientContext *context,
|
||||
const CLIPRDR_CAPABILITIES *capabilities);
|
||||
typedef UINT (*pcCliprdrClientCapabilities)(CliprdrClientContext *context,
|
||||
const CLIPRDR_CAPABILITIES *capabilities);
|
||||
typedef UINT (*pcCliprdrMonitorReady)(CliprdrClientContext *context,
|
||||
const CLIPRDR_MONITOR_READY *monitorReady);
|
||||
typedef UINT (*pcCliprdrTempDirectory)(CliprdrClientContext *context,
|
||||
const CLIPRDR_TEMP_DIRECTORY *tempDirectory);
|
||||
|
||||
typedef UINT (*pcNotifyClipboardMsg)(UINT32 connID, const NOTIFICATION_MESSAGE *msg);
|
||||
|
||||
typedef UINT (*pcHandleClipboardFiles)(UINT32 connID, size_t nFiles, WCHAR **fileNames);
|
||||
|
||||
typedef UINT (*pcCliprdrClientFormatList)(CliprdrClientContext *context,
|
||||
const CLIPRDR_FORMAT_LIST *formatList);
|
||||
typedef UINT (*pcCliprdrServerFormatList)(CliprdrClientContext *context,
|
||||
const CLIPRDR_FORMAT_LIST *formatList);
|
||||
typedef UINT (*pcCliprdrClientFormatListResponse)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse);
|
||||
typedef UINT (*pcCliprdrServerFormatListResponse)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse);
|
||||
typedef UINT (*pcCliprdrClientLockClipboardData)(
|
||||
CliprdrClientContext *context, const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData);
|
||||
typedef UINT (*pcCliprdrServerLockClipboardData)(
|
||||
CliprdrClientContext *context, const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData);
|
||||
typedef UINT (*pcCliprdrClientUnlockClipboardData)(
|
||||
CliprdrClientContext *context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData);
|
||||
typedef UINT (*pcCliprdrServerUnlockClipboardData)(
|
||||
CliprdrClientContext *context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData);
|
||||
typedef UINT (*pcCliprdrClientFormatDataRequest)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest);
|
||||
typedef UINT (*pcCliprdrServerFormatDataRequest)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest);
|
||||
typedef UINT (*pcCliprdrClientFormatDataResponse)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse);
|
||||
typedef UINT (*pcCliprdrServerFormatDataResponse)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse);
|
||||
typedef UINT (*pcCliprdrClientFileContentsRequest)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest);
|
||||
typedef UINT (*pcCliprdrServerFileContentsRequest)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest);
|
||||
typedef UINT (*pcCliprdrClientFileContentsResponse)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse);
|
||||
typedef UINT (*pcCliprdrServerFileContentsResponse)(
|
||||
CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse);
|
||||
|
||||
// TODO: hide more members of clipboard context
|
||||
struct _cliprdr_client_context
|
||||
{
|
||||
void *Custom;
|
||||
BOOL EnableFiles;
|
||||
BOOL EnableOthers;
|
||||
|
||||
BOOL IsStopped;
|
||||
UINT32 ResponseWaitTimeoutSecs;
|
||||
pcCliprdrServerCapabilities ServerCapabilities;
|
||||
pcCliprdrClientCapabilities ClientCapabilities;
|
||||
pcCliprdrMonitorReady MonitorReady;
|
||||
pcCliprdrTempDirectory TempDirectory;
|
||||
pcNotifyClipboardMsg NotifyClipboardMsg;
|
||||
pcHandleClipboardFiles HandleClipboardFiles;
|
||||
pcCliprdrClientFormatList ClientFormatList;
|
||||
pcCliprdrServerFormatList ServerFormatList;
|
||||
pcCliprdrClientFormatListResponse ClientFormatListResponse;
|
||||
pcCliprdrServerFormatListResponse ServerFormatListResponse;
|
||||
pcCliprdrClientLockClipboardData ClientLockClipboardData;
|
||||
pcCliprdrServerLockClipboardData ServerLockClipboardData;
|
||||
pcCliprdrClientUnlockClipboardData ClientUnlockClipboardData;
|
||||
pcCliprdrServerUnlockClipboardData ServerUnlockClipboardData;
|
||||
pcCliprdrClientFormatDataRequest ClientFormatDataRequest;
|
||||
pcCliprdrServerFormatDataRequest ServerFormatDataRequest;
|
||||
pcCliprdrClientFormatDataResponse ClientFormatDataResponse;
|
||||
pcCliprdrServerFormatDataResponse ServerFormatDataResponse;
|
||||
pcCliprdrClientFileContentsRequest ClientFileContentsRequest;
|
||||
pcCliprdrServerFileContentsRequest ServerFileContentsRequest;
|
||||
pcCliprdrClientFileContentsResponse ClientFileContentsResponse;
|
||||
pcCliprdrServerFileContentsResponse ServerFileContentsResponse;
|
||||
|
||||
UINT32 LastRequestedFormatId;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // WF_CLIPRDR_H__
|
||||
@@ -0,0 +1,79 @@
|
||||
use hbb_common::{log, ResultType};
|
||||
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::default();
|
||||
}
|
||||
|
||||
#[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.lock().unwrap().is_some()
|
||||
}
|
||||
|
||||
pub fn set_is_stopped() {
|
||||
let _res = Self::proc(|c| c.set_is_stopped().map_err(|e| e.into()));
|
||||
}
|
||||
|
||||
pub fn enable(enabled: bool) {
|
||||
let mut lock = CONTEXT_SEND.lock().unwrap();
|
||||
if enabled {
|
||||
if lock.is_some() {
|
||||
return;
|
||||
}
|
||||
match crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS) {
|
||||
Ok(context) => {
|
||||
log::info!("clipboard context for file transfer created.");
|
||||
*lock = Some(context)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"create clipboard context for file transfer: {}",
|
||||
err.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if let Some(_clp) = lock.take() {
|
||||
*lock = None;
|
||||
log::info!("clipboard context for file transfer destroyed.");
|
||||
}
|
||||
}
|
||||
|
||||
/// make sure the clipboard context is enabled.
|
||||
pub fn make_sure_enabled() -> ResultType<()> {
|
||||
let mut lock = CONTEXT_SEND.lock().unwrap();
|
||||
if lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ctx = crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS)?;
|
||||
*lock = Some(ctx);
|
||||
log::info!("clipboard context for file transfer recreated.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn proc<F: FnOnce(&mut Box<dyn CliprdrServiceContext>) -> ResultType<()>>(
|
||||
f: F,
|
||||
) -> ResultType<()> {
|
||||
let mut lock = CONTEXT_SEND.lock().unwrap();
|
||||
match lock.as_mut() {
|
||||
Some(context) => f(context),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[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};
|
||||
use hbb_common::{
|
||||
lazy_static,
|
||||
tokio::sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
Mutex as TokioMutex,
|
||||
},
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "macos", feature = "unix-file-copy-paste")
|
||||
))]
|
||||
pub mod context_send;
|
||||
pub mod platform;
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(target_os = "macos", feature = "unix-file-copy-paste")
|
||||
))]
|
||||
pub use context_send::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
|
||||
|
||||
#[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
|
||||
///
|
||||
/// # Note
|
||||
/// There actually should be 2 parts to implement a useable clipboard file service,
|
||||
/// but this only contains the RPC server part.
|
||||
/// The local listener and transport part is too platform specific to wrap up in typeclasses.
|
||||
pub trait CliprdrServiceContext: Send + Sync {
|
||||
/// set to be stopped
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError>;
|
||||
/// clear the content on clipboard
|
||||
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)]
|
||||
pub enum CliprdrError {
|
||||
#[error("invalid cliprdr name")]
|
||||
CliprdrName,
|
||||
#[error("failed to init cliprdr")]
|
||||
CliprdrInit,
|
||||
#[error("cliprdr out of memory")]
|
||||
CliprdrOutOfMemory,
|
||||
#[error("cliprdr internal error")]
|
||||
ClipboardInternalError,
|
||||
#[error("cliprdr occupied")]
|
||||
ClipboardOccupied,
|
||||
#[error("conversion failure")]
|
||||
ConversionFailure,
|
||||
#[error("failure to read clipboard")]
|
||||
OpenClipboard,
|
||||
#[error("failure to read file metadata or content, path: {path}, err: {err}")]
|
||||
FileError { path: String, err: std::io::Error },
|
||||
#[error("invalid request: {description}")]
|
||||
InvalidRequest { description: String },
|
||||
#[error("common request: {description}")]
|
||||
CommonError { description: String },
|
||||
#[error("unknown cliprdr error")]
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum ClipboardFile {
|
||||
NotifyCallback {
|
||||
r#type: String,
|
||||
title: String,
|
||||
text: String,
|
||||
},
|
||||
MonitorReady,
|
||||
FormatList {
|
||||
format_list: Vec<(i32, String)>,
|
||||
},
|
||||
FormatListResponse {
|
||||
msg_flags: i32,
|
||||
},
|
||||
FormatDataRequest {
|
||||
requested_format_id: i32,
|
||||
},
|
||||
FormatDataResponse {
|
||||
msg_flags: i32,
|
||||
format_data: Vec<u8>,
|
||||
},
|
||||
FileContentsRequest {
|
||||
stream_id: i32,
|
||||
list_index: i32,
|
||||
dw_flags: i32,
|
||||
n_position_low: i32,
|
||||
n_position_high: i32,
|
||||
cb_requested: i32,
|
||||
have_clip_data_id: bool,
|
||||
clip_data_id: i32,
|
||||
},
|
||||
FileContentsResponse {
|
||||
msg_flags: i32,
|
||||
stream_id: i32,
|
||||
requested_data: Vec<u8>,
|
||||
},
|
||||
TryEmpty,
|
||||
Files {
|
||||
files: Vec<(String, u64)>,
|
||||
},
|
||||
}
|
||||
|
||||
struct MsgChannel {
|
||||
peer_id: String,
|
||||
conn_id: i32,
|
||||
#[allow(dead_code)]
|
||||
sender: UnboundedSender<ClipboardFile>,
|
||||
receiver: Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref VEC_MSG_CHANNEL: RwLock<Vec<MsgChannel>> = Default::default();
|
||||
static ref CLIENT_CONN_ID_COUNTER: Mutex<i32> = Mutex::new(0);
|
||||
}
|
||||
|
||||
impl ClipboardFile {
|
||||
pub fn is_stopping_allowed(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ClipboardFile::MonitorReady
|
||||
| ClipboardFile::FormatList { .. }
|
||||
| ClipboardFile::FormatDataRequest { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_beginning_message(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_client_conn_id(peer_id: &str) -> Option<i32> {
|
||||
VEC_MSG_CHANNEL
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|x| x.peer_id == peer_id)
|
||||
.map(|x| x.conn_id)
|
||||
}
|
||||
|
||||
fn get_conn_id() -> i32 {
|
||||
let mut lock = CLIENT_CONN_ID_COUNTER.lock().unwrap();
|
||||
*lock += 1;
|
||||
*lock
|
||||
}
|
||||
|
||||
pub fn get_rx_cliprdr_client(
|
||||
peer_id: &str,
|
||||
) -> (i32, Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>) {
|
||||
let mut lock = VEC_MSG_CHANNEL.write().unwrap();
|
||||
match lock.iter().find(|x| x.peer_id == peer_id) {
|
||||
Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()),
|
||||
None => {
|
||||
let (sender, receiver) = unbounded_channel();
|
||||
let receiver = Arc::new(TokioMutex::new(receiver));
|
||||
let receiver2 = receiver.clone();
|
||||
let conn_id = get_conn_id();
|
||||
let msg_channel = MsgChannel {
|
||||
peer_id: peer_id.to_owned(),
|
||||
conn_id,
|
||||
sender,
|
||||
receiver,
|
||||
};
|
||||
lock.push(msg_channel);
|
||||
(conn_id, receiver2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>> {
|
||||
let mut lock = VEC_MSG_CHANNEL.write().unwrap();
|
||||
match lock.iter().find(|x| x.conn_id == conn_id) {
|
||||
Some(msg_channel) => msg_channel.receiver.clone(),
|
||||
None => {
|
||||
let (sender, receiver) = unbounded_channel();
|
||||
let receiver = Arc::new(TokioMutex::new(receiver));
|
||||
let receiver2 = receiver.clone();
|
||||
let msg_channel = MsgChannel {
|
||||
peer_id: "".to_string(),
|
||||
conn_id,
|
||||
sender,
|
||||
receiver,
|
||||
};
|
||||
lock.push(msg_channel);
|
||||
receiver2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_channel_by_conn_id(conn_id: i32) {
|
||||
let mut lock = VEC_MSG_CHANNEL.write().unwrap();
|
||||
if let Some(index) = lock.iter().position(|x| x.conn_id == conn_id) {
|
||||
lock.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
#[inline]
|
||||
pub fn send_data(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return send_data_to_channel(conn_id, data);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if conn_id == 0 {
|
||||
let _ = send_data_to_all(data);
|
||||
Ok(())
|
||||
} else {
|
||||
send_data_to_channel(conn_id, data)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
if let Some(msg_channel) = VEC_MSG_CHANNEL
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|x| x.conn_id == conn_id)
|
||||
{
|
||||
msg_channel
|
||||
.sender
|
||||
.send(data)
|
||||
.map_err(|e| CliprdrError::CommonError {
|
||||
description: e.to_string(),
|
||||
})
|
||||
} else {
|
||||
Err(CliprdrError::InvalidRequest {
|
||||
description: "conn_id not found".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) {
|
||||
// Need more tests to see if it's necessary to handle the error.
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
if msg_channel.conn_id != conn_id {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
fn send_data_to_all(data: ClipboardFile) {
|
||||
// Need more tests to see if it's necessary to handle the error.
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// #[test]
|
||||
// fn test_cliprdr_run() {
|
||||
// super::cliprdr_run();
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn create_cliprdr_context(
|
||||
enable_files: bool,
|
||||
enable_others: bool,
|
||||
response_wait_timeout_secs: u32,
|
||||
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
|
||||
let boxed =
|
||||
windows::create_cliprdr_context(enable_files, enable_others, response_wait_timeout_secs)?
|
||||
as Box<_>;
|
||||
Ok(boxed)
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub mod unix;
|
||||
|
||||
#[cfg(all(feature = "unix-file-copy-paste", 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)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
use super::{FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_UNIX_MODE, LDAP_EPOCH_DELTA};
|
||||
use crate::CliprdrError;
|
||||
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, Serialize, Deserialize)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
// todo: support symlink
|
||||
Symlink,
|
||||
}
|
||||
|
||||
/// read only permission
|
||||
pub const PERM_READ: u16 = 0o444;
|
||||
/// read and write permission
|
||||
pub const PERM_RW: u16 = 0o644;
|
||||
/// only self can read and readonly
|
||||
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, Serialize, Deserialize)]
|
||||
pub struct FileDescription {
|
||||
pub conn_id: i32,
|
||||
pub name: PathBuf,
|
||||
pub kind: FileType,
|
||||
pub atime: SystemTime,
|
||||
pub last_modified: SystemTime,
|
||||
pub last_metadata_changed: SystemTime,
|
||||
pub creation_time: SystemTime,
|
||||
pub size: u64,
|
||||
pub perm: u16,
|
||||
}
|
||||
|
||||
impl FileDescription {
|
||||
fn parse_file_descriptor(
|
||||
bytes: &mut Bytes,
|
||||
conn_id: i32,
|
||||
) -> Result<FileDescription, CliprdrError> {
|
||||
let flags = bytes.get_u32_le();
|
||||
// skip reserved 32 bytes
|
||||
bytes.advance(32);
|
||||
let attributes = bytes.get_u32_le();
|
||||
|
||||
// in original specification, this is 16 bytes reserved
|
||||
// we use the last 4 bytes to store the file mode
|
||||
// skip reserved 12 bytes
|
||||
bytes.advance(12);
|
||||
let perm = bytes.get_u32_le() as u16;
|
||||
|
||||
// last write time from 1601-01-01 00:00:00, in 100ns
|
||||
let last_write_time = bytes.get_u64_le();
|
||||
// file size
|
||||
let file_size_high = bytes.get_u32_le();
|
||||
let file_size_low = bytes.get_u32_le();
|
||||
// utf16 file name, double \0 terminated, in 520 bytes block
|
||||
// read with another pointer, and advance the main pointer
|
||||
let block = bytes.clone();
|
||||
bytes.advance(520);
|
||||
|
||||
let block = &block[..520];
|
||||
let wstr = WStr::from_utf16le(block).map_err(|e| {
|
||||
log::error!("cannot convert file descriptor path: {:?}", e);
|
||||
CliprdrError::ConversionFailure
|
||||
})?;
|
||||
|
||||
let from_unix = flags & FLAGS_FD_UNIX_MODE != 0;
|
||||
|
||||
let valid_attributes = flags & FLAGS_FD_ATTRIBUTES != 0;
|
||||
if !valid_attributes {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file description must have valid attributes".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// todo: check normal, hidden, system, readonly, archive...
|
||||
let directory = attributes & 0x10 != 0;
|
||||
let normal = attributes == 0x80;
|
||||
let hidden = attributes & 0x02 != 0;
|
||||
let readonly = attributes & 0x01 != 0;
|
||||
|
||||
let perm = if from_unix {
|
||||
// as is
|
||||
perm
|
||||
// cannot set as is...
|
||||
} else if normal {
|
||||
PERM_RWX
|
||||
} else if readonly {
|
||||
PERM_READ
|
||||
} else if hidden {
|
||||
PERM_SELF_RO
|
||||
} else if directory {
|
||||
PERM_RWX
|
||||
} else {
|
||||
PERM_RW
|
||||
};
|
||||
|
||||
let kind = if directory {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
};
|
||||
|
||||
// to-do: use `let valid_size = flags & FLAGS_FD_SIZE != 0;`
|
||||
// We use `true` to for compatibility with Windows.
|
||||
// let valid_size = flags & FLAGS_FD_SIZE != 0;
|
||||
let valid_size = true;
|
||||
let size = if valid_size {
|
||||
((file_size_high as u64) << 32) + file_size_low as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let valid_write_time = flags & FLAGS_FD_LAST_WRITE != 0;
|
||||
let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA {
|
||||
let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100;
|
||||
let last_write_time = Duration::from_nanos(last_write_time);
|
||||
SystemTime::UNIX_EPOCH + last_write_time
|
||||
} else {
|
||||
SystemTime::UNIX_EPOCH
|
||||
};
|
||||
|
||||
let name = wstr.to_utf8().replace('\\', "/");
|
||||
let name = PathBuf::from(name.trim_end_matches('\0'));
|
||||
|
||||
let desc = FileDescription {
|
||||
conn_id,
|
||||
name,
|
||||
kind,
|
||||
atime: last_modified,
|
||||
last_modified,
|
||||
last_metadata_changed: last_modified,
|
||||
creation_time: last_modified,
|
||||
size,
|
||||
perm,
|
||||
};
|
||||
|
||||
Ok(desc)
|
||||
}
|
||||
|
||||
/// parse file descriptions from a format data response PDU
|
||||
/// which containing a CSPTR_FILEDESCRIPTORW indicated format data
|
||||
pub fn parse_file_descriptors(
|
||||
file_descriptor_pdu: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<Self>, CliprdrError> {
|
||||
let mut data = Bytes::from(file_descriptor_pdu);
|
||||
if data.remaining() < 4 {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with infficient length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let count = data.get_u32_le() as usize;
|
||||
if data.remaining() == 0 && count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
if data.remaining() != 592 * count {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with invalid length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut files = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let desc = Self::parse_file_descriptor(&mut data, conn_id)?;
|
||||
files.push(desc);
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,225 @@
|
||||
mod cs;
|
||||
|
||||
use super::filetype::FileDescription;
|
||||
use crate::{ClipboardFile, CliprdrError};
|
||||
use cs::FuseServer;
|
||||
use fuser::MountOption;
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
|
||||
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
|
||||
static ref FUSE_CONTEXT_CLIENT: Arc<Mutex<Option<FuseContext>>> = Arc::new(Mutex::new(None));
|
||||
static ref FUSE_CONTEXT_SERVER: Arc<Mutex<Option<FuseContext>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
|
||||
if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
} else {
|
||||
FUSE_MOUNT_POINT_SERVER.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_fuse_context_inited(is_client: bool) -> bool {
|
||||
if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock().is_some()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
||||
let mut fuse_context_lock = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
if fuse_context_lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let mount_point = if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
} else {
|
||||
FUSE_MOUNT_POINT_SERVER.clone()
|
||||
};
|
||||
|
||||
let mount_point = std::path::PathBuf::from(&*mount_point);
|
||||
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
|
||||
prepare_fuse_mount_point(&mount_point);
|
||||
let mnt_opts = [
|
||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||
MountOption::NoAtime,
|
||||
MountOption::RO,
|
||||
];
|
||||
log::info!("mounting clipboard FUSE to {}", mount_point.display());
|
||||
// to-do: ignore the error if the mount point is already mounted
|
||||
// Because the sciter version uses separate processes as the controlling side.
|
||||
let session = fuser::spawn_mount2(
|
||||
FuseServer::client(server.clone()),
|
||||
mount_point.clone(),
|
||||
&mnt_opts,
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to mount cliprdr fuse: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
let session = Mutex::new(Some(session));
|
||||
|
||||
let ctx = FuseContext {
|
||||
server,
|
||||
tx,
|
||||
mount_point,
|
||||
session,
|
||||
conn_id: 0,
|
||||
};
|
||||
*fuse_context_lock = Some(ctx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninit_fuse_context(is_client: bool) {
|
||||
uninit_fuse_context_(is_client)
|
||||
}
|
||||
|
||||
pub fn format_data_response_to_urls(
|
||||
is_client: bool,
|
||||
format_data: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<String>, CliprdrError> {
|
||||
let mut ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_mut()
|
||||
.ok_or(CliprdrError::CliprdrInit)?
|
||||
.format_data_response_to_urls(format_data, conn_id)
|
||||
}
|
||||
|
||||
pub fn handle_file_content_response(
|
||||
is_client: bool,
|
||||
clip: ClipboardFile,
|
||||
) -> Result<(), CliprdrError> {
|
||||
// we don't know its corresponding request, no resend can be performed
|
||||
let ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_ref()
|
||||
.ok_or(CliprdrError::CliprdrInit)?
|
||||
.tx
|
||||
.send(clip)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to send file contents response to fuse: {:?}", e);
|
||||
CliprdrError::ClipboardInternalError
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn empty_local_files(is_client: bool, conn_id: i32) -> bool {
|
||||
let ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_ref()
|
||||
.map(|c| c.empty_local_files(conn_id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
struct FuseContext {
|
||||
server: Arc<Mutex<FuseServer>>,
|
||||
tx: Sender<ClipboardFile>,
|
||||
mount_point: PathBuf,
|
||||
// stores fuse background session handle
|
||||
session: Mutex<Option<fuser::BackgroundSession>>,
|
||||
// Indicates the connection ID of that set the clipboard content
|
||||
conn_id: i32,
|
||||
}
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
|
||||
use std::{
|
||||
fs::{self, Permissions},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
};
|
||||
|
||||
fs::create_dir(mount_point).ok();
|
||||
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
|
||||
|
||||
if let Err(e) = std::process::Command::new("umount")
|
||||
.arg(mount_point)
|
||||
.status()
|
||||
{
|
||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
||||
}
|
||||
}
|
||||
|
||||
fn uninit_fuse_context_(is_client: bool) {
|
||||
if is_client {
|
||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
||||
} else {
|
||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FuseContext {
|
||||
fn drop(&mut self) {
|
||||
self.session.lock().take().map(|s| s.join());
|
||||
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
|
||||
}
|
||||
}
|
||||
|
||||
impl FuseContext {
|
||||
pub fn empty_local_files(&self, conn_id: i32) -> bool {
|
||||
if conn_id != 0 && self.conn_id != conn_id {
|
||||
return false;
|
||||
}
|
||||
let mut fuse_guard = self.server.lock();
|
||||
let _ = fuse_guard.load_file_list(vec![]);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn format_data_response_to_urls(
|
||||
&mut self,
|
||||
format_data: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<String>, CliprdrError> {
|
||||
let files = FileDescription::parse_file_descriptors(format_data, conn_id)?;
|
||||
|
||||
let paths = {
|
||||
let mut fuse_guard = self.server.lock();
|
||||
fuse_guard.load_file_list(files)?;
|
||||
self.conn_id = conn_id;
|
||||
|
||||
fuse_guard.list_root()
|
||||
};
|
||||
|
||||
let prefix = self.mount_point.clone();
|
||||
Ok(paths
|
||||
.into_iter()
|
||||
.map(|p| prefix.join(p).to_string_lossy().to_string())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
use super::{BLOCK_SIZE, LDAP_EPOCH_DELTA};
|
||||
use crate::{
|
||||
platform::unix::{
|
||||
FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_PROGRESSUI, FLAGS_FD_SIZE,
|
||||
FLAGS_FD_UNIX_MODE,
|
||||
},
|
||||
CliprdrError,
|
||||
};
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read, Seek},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::SystemTime,
|
||||
};
|
||||
use utf16string::WString;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LocalFile {
|
||||
pub relative_root: PathBuf,
|
||||
pub path: PathBuf,
|
||||
|
||||
pub handle: Option<BufReader<File>>,
|
||||
pub offset: AtomicU64,
|
||||
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub last_write_time: SystemTime,
|
||||
pub is_dir: bool,
|
||||
pub perm: u32,
|
||||
pub read_only: bool,
|
||||
pub hidden: bool,
|
||||
pub system: bool,
|
||||
pub archive: bool,
|
||||
pub normal: bool,
|
||||
}
|
||||
|
||||
impl LocalFile {
|
||||
pub fn try_open(relative_root: &Path, path: &Path) -> Result<Self, CliprdrError> {
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let size = mt.len() as u64;
|
||||
let is_dir = mt.is_dir();
|
||||
let read_only = mt.permissions().readonly();
|
||||
let system = false;
|
||||
let hidden = path.to_string_lossy().starts_with('.');
|
||||
let archive = false;
|
||||
let normal = !(is_dir || read_only || system || hidden || archive);
|
||||
let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
|
||||
let perm = mt.permissions().mode();
|
||||
|
||||
let name = path
|
||||
.display()
|
||||
.to_string()
|
||||
.trim_start_matches('/')
|
||||
.replace('/', "\\");
|
||||
|
||||
// NOTE: open files lazily
|
||||
let handle = None;
|
||||
let offset = AtomicU64::new(0);
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
relative_root: relative_root.to_path_buf(),
|
||||
path: path.to_path_buf(),
|
||||
handle,
|
||||
offset,
|
||||
size,
|
||||
last_write_time,
|
||||
is_dir,
|
||||
read_only,
|
||||
system,
|
||||
hidden,
|
||||
perm,
|
||||
archive,
|
||||
normal,
|
||||
})
|
||||
}
|
||||
pub fn as_bin(&self) -> Vec<u8> {
|
||||
let mut buf = BytesMut::with_capacity(592);
|
||||
|
||||
let read_only_flag = if self.read_only { 0x1 } else { 0 };
|
||||
let hidden_flag = if self.hidden { 0x2 } else { 0 };
|
||||
let system_flag = if self.system { 0x4 } else { 0 };
|
||||
let directory_flag = if self.is_dir { 0x10 } else { 0 };
|
||||
let archive_flag = if self.archive { 0x20 } else { 0 };
|
||||
let normal_flag = if self.normal { 0x80 } else { 0 };
|
||||
|
||||
let file_attributes: u32 = read_only_flag
|
||||
| hidden_flag
|
||||
| system_flag
|
||||
| directory_flag
|
||||
| archive_flag
|
||||
| normal_flag;
|
||||
|
||||
let win32_time = self
|
||||
.last_write_time
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos() as u64
|
||||
/ 100
|
||||
+ LDAP_EPOCH_DELTA;
|
||||
|
||||
let size_high = (self.size >> 32) as u32;
|
||||
let size_low = (self.size & (u32::MAX as u64)) as u32;
|
||||
|
||||
let path = self
|
||||
.path
|
||||
.strip_prefix(&self.relative_root)
|
||||
.unwrap_or(&self.path)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
let wstr: WString<utf16string::LE> = WString::from(&path);
|
||||
let name = wstr.as_bytes();
|
||||
|
||||
log::trace!(
|
||||
"put file to list: name_len {}, name {}",
|
||||
name.len(),
|
||||
&self.name
|
||||
);
|
||||
|
||||
let flags = FLAGS_FD_SIZE
|
||||
| FLAGS_FD_LAST_WRITE
|
||||
| FLAGS_FD_ATTRIBUTES
|
||||
| FLAGS_FD_PROGRESSUI
|
||||
| FLAGS_FD_UNIX_MODE;
|
||||
|
||||
// flags, 4 bytes
|
||||
buf.put_u32_le(flags);
|
||||
// 32 bytes reserved
|
||||
buf.put(&[0u8; 32][..]);
|
||||
// file attributes, 4 bytes
|
||||
buf.put_u32_le(file_attributes);
|
||||
|
||||
// NOTE: this is not used in windows
|
||||
// in the specification, this is 16 bytes reserved
|
||||
// lets use the last 4 bytes to store the file mode
|
||||
//
|
||||
// 12 bytes reserved
|
||||
buf.put(&[0u8; 12][..]);
|
||||
// file permissions, 4 bytes
|
||||
buf.put_u32_le(self.perm);
|
||||
|
||||
// last write time, 8 bytes
|
||||
buf.put_u64_le(win32_time);
|
||||
// file size (high)
|
||||
buf.put_u32_le(size_high);
|
||||
// file size (low)
|
||||
buf.put_u32_le(size_low);
|
||||
// put name and padding to 520 bytes
|
||||
let name_len = name.len();
|
||||
buf.put(name);
|
||||
buf.put(&vec![0u8; 520 - name_len][..]);
|
||||
|
||||
buf.to_vec()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn load_handle(&mut self) -> Result<(), CliprdrError> {
|
||||
if !self.is_dir && self.handle.is_none() {
|
||||
let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle);
|
||||
reader.fill_buf().map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
self.handle = Some(reader);
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
|
||||
self.load_handle()?;
|
||||
|
||||
let Some(handle) = self.handle.as_mut() else {
|
||||
return Err(CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle not found"),
|
||||
});
|
||||
};
|
||||
|
||||
if offset != self.offset.load(Ordering::Relaxed) {
|
||||
handle
|
||||
.seek(std::io::SeekFrom::Start(offset))
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
}
|
||||
handle
|
||||
.read_exact(buf)
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let new_offset = offset + (buf.len() as u64);
|
||||
self.offset.store(new_offset, Ordering::Relaxed);
|
||||
|
||||
// gc file handle
|
||||
if new_offset >= self.size {
|
||||
self.offset.store(0, Ordering::Relaxed);
|
||||
self.handle = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||
fn constr_file_lst(
|
||||
relative_root: &Path,
|
||||
path: &Path,
|
||||
file_list: &mut Vec<LocalFile>,
|
||||
visited: &mut HashSet<PathBuf>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
// prevent fs loop
|
||||
if visited.contains(path) {
|
||||
return Ok(());
|
||||
}
|
||||
visited.insert(path.to_path_buf());
|
||||
|
||||
let local_file = LocalFile::try_open(relative_root, path)?;
|
||||
file_list.push(local_file);
|
||||
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
|
||||
if mt.is_dir() {
|
||||
let dir = std::fs::read_dir(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
for entry in dir {
|
||||
let entry = entry.map_err(|e| CliprdrError::FileError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let path = entry.path();
|
||||
constr_file_lst(relative_root, &path, file_list, visited)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut file_list = Vec::new();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
let relative_root = paths
|
||||
.first()
|
||||
.ok_or(CliprdrError::InvalidRequest {
|
||||
description: "empty file list".to_string(),
|
||||
})?
|
||||
.parent()
|
||||
.ok_or(CliprdrError::InvalidRequest {
|
||||
description: "empty parent".to_string(),
|
||||
})?
|
||||
.to_path_buf();
|
||||
for path in paths {
|
||||
constr_file_lst(&relative_root, path, &mut file_list, &mut visited)?;
|
||||
}
|
||||
Ok(file_list)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod file_list_test {
|
||||
use std::{path::PathBuf, sync::atomic::AtomicU64};
|
||||
|
||||
use hbb_common::bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::{platform::unix::filetype::FileDescription, CliprdrError};
|
||||
|
||||
use super::LocalFile;
|
||||
|
||||
#[inline]
|
||||
fn generate_tree(prefix: &str) -> Vec<LocalFile> {
|
||||
// generate a tree of local files, no handles
|
||||
// - /
|
||||
// |- a.txt
|
||||
// |- b
|
||||
// |- c.txt
|
||||
#[inline]
|
||||
fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile {
|
||||
LocalFile {
|
||||
relative_root: PathBuf::from("."),
|
||||
path: PathBuf::from(path),
|
||||
handle: None,
|
||||
name: name.to_string(),
|
||||
size: 0,
|
||||
offset: AtomicU64::new(0),
|
||||
last_write_time: std::time::SystemTime::UNIX_EPOCH,
|
||||
read_only: false,
|
||||
is_dir,
|
||||
perm: 0o754,
|
||||
hidden: false,
|
||||
system: false,
|
||||
archive: false,
|
||||
normal: false,
|
||||
}
|
||||
}
|
||||
|
||||
let p = prefix;
|
||||
|
||||
let (r_path, a_path, b_path, c_path) = if !prefix.is_empty() {
|
||||
(
|
||||
p.to_string(),
|
||||
format!("{}/a.txt", p),
|
||||
format!("{}/b", p),
|
||||
format!("{}/b/c.txt", p),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
".".to_string(),
|
||||
"a.txt".to_string(),
|
||||
"b".to_string(),
|
||||
"b/c.txt".to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
let root = generate_file(&r_path, ".", true);
|
||||
let a = generate_file(&a_path, "a.txt", false);
|
||||
let b = generate_file(&b_path, "b", true);
|
||||
let c = generate_file(&c_path, "c.txt", false);
|
||||
|
||||
vec![root, a, b, c]
|
||||
}
|
||||
|
||||
fn as_bin_parse_test(prefix: &str) -> Result<(), CliprdrError> {
|
||||
let tree = generate_tree(prefix);
|
||||
let mut pdu = BytesMut::with_capacity(4 + 592 * tree.len());
|
||||
pdu.put_u32_le(tree.len() as u32);
|
||||
for file in tree {
|
||||
pdu.put(file.as_bin().as_slice());
|
||||
}
|
||||
|
||||
let parsed = FileDescription::parse_file_descriptors(pdu.to_vec(), 0)?;
|
||||
assert_eq!(parsed.len(), 4);
|
||||
|
||||
if !prefix.is_empty() {
|
||||
assert_eq!(parsed[0].name.to_str().unwrap(), format!("{}", prefix));
|
||||
assert_eq!(
|
||||
parsed[1].name.to_str().unwrap(),
|
||||
format!("{}/a.txt", prefix)
|
||||
);
|
||||
assert_eq!(parsed[2].name.to_str().unwrap(), format!("{}/b", prefix));
|
||||
assert_eq!(
|
||||
parsed[3].name.to_str().unwrap(),
|
||||
format!("{}/b/c.txt", prefix)
|
||||
);
|
||||
} else {
|
||||
assert_eq!(parsed[0].name.to_str().unwrap(), ".");
|
||||
assert_eq!(parsed[1].name.to_str().unwrap(), "a.txt");
|
||||
assert_eq!(parsed[2].name.to_str().unwrap(), "b");
|
||||
assert_eq!(parsed[3].name.to_str().unwrap(), "b/c.txt");
|
||||
}
|
||||
|
||||
assert!(parsed[0].perm & 0o777 == 0o754);
|
||||
assert!(parsed[1].perm & 0o777 == 0o754);
|
||||
assert!(parsed[2].perm & 0o777 == 0o754);
|
||||
assert!(parsed[3].perm & 0o777 == 0o754);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_file_descriptors() -> Result<(), CliprdrError> {
|
||||
as_bin_parse_test("")?;
|
||||
as_bin_parse_test("/")?;
|
||||
as_bin_parse_test("test")?;
|
||||
as_bin_parse_test("/test")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
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::autoreleasepool, 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(),
|
||||
});
|
||||
};
|
||||
|
||||
autoreleasepool(|_| self.set_clipboard_item(tx_handle, conn_id, file_descriptor_id))?;
|
||||
} else {
|
||||
return Err(CliprdrError::CommonError {
|
||||
description: "pasteboard context is not inited".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_clipboard_item(
|
||||
&self,
|
||||
tx_handle: &ContextInfo,
|
||||
conn_id: i32,
|
||||
file_descriptor_id: i32,
|
||||
) -> Result<(), CliprdrError> {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
let mut created_files = vec![];
|
||||
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() {
|
||||
created_files.push(path);
|
||||
c += 1;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(c, super::PasteboardContext::temp_files_count());
|
||||
|
||||
// Clean up the created files.
|
||||
for file in created_files {
|
||||
std::fs::remove_file(&file).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
|
||||
/// has valid file attributes
|
||||
pub const FLAGS_FD_ATTRIBUTES: u32 = 0x04;
|
||||
/// has valid file size
|
||||
pub const FLAGS_FD_SIZE: u32 = 0x40;
|
||||
/// has valid last write time
|
||||
pub const FLAGS_FD_LAST_WRITE: u32 = 0x20;
|
||||
/// show progress
|
||||
pub const FLAGS_FD_PROGRESSUI: u32 = 0x4000;
|
||||
/// transferred from unix, contains file mode
|
||||
/// P.S. this flag is not used in windows
|
||||
pub const FLAGS_FD_UNIX_MODE: u32 = 0x08;
|
||||
|
||||
// not actual format id, just a placeholder
|
||||
pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
|
||||
pub const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
|
||||
// not actual format id, just a placeholder
|
||||
pub const FILECONTENTS_FORMAT_ID: i32 = 49267;
|
||||
pub const FILECONTENTS_FORMAT_NAME: &str = "FileContents";
|
||||
|
||||
/// block size for fuse, align to our asynchronic request size over FileContentsRequest.
|
||||
pub(crate) const BLOCK_SIZE: u32 = 4 * 1024 * 1024;
|
||||
|
||||
// begin of epoch used by microsoft
|
||||
// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00
|
||||
const LDAP_EPOCH_DELTA: u64 = 116444772610000000;
|
||||
|
||||
lazy_static! {
|
||||
static ref REMOTE_FORMAT_MAP: DashMap<i32, String> = DashMap::from_iter(
|
||||
[
|
||||
(
|
||||
FILEDESCRIPTOR_FORMAT_ID,
|
||||
FILEDESCRIPTORW_FORMAT_NAME.to_string()
|
||||
),
|
||||
(FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string())
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_local_format(remote_id: i32) -> Option<String> {
|
||||
REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone())
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
use super::local_file::LocalFile;
|
||||
use crate::{platform::unix::local_file::construct_file_list, ClipboardFile, CliprdrError};
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{path::PathBuf, sync::Arc, usize};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// local files are cached, this value should not be changed when copying files
|
||||
// Because `CliprdrFileContentsRequest` only contains the index of the file in the list.
|
||||
// We need to keep the file list in the same order as the remote side.
|
||||
// We may add a `FileId` field to `CliprdrFileContentsRequest` in the future.
|
||||
static ref CLIP_FILES: Arc<Mutex<ClipFiles>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FileContentsRequest {
|
||||
Size {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
},
|
||||
|
||||
Range {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClipFiles {
|
||||
files: Vec<String>,
|
||||
file_list: Vec<LocalFile>,
|
||||
first_file_index: usize,
|
||||
files_pdu: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ClipFiles {
|
||||
fn clear(&mut self) {
|
||||
self.files.clear();
|
||||
self.file_list.clear();
|
||||
self.first_file_index = usize::MAX;
|
||||
self.files_pdu.clear();
|
||||
}
|
||||
|
||||
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
|
||||
let clipboard_paths = clipboard_files
|
||||
.iter()
|
||||
.map(|s| PathBuf::from(s))
|
||||
.collect::<Vec<_>>();
|
||||
self.file_list = construct_file_list(&clipboard_paths)?;
|
||||
self.first_file_index = self
|
||||
.file_list
|
||||
.iter()
|
||||
.position(|f| !f.path.is_dir())
|
||||
.unwrap_or(usize::MAX);
|
||||
self.files = clipboard_files.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_file_list_pdu(&mut self) {
|
||||
let mut data = BytesMut::with_capacity(4 + 592 * self.file_list.len());
|
||||
data.put_u32_le(self.file_list.len() as u32);
|
||||
for file in self.file_list.iter() {
|
||||
data.put(file.as_bin().as_slice());
|
||||
}
|
||||
self.files_pdu = data.to_vec()
|
||||
}
|
||||
|
||||
fn get_files_for_audit(&self, request: &FileContentsRequest) -> Option<ClipboardFile> {
|
||||
if let FileContentsRequest::Range {
|
||||
file_idx, offset, ..
|
||||
} = request
|
||||
{
|
||||
if *file_idx == self.first_file_index && *offset == 0 {
|
||||
let files: Vec<(String, u64)> = self
|
||||
.file_list
|
||||
.iter()
|
||||
.filter_map(|f| {
|
||||
if f.path.is_file() {
|
||||
Some((f.path.to_string_lossy().to_string(), f.size))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<_>();
|
||||
if files.is_empty() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(ClipboardFile::Files { files });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn serve_file_contents(
|
||||
&mut self,
|
||||
conn_id: i32,
|
||||
request: FileContentsRequest,
|
||||
) -> Result<ClipboardFile, CliprdrError> {
|
||||
let (file_idx, file_contents_resp) = match request {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx,
|
||||
} => {
|
||||
log::debug!("file contents (size) requested from conn: {}", conn_id);
|
||||
let Some(file) = self.file_list.get(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_id
|
||||
);
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx, conn_id
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"conn {} requested file-{}: {}",
|
||||
conn_id,
|
||||
file_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
let size = file.size;
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: size.to_le_bytes().to_vec(),
|
||||
},
|
||||
)
|
||||
}
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
log::debug!(
|
||||
"file contents (range from {} length {}) request from conn: {}",
|
||||
offset,
|
||||
length,
|
||||
conn_id
|
||||
);
|
||||
let Some(file) = self.file_list.get_mut(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_id
|
||||
);
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx, conn_id
|
||||
),
|
||||
});
|
||||
};
|
||||
log::debug!(
|
||||
"conn {} requested file-{}: {}",
|
||||
conn_id,
|
||||
file_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
if offset > file.size {
|
||||
log::error!("invalid reading offset requested from conn: {}", conn_id);
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid reading offset requested from conn: {}",
|
||||
conn_id
|
||||
),
|
||||
});
|
||||
}
|
||||
let read_size = if offset + length > file.size {
|
||||
file.size - offset
|
||||
} else {
|
||||
length
|
||||
};
|
||||
|
||||
let mut buf = vec![0u8; read_size as usize];
|
||||
|
||||
file.read_exact_at(&mut buf, offset)?;
|
||||
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: buf,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!("file contents sent to conn: {}", conn_id);
|
||||
// hot reload next file
|
||||
for next_file in self.file_list.iter_mut().skip(file_idx + 1) {
|
||||
if !next_file.is_dir {
|
||||
next_file.load_handle()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(file_contents_resp)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn clear_files() {
|
||||
CLIP_FILES.lock().clear();
|
||||
}
|
||||
|
||||
pub fn read_file_contents(
|
||||
conn_id: i32,
|
||||
stream_id: i32,
|
||||
list_index: i32,
|
||||
dw_flags: i32,
|
||||
n_position_low: i32,
|
||||
n_position_high: i32,
|
||||
cb_requested: i32,
|
||||
) -> Vec<Result<ClipboardFile, CliprdrError>> {
|
||||
let fcr = if dw_flags == 0x1 {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx: list_index as usize,
|
||||
}
|
||||
} else if dw_flags == 0x2 {
|
||||
let offset = (n_position_high as u64) << 32 | n_position_low as u64;
|
||||
let length = cb_requested as u64;
|
||||
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx: list_index as usize,
|
||||
offset,
|
||||
length,
|
||||
}
|
||||
} else {
|
||||
return vec![Err(CliprdrError::InvalidRequest {
|
||||
description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"),
|
||||
})];
|
||||
};
|
||||
|
||||
let mut clip_files = CLIP_FILES.lock();
|
||||
let mut res = vec![];
|
||||
if let Some(files_res) = clip_files.get_files_for_audit(&fcr) {
|
||||
res.push(Ok(files_res));
|
||||
}
|
||||
res.push(clip_files.serve_file_contents(conn_id, fcr));
|
||||
res
|
||||
}
|
||||
|
||||
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
||||
let mut files_lock = CLIP_FILES.lock();
|
||||
if files_lock.files == files {
|
||||
return Ok(());
|
||||
}
|
||||
files_lock.sync_files(files)?;
|
||||
Ok(files_lock.build_file_list_pdu())
|
||||
}
|
||||
|
||||
pub fn get_file_list_pdu() -> Vec<u8> {
|
||||
CLIP_FILES.lock().files_pdu.clone()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user