Skip to main content

virtual_file_transfer/
transfer.rs

1use crate::cancellation::CancellationToken;
2
3use super::virtual_file::VirtualFile;
4
5use std::sync::{Arc, Mutex};
6
7/// The data-transfer operation reported by a receiving application.
8///
9/// An effect describes what operation was requested or reported, not whether
10/// reading the file contents ultimately succeeded. Check [`TransferStatus`]
11/// for the reported outcome.
12#[non_exhaustive]
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum TransferEffect {
15    /// No copy, move, or link effect was reported.
16    None,
17    /// The receiver reported a copy operation.
18    Copy,
19    /// The receiver reported a move operation.
20    Move,
21    /// The receiver reported a link operation.
22    Link,
23    /// An unrecognized platform-specific effect, retaining its raw value.
24    Other(u32),
25}
26
27/// A platform-specific completion status reported for a transfer.
28///
29/// The numeric value is intended for diagnostics. Interpret it according to
30/// the originating platform, and use [`TransferStatus`] for the overall
31/// transfer state.
32#[non_exhaustive]
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum NativeStatusCode {
35    /// A Windows `HRESULT`, stored in its original signed 32-bit representation.
36    ///
37    /// For example, `0x8003001E` represents `STG_E_READFAULT`.
38    WindowsHResult(i32),
39    // add here for other operating system.
40}
41
42/// The observed state of a drag-and-drop operation.
43///
44/// A successful drop and successful delivery of file contents are different
45/// events for receivers using asynchronous OLE transfers. In particular,
46/// [`Self::Dropped`] is not proof that the receiver finished reading data.
47#[non_exhaustive]
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum TransferStatus {
50    /// No terminal drag or asynchronous completion status has been recorded.
51    Pending,
52    /// The operation was cancelled, including cancellation observed during reading.
53    Cancelled,
54    /// The receiver accepted the drop operation.
55    ///
56    /// An asynchronous receiver may still be reading the contents. This status
57    /// does not guarantee that the resulting destination file was completed.
58    Dropped,
59    /// The receiver reported completion of an asynchronous transfer.
60    ///
61    /// Inspect `succeeded`, `native_code`, and `effect` together rather than
62    /// assuming that a non-`None` effect means success.
63    AsyncFinished {
64        /// Whether the receiver reported successful completion.
65        succeeded: bool,
66        /// The platform-specific completion code reported by the receiver.
67        native_code: NativeStatusCode,
68        /// The reported copy, move, link, or other transfer effect.
69        effect: TransferEffect,
70    },
71    /// The reported copy, move, link, or other transfer effect.
72    Failed,
73}
74
75/// A set of virtual files and a cancellation token for one transfer.
76///
77/// Create a request when you need access to its cancellation token before
78/// starting a blocking drag operation. Functions accepting
79/// `Into<TransferRequest>` can also take a `Vec<VirtualFile>` directly.
80pub struct TransferRequest {
81    pub(crate) files: Vec<VirtualFile>,
82    pub(crate) cancellation: CancellationToken,
83}
84
85impl TransferRequest {
86    /// Creates a transfer request containing the supplied virtual files.
87    ///
88    /// The request starts with a new, non-cancelled token. File names and
89    /// transfer-level conflicts are validated when the transfer data object is
90    /// created, not by this constructor.
91    pub fn new(files: Vec<VirtualFile>) -> Self {
92        Self {
93            files,
94            cancellation: CancellationToken::new(),
95        }
96    }
97    /// Returns a clone of the request's shared cancellation token.
98    ///
99    /// Keep the token to request cancellation while a blocking drag operation
100    /// is running, for example from another thread or UI callback.
101    pub fn cancellation_token(&self) -> CancellationToken {
102        self.cancellation.clone()
103    }
104}
105
106impl From<Vec<VirtualFile>> for TransferRequest {
107    fn from(files: Vec<VirtualFile>) -> Self {
108        Self::new(files)
109    }
110}
111
112#[derive(Clone, Copy, Debug)]
113enum CompletionState {
114    Pending,
115    ReadAborted,
116    Finished(TransferStatus),
117}
118
119pub(crate) struct TransferCompletion {
120    state: Mutex<CompletionState>,
121}
122
123impl TransferCompletion {
124    pub(crate) fn new() -> Self {
125        TransferCompletion {
126            state: Mutex::new(CompletionState::Pending),
127        }
128    }
129
130    pub(crate) fn mark_as_read_aborted(&self) -> std::result::Result<(), ()> {
131        let mut state = self.state.lock().map_err(|_| ())?;
132        if matches!(*state, CompletionState::Pending) {
133            *state = CompletionState::ReadAborted;
134        }
135        Ok(())
136    }
137
138    pub(crate) fn record_if_pending(
139        &self,
140        new_status: TransferStatus,
141    ) -> std::result::Result<(), ()> {
142        let mut state = self.state.lock().map_err(|_| ())?;
143        *state = match *state {
144            CompletionState::Pending => CompletionState::Finished(new_status),
145
146            CompletionState::ReadAborted => CompletionState::Finished(TransferStatus::Cancelled),
147
148            CompletionState::Finished(_) => return Ok(()),
149        };
150        Ok(())
151    }
152
153    pub(crate) fn status(&self) -> TransferStatus {
154        let state = self.state.lock().unwrap_or_else(|error| error.into_inner());
155
156        match *state {
157            CompletionState::Pending | CompletionState::ReadAborted => TransferStatus::Pending,
158            CompletionState::Finished(status) => status,
159        }
160    }
161}
162
163/// Provides cancellation and status inspection for a drag transfer.
164///
165/// A handle can outlive the initial drag-and-drop call because the receiver
166/// may continue transferring file contents asynchronously.
167pub struct TransferHandle {
168    completion: Arc<TransferCompletion>,
169    cancellation: CancellationToken,
170}
171
172impl TransferHandle {
173    pub(crate) fn new(completion: Arc<TransferCompletion>, token: CancellationToken) -> Self {
174        Self {
175            completion,
176            cancellation: token,
177        }
178    }
179
180    /// Returns the latest recorded transfer status without waiting.
181    ///
182    /// For asynchronous receivers, a drop is not the same as successful file
183    /// delivery. Interpret [`TransferStatus::Dropped`] accordingly.
184    pub fn status(&self) -> TransferStatus {
185        self.completion.status()
186    }
187
188    /// Requests cooperative cancellation of this transfer.
189    ///
190    /// The method does not wait for completion and cannot interrupt an already
191    /// blocking source read immediately.
192    pub fn cancel(&self) {
193        self.cancellation.cancel()
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use std::sync::Arc;
201
202    #[test]
203    fn initial_status_is_pending() {
204        let completion = Arc::new(TransferCompletion::new());
205        let handle = TransferHandle::new(completion, CancellationToken::new());
206
207        assert_eq!(handle.status(), TransferStatus::Pending);
208    }
209
210    #[test]
211    fn completion_updates_handle_status() {
212        let completion = Arc::new(TransferCompletion::new());
213        let handle = TransferHandle::new(Arc::clone(&completion), CancellationToken::new());
214
215        _ = completion.record_if_pending(TransferStatus::Dropped);
216
217        assert_eq!(handle.status(), TransferStatus::Dropped);
218    }
219
220    #[test]
221    fn completed_status_cannot_be_overwritten() {
222        let completion = TransferCompletion::new();
223
224        _ = completion.record_if_pending(TransferStatus::Cancelled);
225        _ = completion.record_if_pending(TransferStatus::Dropped);
226
227        assert_eq!(completion.status(), TransferStatus::Cancelled);
228    }
229
230    #[test]
231    fn completion_is_shared_across_threads() {
232        let completion = Arc::new(TransferCompletion::new());
233        let handle = TransferHandle::new(Arc::clone(&completion), CancellationToken::new());
234
235        let worker = std::thread::spawn(move || {
236            _ = completion.record_if_pending(TransferStatus::Dropped);
237        });
238
239        worker.join().unwrap();
240
241        assert_eq!(handle.status(), TransferStatus::Dropped);
242    }
243
244    #[test]
245    fn request_starts_not_cancelled() {
246        let request = TransferRequest::new(vec![]);
247
248        assert!(!request.cancellation_token().is_cancelled());
249    }
250
251    #[test]
252    fn request_preserves_cancellation_after_move() {
253        let request = TransferRequest::new(vec![]);
254        let token = request.cancellation_token();
255
256        let TransferRequest {
257            files,
258            cancellation,
259        } = request;
260
261        assert!(files.is_empty());
262        assert!(!cancellation.is_cancelled());
263
264        token.cancel();
265
266        assert!(cancellation.is_cancelled());
267    }
268
269    #[test]
270    fn request_can_be_created_from_files() {
271        let files: Vec<VirtualFile> = vec![];
272
273        let request: TransferRequest = files.into();
274
275        assert!(request.files.is_empty());
276        assert!(!request.cancellation_token().is_cancelled());
277    }
278}