winapi_easy/
fs.rs

1//! Filesystem functionality.
2
3use std::ffi::c_void;
4use std::path::Path;
5use std::{
6    io,
7    ptr,
8};
9
10use num_enum::IntoPrimitive;
11use windows::Win32::Foundation::HANDLE;
12use windows::Win32::Storage::FileSystem::{
13    COPY_FILE_COPY_SYMLINK,
14    COPY_FILE_FAIL_IF_EXISTS,
15    COPYPROGRESSROUTINE_PROGRESS,
16    CopyFileExW,
17    LPPROGRESS_ROUTINE,
18    LPPROGRESS_ROUTINE_CALLBACK_REASON,
19    MOVEFILE_COPY_ALLOWED,
20    MOVEFILE_WRITE_THROUGH,
21    MoveFileWithProgressW,
22    PROGRESS_CANCEL,
23    PROGRESS_CONTINUE,
24    PROGRESS_QUIET,
25    PROGRESS_STOP,
26};
27
28use crate::internal::catch_unwind_and_abort;
29use crate::string::{
30    ZeroTerminatedWideString,
31    max_path_extend,
32};
33
34/// Optional function called by Windows for every transferred chunk of a file.
35///
36/// This is used in [`PathExt::copy_file_to`] and [`PathExt::move_to`]
37/// to receive progress notifications and to potentially pause or cancel the operation.
38///
39/// Use [`Default::default`] to disable.
40#[derive(Clone, Debug)]
41#[repr(transparent)]
42pub struct ProgressCallback<F>(Option<F>);
43
44impl<F> ProgressCallback<F>
45where
46    F: FnMut(ProgressStatus) -> ProgressRetVal,
47{
48    pub fn new(value: F) -> Self {
49        // No `From` impl since that has problems with type inference when declaring the closure
50        ProgressCallback(Some(value))
51    }
52
53    fn typed_raw_progress_callback(&self) -> LPPROGRESS_ROUTINE {
54        if self.0.is_some() {
55            Some(transfer_internal_callback::<F> as _)
56        } else {
57            None
58        }
59    }
60
61    fn as_raw_lpdata(&mut self) -> Option<*const c_void> {
62        self.0
63            .as_mut()
64            .map(|callback| ptr::from_mut::<F>(callback).cast_const().cast::<c_void>())
65    }
66}
67
68impl Default for ProgressCallback<fn(ProgressStatus) -> ProgressRetVal> {
69    fn default() -> Self {
70        Self(None)
71    }
72}
73
74/// Progress status used in [`ProgressCallback`].
75#[derive(Copy, Clone, PartialEq, Eq, Debug)]
76pub struct ProgressStatus {
77    /// Total size in bytes of the file being transferred.
78    pub total_file_bytes: u64,
79    /// Total bytes completed in the current file transfer.
80    pub total_transferred_bytes: u64,
81}
82
83/// Return value used in [`ProgressCallback`] to control the ongoing file transfer.
84#[derive(IntoPrimitive, Copy, Clone, Eq, PartialEq, Default, Debug)]
85#[repr(u32)]
86pub enum ProgressRetVal {
87    /// Continue the operation.
88    #[default]
89    Continue = PROGRESS_CONTINUE.0,
90    /// Stop the operation with the option of continuing later.
91    Stop = PROGRESS_STOP.0,
92    /// Cancel the operation.
93    Cancel = PROGRESS_CANCEL.0,
94    /// Continue but stop calling the user callback.
95    Quiet = PROGRESS_QUIET.0,
96}
97
98impl From<ProgressRetVal> for COPYPROGRESSROUTINE_PROGRESS {
99    fn from(value: ProgressRetVal) -> Self {
100        COPYPROGRESSROUTINE_PROGRESS(u32::from(value))
101    }
102}
103
104/// Additional methods on [`Path`] using Windows-specific functionality.
105pub trait PathExt: AsRef<Path> {
106    /// Copies a file.
107    ///
108    /// - Will copy symlinks themselves, not their targets.
109    /// - Will block until the operation is complete.
110    /// - Will fail if the target path already exists.
111    /// - Supports file names longer than `MAX_PATH` characters.
112    ///
113    /// Progress notifications can be enabled using a [`ProgressCallback`].
114    /// Use [`Default::default`] to disable.
115    fn copy_file_to<Q, F>(
116        &self,
117        new_path: Q,
118        mut progress_callback: ProgressCallback<F>,
119    ) -> io::Result<()>
120    where
121        Q: AsRef<Path>,
122        F: FnMut(ProgressStatus) -> ProgressRetVal,
123    {
124        let source =
125            ZeroTerminatedWideString::from_os_str(max_path_extend(self.as_ref().as_os_str()));
126        let target =
127            ZeroTerminatedWideString::from_os_str(max_path_extend(new_path.as_ref().as_os_str()));
128        unsafe {
129            CopyFileExW(
130                source.as_raw_pcwstr(),
131                target.as_raw_pcwstr(),
132                progress_callback.typed_raw_progress_callback(),
133                progress_callback.as_raw_lpdata(),
134                None,
135                COPY_FILE_COPY_SYMLINK | COPY_FILE_FAIL_IF_EXISTS,
136            )?;
137        }
138        Ok(())
139    }
140
141    /// Moves a file or directory within a volume or a file between volumes.
142    ///
143    /// - The operation is equivalent to a rename if the new path is on the same volume.
144    /// - Only files can be moved between volumes, not directories.
145    /// - Will move symlinks themselves, not their targets.
146    /// - Symlinks can be moved within the same volume (renamed) without extended permission.
147    /// - Will block until the operation is complete.
148    /// - Will fail if the target path already exists.
149    /// - Supports file names longer than `MAX_PATH` characters.
150    ///
151    /// Progress notifications can be enabled using a [`ProgressCallback`].
152    /// Use [`Default::default`] to disable.
153    fn move_to<Q, F>(
154        &self,
155        new_path: Q,
156        mut progress_callback: ProgressCallback<F>,
157    ) -> io::Result<()>
158    where
159        Q: AsRef<Path>,
160        F: FnMut(ProgressStatus) -> ProgressRetVal,
161    {
162        let source =
163            ZeroTerminatedWideString::from_os_str(max_path_extend(self.as_ref().as_os_str()));
164        let target =
165            ZeroTerminatedWideString::from_os_str(max_path_extend(new_path.as_ref().as_os_str()));
166        unsafe {
167            MoveFileWithProgressW(
168                source.as_raw_pcwstr(),
169                target.as_raw_pcwstr(),
170                progress_callback.typed_raw_progress_callback(),
171                progress_callback.as_raw_lpdata(),
172                MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH,
173            )?;
174        }
175        Ok(())
176    }
177}
178
179impl<T: AsRef<Path>> PathExt for T {}
180
181unsafe extern "system" fn transfer_internal_callback<F>(
182    totalfilesize: i64,
183    totalbytestransferred: i64,
184    _streamsize: i64,
185    _streambytestransferred: i64,
186    _dwstreamnumber: u32,
187    _dwcallbackreason: LPPROGRESS_ROUTINE_CALLBACK_REASON,
188    _hsourcefile: HANDLE,
189    _hdestinationfile: HANDLE,
190    lpdata: *const c_void,
191) -> COPYPROGRESSROUTINE_PROGRESS
192where
193    F: FnMut(ProgressStatus) -> ProgressRetVal,
194{
195    let call = move || {
196        let user_callback: &mut F = unsafe { &mut *(lpdata.cast_mut().cast::<F>()) };
197        user_callback(ProgressStatus {
198            total_file_bytes: totalfilesize.try_into().unwrap_or_else(|_| unreachable!()),
199            total_transferred_bytes: totalbytestransferred
200                .try_into()
201                .unwrap_or_else(|_| unreachable!()),
202        })
203    };
204    catch_unwind_and_abort(call).into()
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn check_transfer_internal_callback() -> io::Result<()> {
213        let target_progress_status = ProgressStatus {
214            total_file_bytes: 1,
215            total_transferred_bytes: 1,
216        };
217        let progress_ret_val = ProgressRetVal::Stop;
218        let mut progress_callback = ProgressCallback::new(|progress_status| {
219            assert_eq!(progress_status, target_progress_status);
220            progress_ret_val
221        });
222        let raw_progress_callback = progress_callback
223            .typed_raw_progress_callback()
224            .unwrap_or_else(|| unreachable!());
225        let raw_call_result = unsafe {
226            raw_progress_callback(
227                target_progress_status
228                    .total_file_bytes
229                    .try_into()
230                    .unwrap_or_else(|_| unreachable!()),
231                target_progress_status
232                    .total_transferred_bytes
233                    .try_into()
234                    .unwrap_or_else(|_| unreachable!()),
235                Default::default(),
236                Default::default(),
237                Default::default(),
238                Default::default(),
239                Default::default(),
240                Default::default(),
241                progress_callback
242                    .as_raw_lpdata()
243                    .unwrap_or_else(|| unreachable!()),
244            )
245        };
246        assert_eq!(raw_call_result, progress_ret_val.into());
247        Ok(())
248    }
249}