fs_more/file/
copy.rs

1use std::{
2    io::{BufReader, BufWriter, Write},
3    path::Path,
4};
5
6use_enabled_fs_module!();
7
8use super::{
9    progress::{FileProgress, ProgressWriter},
10    validate_destination_file_path,
11    validate_source_file_path,
12    CollidingFileBehaviour,
13    DestinationValidationAction,
14    ValidatedDestinationFilePath,
15    ValidatedSourceFilePath,
16};
17use crate::{
18    error::FileError,
19    DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
20    DEFAULT_READ_BUFFER_SIZE,
21    DEFAULT_WRITE_BUFFER_SIZE,
22};
23
24
25
26/// Options that influence the [`copy_file`] function.
27#[derive(Clone, Copy, PartialEq, Eq, Debug)]
28pub struct FileCopyOptions {
29    /// How to behave when the destination file already exists.
30    pub colliding_file_behaviour: CollidingFileBehaviour,
31}
32
33
34#[allow(clippy::derivable_impls)]
35impl Default for FileCopyOptions {
36    fn default() -> Self {
37        Self {
38            colliding_file_behaviour: CollidingFileBehaviour::Abort,
39        }
40    }
41}
42
43
44/// Results of a successful file copy operation.
45///
46/// Returned from: [`copy_file`] and [`copy_file_with_progress`].
47#[derive(Clone, Copy, PartialEq, Eq, Debug)]
48pub enum FileCopyFinished {
49    /// The destination file did not exist prior to the operation.
50    /// The file was freshly created and written to.
51    Created {
52        /// Number of bytes written to the file.
53        bytes_copied: u64,
54    },
55
56    /// The destination file already existed, and was overwritten by the copy operation.
57    Overwritten {
58        /// Number of bytes written to the file.
59        bytes_copied: u64,
60    },
61
62    /// The destination file already existed, and the copy operation was skipped.
63    ///
64    /// This can only be returned when existing destination file behaviour
65    /// is set to [`CollidingFileBehaviour::Skip`].
66    ///
67    ///
68    /// [`options.colliding_file_behaviour`]: FileCopyOptions::colliding_file_behaviour
69    Skipped,
70}
71
72
73
74/// Copies a single file from the source to the destination path.
75///
76/// The source file path must be an existing file, or a symlink to one.
77/// The destination path must be a *file* path, and must not point to a directory.
78///
79///
80/// # Symbolic links
81/// Symbolic links are not preserved.
82///
83/// This means the following: if `source_file_path` leads to a symbolic link that points to a file,
84/// the contents of the file at the symlink target will be copied to `destination_file_path`.
85///
86/// This matches the behaviour of `cp` without `--no-dereference` (`-P`) on Unix[^unix-cp].
87///
88///
89///
90/// # Options
91/// See [`FileCopyOptions`] for available file copying options.
92///
93///
94/// # Return value
95/// If the copy succeeds, the function returns [`FileCopyFinished`],
96/// which contains information about whether the file was created,
97/// overwritten or skipped. The struct includes the number of bytes copied,
98/// if relevant.
99///
100///
101/// # Errors
102/// If the file cannot be copied to the destination, a [`FileError`] is returned;
103/// see its documentation for more details.
104/// Here is a non-exhaustive list of error causes:
105/// - If the source path has issues (does not exist, does not have the correct permissions, etc.), one of
106///   [`SourceFileNotFound`], [`SourcePathNotAFile`], or [`UnableToAccessSourceFile`]
107///   variants will be returned.
108/// - If the destination already exists, and [`options.colliding_file_behaviour`]
109///   is set to [`CollidingFileBehaviour::Abort`], then a [`DestinationPathAlreadyExists`]
110///   will be returned.
111/// - If the source and destination paths are canonically actually the same file,
112///   then copying will be aborted with [`SourceAndDestinationAreTheSame`].
113/// - If the destination path has other issues (is a directory, does not have the correct permissions, etc.),
114///   [`UnableToAccessDestinationFile`] will be returned.
115///
116/// There do exist other failure points, mostly due to unavoidable
117/// [time-of-check time-of-use](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)
118/// issues and other potential IO errors that can prop up.
119/// These errors are grouped under the [`OtherIoError`] variant.
120///
121///
122/// <br>
123///
124/// #### See also
125/// If you are looking for a file copying function that reports progress,
126/// see [`copy_file_with_progress`].
127///
128///
129/// <br>
130///
131/// <details>
132/// <summary><h4>Implementation details</h4></summary>
133///
134/// *This section describes internal implementations details.
135/// They should not be relied on, because they are informative
136/// and may change in the future.*
137///
138/// <br>
139///
140/// This function currently delegates file IO to [`std::fs::copy`],
141/// or [`fs_err::copy`](https://docs.rs/fs-err/latest/fs_err/fn.copy.html)
142/// if the `fs-err` feature flag is enabled.
143///
144/// </details>
145///
146///
147/// [`options.colliding_file_behaviour`]: FileCopyOptions::colliding_file_behaviour
148/// [`SourceFileNotFound`]: FileError::SourceFileNotFound
149/// [`SourcePathNotAFile`]: FileError::SourcePathNotAFile
150/// [`UnableToAccessSourceFile`]: FileError::UnableToAccessSourceFile
151/// [`DestinationPathAlreadyExists`]: FileError::DestinationPathAlreadyExists
152/// [`UnableToAccessDestinationFile`]: FileError::UnableToAccessDestinationFile
153/// [`SourceAndDestinationAreTheSame`]: FileError::SourceAndDestinationAreTheSame
154/// [`OtherIoError`]: FileError::OtherIoError
155/// [^unix-cp]: Source for coreutils' `cp` is available
156///     [here](https://github.com/coreutils/coreutils/blob/ccf47cad93bc0b85da0401b0a9d4b652e4c930e4/src/cp.c).
157pub fn copy_file<S, D>(
158    source_file_path: S,
159    destination_file_path: D,
160    options: FileCopyOptions,
161) -> Result<FileCopyFinished, FileError>
162where
163    S: AsRef<Path>,
164    D: AsRef<Path>,
165{
166    let source_file_path = source_file_path.as_ref();
167    let destination_file_path = destination_file_path.as_ref();
168
169
170    let validated_source_file_path = validate_source_file_path(source_file_path)?;
171
172    let ValidatedDestinationFilePath {
173        destination_file_path,
174        exists: destination_file_exists,
175    } = match validate_destination_file_path(
176        &validated_source_file_path,
177        destination_file_path,
178        options.colliding_file_behaviour,
179    )? {
180        DestinationValidationAction::Continue(validated_path) => validated_path,
181        DestinationValidationAction::SkipCopyOrMove => {
182            return Ok(FileCopyFinished::Skipped);
183        }
184    };
185
186    let ValidatedSourceFilePath {
187        source_file_path, ..
188    } = validated_source_file_path;
189
190
191    // All checks have passed, pass the copying onto Rust's standard library.
192    // Note that a time-of-check time-of-use errors are certainly possible
193    // (hence [`FileError::OtherIoError`], though there may be other reasons for it as well).
194
195    let bytes_copied = fs::copy(source_file_path, destination_file_path)
196        .map_err(|error| FileError::OtherIoError { error })?;
197
198
199
200    match destination_file_exists {
201        true => Ok(FileCopyFinished::Overwritten { bytes_copied }),
202        false => Ok(FileCopyFinished::Created { bytes_copied }),
203    }
204}
205
206
207
208/// Options that influence the [`copy_file_with_progress`] function.
209#[derive(Clone, Copy, PartialEq, Eq, Debug)]
210pub struct FileCopyWithProgressOptions {
211    /// How to behave when the destination file already exists.
212    pub colliding_file_behaviour: CollidingFileBehaviour,
213
214    /// Internal buffer size used for reading the source file.
215    ///
216    /// Defaults to 64 KiB.
217    pub read_buffer_size: usize,
218
219    /// Internal buffer size used for writing to the destination file.
220    ///
221    /// Defaults to 64 KiB.
222    pub write_buffer_size: usize,
223
224    /// The smallest number of bytes copied between two consecutive progress reports.
225    ///
226    /// Increase this value to make progress reports less frequent,
227    /// and decrease it to make them more frequent. Keep in mind that
228    /// decreasing the interval will likely come at some performance cost,
229    /// depending on your progress handling closure.
230    ///
231    /// *Note that this is the minimum interval.* The actual reporting interval may be larger!
232    /// Consult [`copy_file_with_progress`] documentation for more details.
233    ///
234    /// Defaults to 512 KiB.
235    pub progress_update_byte_interval: u64,
236}
237
238impl Default for FileCopyWithProgressOptions {
239    /// Constructs relatively safe defaults for copying a file:
240    /// - aborts if there is an existing destination file ([`CollidingFileBehaviour::Abort`]),
241    /// - sets buffer size for reading and writing to 64 KiB, and
242    /// - sets the progress update closure call interval to 512 KiB.
243    fn default() -> Self {
244        Self {
245            colliding_file_behaviour: CollidingFileBehaviour::Abort,
246            read_buffer_size: DEFAULT_READ_BUFFER_SIZE,
247            write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
248            progress_update_byte_interval: DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
249        }
250    }
251}
252
253
254/// Copies the specified file from the source to the destination using the provided options
255/// and progress reporting closure.
256///
257/// This is done by opening two file handles (one for reading, another for writing),
258/// wrapping them in buffered readers and writers, plus our progress tracker intermediary,
259/// and then finally using the [`std::io::copy`] function to copy the entire file.
260///
261///
262/// # Invariants
263/// **Be warned:** no path validation or other checks are performed before copying.
264/// It is fully up to the caller to use e.g. [`validate_source_file_path`] +
265/// [`validate_destination_file_path`], before passing the validated paths to this function.
266pub(crate) fn copy_file_with_progress_unchecked<F>(
267    source_file_path: &Path,
268    destination_file_path: &Path,
269    options: FileCopyWithProgressOptions,
270    progress_handler: F,
271) -> Result<u64, FileError>
272where
273    F: FnMut(&FileProgress),
274{
275    let bytes_total = fs::metadata(source_file_path)
276        .map_err(|error| FileError::OtherIoError { error })?
277        .len();
278
279    // Open a file for reading and a file for writing,
280    // wrap them in buffers and progress monitors, then copy the file.
281    let input_file = fs::OpenOptions::new()
282        .read(true)
283        .open(source_file_path)
284        .map_err(|error| FileError::OtherIoError { error })?;
285
286    let mut input_file_buffered = BufReader::with_capacity(options.read_buffer_size, input_file);
287
288
289    let output_file = fs::OpenOptions::new()
290        .create(true)
291        .write(true)
292        .truncate(true)
293        .open(destination_file_path)
294        .map_err(|error| FileError::OtherIoError { error })?;
295
296    let output_file_progress_monitored = ProgressWriter::new(
297        output_file,
298        progress_handler,
299        options.progress_update_byte_interval,
300        bytes_total,
301    );
302    let mut output_file_buffered =
303        BufWriter::with_capacity(options.write_buffer_size, output_file_progress_monitored);
304
305
306
307    let final_number_of_bytes_copied =
308        std::io::copy(&mut input_file_buffered, &mut output_file_buffered)
309            .map_err(|error| FileError::OtherIoError { error })?;
310
311
312
313    // Unwrap writers and flush any remaining output.
314    let (mut output_file, mut copy_progress, mut progress_handler) = output_file_buffered
315        .into_inner()
316        .map_err(|error| FileError::OtherIoError {
317            error: error.into_error(),
318        })?
319        .into_inner();
320
321    output_file
322        .flush()
323        .map_err(|error| FileError::OtherIoError { error })?;
324
325    // Perform one last progress update.
326    copy_progress.bytes_finished = final_number_of_bytes_copied;
327    progress_handler(&copy_progress);
328
329    Ok(final_number_of_bytes_copied)
330}
331
332
333
334/// Copies a single file from the source to the destination path, with progress reporting.
335///
336/// The source file path must be an existing file, or a symlink to one.
337/// The destination path must be a *file* path, and must not point to a directory.
338///
339///
340/// # Symbolic links
341/// Symbolic links are not preserved.
342///
343/// This means the following: if `source_file_path` leads to a symbolic link that points to a file,
344/// the contents of the file at the symlink target will be copied to `destination_file_path`.
345///
346/// This matches the behaviour of `cp` without `--no-dereference` (`-P`) on Unix[^unix-cp].
347///
348///
349/// # Options
350/// See [`FileCopyWithProgressOptions`] for available file copying options.
351///
352///
353/// # Return value
354/// If the copy succeeds, the function returns [`FileCopyFinished`],
355/// which contains information about whether the file was created,
356/// overwritten or skipped. The struct includes the number of bytes copied,
357/// if relevant.
358///
359///
360/// # Progress reporting
361/// This function allows you to receive progress reports by passing
362/// a `progress_handler` closure. It will be called with
363/// a reference to [`FileProgress`] regularly.
364///
365/// You can control the progress reporting frequency by setting the
366/// [`options.progress_update_byte_interval`] option to a sufficiently small or large value,
367/// but note that smaller intervals are likely to have an impact on performance.
368/// The value of this option is the minimum number of bytes written to a file between
369/// two calls to the provided `progress_handler`.
370///
371/// This function does not guarantee a precise number of progress reports per file size
372/// and progress reporting interval. However, it does guarantee at least one progress report:
373/// the final one, which happens when the file has been completely copied.
374/// In most cases though, the number of calls to the closure will be near the expected number,
375/// which is `file_size / progress_update_byte_interval`.
376///
377///
378/// # Errors
379/// If the file cannot be copied to the destination, a [`FileError`] is returned;
380/// see its documentation for more details.
381/// Here is a non-exhaustive list of error causes:
382/// - If the source path has issues (does not exist, does not have the correct permissions, etc.), one of
383///   [`SourceFileNotFound`], [`SourcePathNotAFile`], or [`UnableToAccessSourceFile`]
384///   variants will be returned.
385/// - If the destination already exists, and [`options.colliding_file_behaviour`]
386///   is set to [`CollidingFileBehaviour::Abort`], then a [`DestinationPathAlreadyExists`]
387///   will be returned.
388/// - If the source and destination paths are canonically actually the same file,
389///   then copying will be aborted with [`SourceAndDestinationAreTheSame`].
390/// - If the destination path has other issues (is a directory, does not have the correct permissions, etc.),
391///   [`UnableToAccessDestinationFile`] will be returned.
392///
393/// There do exist other failure points, mostly due to unavoidable
394/// [time-of-check time-of-use](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)
395/// issues and other potential IO errors that can prop up.
396/// These errors are grouped under the [`OtherIoError`] variant.
397///
398///
399/// <br>
400///
401/// #### See also
402/// If you are looking for a file copying function that does not report progress,
403/// see [`copy_file`].
404///
405///
406/// <br>
407///
408/// <details>
409/// <summary><h4>Implementation details</h4></summary>
410///
411/// *This section describes internal implementations details.
412/// They should not be relied on, because they are informative
413/// and may change in the future.*
414///
415/// <br>
416///
417/// Unlike [`copy_file`], this function handles copying itself by opening file handles for
418/// both the source and destination file, then buffering reads and writes.
419///
420/// </details>
421///
422///
423/// [`options.progress_update_byte_interval`]: FileCopyWithProgressOptions::progress_update_byte_interval
424/// [`options.colliding_file_behaviour`]: FileCopyOptions::colliding_file_behaviour
425/// [`SourceFileNotFound`]: FileError::SourceFileNotFound
426/// [`SourcePathNotAFile`]: FileError::SourcePathNotAFile
427/// [`UnableToAccessSourceFile`]: FileError::UnableToAccessSourceFile
428/// [`DestinationPathAlreadyExists`]: FileError::DestinationPathAlreadyExists
429/// [`UnableToAccessDestinationFile`]: FileError::UnableToAccessDestinationFile
430/// [`SourceAndDestinationAreTheSame`]: FileError::SourceAndDestinationAreTheSame
431/// [`OtherIoError`]: FileError::OtherIoError
432/// [^unix-cp]: Source for coreutils' `cp` is available
433///     [here](https://github.com/coreutils/coreutils/blob/ccf47cad93bc0b85da0401b0a9d4b652e4c930e4/src/cp.c).
434pub fn copy_file_with_progress<P, T, F>(
435    source_file_path: P,
436    destination_file_path: T,
437    options: FileCopyWithProgressOptions,
438    progress_handler: F,
439) -> Result<FileCopyFinished, FileError>
440where
441    P: AsRef<Path>,
442    T: AsRef<Path>,
443    F: FnMut(&FileProgress),
444{
445    let source_file_path = source_file_path.as_ref();
446    let destination_file_path = destination_file_path.as_ref();
447
448
449    let validated_source_file_path = validate_source_file_path(source_file_path)?;
450
451    let ValidatedDestinationFilePath {
452        destination_file_path,
453        exists: destination_file_exists,
454    } = match validate_destination_file_path(
455        &validated_source_file_path,
456        destination_file_path,
457        options.colliding_file_behaviour,
458    )? {
459        DestinationValidationAction::Continue(validated_path) => validated_path,
460        DestinationValidationAction::SkipCopyOrMove => {
461            return Ok(FileCopyFinished::Skipped);
462        }
463    };
464
465    let ValidatedSourceFilePath {
466        source_file_path, ..
467    } = validated_source_file_path;
468
469
470    // All checks have passed, we must now copy the file.
471    // Unlike in the `copy_file` function, we must copy the file ourselves, as we
472    // can't report progress otherwise. This is delegated to the `copy_file_with_progress_unchecked`
473    // function which is used in other parts of the library as well.
474
475    let bytes_copied = copy_file_with_progress_unchecked(
476        &source_file_path,
477        &destination_file_path,
478        options,
479        progress_handler,
480    )?;
481
482    match destination_file_exists {
483        true => Ok(FileCopyFinished::Overwritten { bytes_copied }),
484        false => Ok(FileCopyFinished::Created { bytes_copied }),
485    }
486}