fs-more 0.8.1

Convenient file and directory operations with progress reporting built on top of std::fs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
use std::{
    io::{BufReader, BufWriter, Write},
    path::Path,
};

use_enabled_fs_module!();

use super::{
    progress::{FileProgress, ProgressWriter},
    validate_destination_file_path,
    validate_source_file_path,
    CollidingFileBehaviour,
    DestinationValidationAction,
    ValidatedDestinationFilePath,
    ValidatedSourceFilePath,
};
use crate::{
    error::FileError,
    DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
    DEFAULT_READ_BUFFER_SIZE,
    DEFAULT_WRITE_BUFFER_SIZE,
};



/// Options that influence the [`copy_file`] function.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct FileCopyOptions {
    /// How to behave when the destination file already exists.
    pub colliding_file_behaviour: CollidingFileBehaviour,
}


#[allow(clippy::derivable_impls)]
impl Default for FileCopyOptions {
    fn default() -> Self {
        Self {
            colliding_file_behaviour: CollidingFileBehaviour::Abort,
        }
    }
}


/// Results of a successful file copy operation.
///
/// Returned from: [`copy_file`] and [`copy_file_with_progress`].
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FileCopyFinished {
    /// The destination file did not exist prior to the operation.
    /// The file was freshly created and written to.
    Created {
        /// Number of bytes written to the file.
        bytes_copied: u64,
    },

    /// The destination file already existed, and was overwritten by the copy operation.
    Overwritten {
        /// Number of bytes written to the file.
        bytes_copied: u64,
    },

    /// The destination file already existed, and the copy operation was skipped.
    ///
    /// This can only be returned when existing destination file behaviour
    /// is set to [`CollidingFileBehaviour::Skip`].
    ///
    ///
    /// [`options.colliding_file_behaviour`]: FileCopyOptions::colliding_file_behaviour
    Skipped,
}



/// Copies a single file from the source to the destination path.
///
/// The source file path must be an existing file, or a symlink to one.
/// The destination path must be a *file* path, and must not point to a directory.
///
///
/// # Symbolic links
/// Symbolic links are not preserved.
///
/// This means the following: if `source_file_path` leads to a symbolic link that points to a file,
/// the contents of the file at the symlink target will be copied to `destination_file_path`.
///
/// This matches the behaviour of `cp` without `--no-dereference` (`-P`) on Unix[^unix-cp].
///
///
///
/// # Options
/// See [`FileCopyOptions`] for available file copying options.
///
///
/// # Return value
/// If the copy succeeds, the function returns [`FileCopyFinished`],
/// which contains information about whether the file was created,
/// overwritten or skipped. The struct includes the number of bytes copied,
/// if relevant.
///
///
/// # Errors
/// If the file cannot be copied to the destination, a [`FileError`] is returned;
/// see its documentation for more details.
/// Here is a non-exhaustive list of error causes:
/// - If the source path has issues (does not exist, does not have the correct permissions, etc.), one of
///   [`SourceFileNotFound`], [`SourcePathNotAFile`], or [`UnableToAccessSourceFile`]
///   variants will be returned.
/// - If the destination already exists, and [`options.colliding_file_behaviour`]
///   is set to [`CollidingFileBehaviour::Abort`], then a [`DestinationPathAlreadyExists`]
///   will be returned.
/// - If the source and destination paths are canonically actually the same file,
///   then copying will be aborted with [`SourceAndDestinationAreTheSame`].
/// - If the destination path has other issues (is a directory, does not have the correct permissions, etc.),
///   [`UnableToAccessDestinationFile`] will be returned.
///
/// There do exist other failure points, mostly due to unavoidable
/// [time-of-check time-of-use](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)
/// issues and other potential IO errors that can prop up.
/// These errors are grouped under the [`OtherIoError`] variant.
///
///
/// <br>
///
/// #### See also
/// If you are looking for a file copying function that reports progress,
/// see [`copy_file_with_progress`].
///
///
/// <br>
///
/// <details>
/// <summary><h4>Implementation details</h4></summary>
///
/// *This section describes internal implementations details.
/// They should not be relied on, because they are informative
/// and may change in the future.*
///
/// <br>
///
/// This function currently delegates file IO to [`std::fs::copy`],
/// or [`fs_err::copy`](https://docs.rs/fs-err/latest/fs_err/fn.copy.html)
/// if the `fs-err` feature flag is enabled.
///
/// </details>
///
///
/// [`options.colliding_file_behaviour`]: FileCopyOptions::colliding_file_behaviour
/// [`SourceFileNotFound`]: FileError::SourceFileNotFound
/// [`SourcePathNotAFile`]: FileError::SourcePathNotAFile
/// [`UnableToAccessSourceFile`]: FileError::UnableToAccessSourceFile
/// [`DestinationPathAlreadyExists`]: FileError::DestinationPathAlreadyExists
/// [`UnableToAccessDestinationFile`]: FileError::UnableToAccessDestinationFile
/// [`SourceAndDestinationAreTheSame`]: FileError::SourceAndDestinationAreTheSame
/// [`OtherIoError`]: FileError::OtherIoError
/// [^unix-cp]: Source for coreutils' `cp` is available
///     [here](https://github.com/coreutils/coreutils/blob/ccf47cad93bc0b85da0401b0a9d4b652e4c930e4/src/cp.c).
pub fn copy_file<S, D>(
    source_file_path: S,
    destination_file_path: D,
    options: FileCopyOptions,
) -> Result<FileCopyFinished, FileError>
where
    S: AsRef<Path>,
    D: AsRef<Path>,
{
    let source_file_path = source_file_path.as_ref();
    let destination_file_path = destination_file_path.as_ref();


    let validated_source_file_path = validate_source_file_path(source_file_path)?;

    let ValidatedDestinationFilePath {
        destination_file_path,
        exists: destination_file_exists,
    } = match validate_destination_file_path(
        &validated_source_file_path,
        destination_file_path,
        options.colliding_file_behaviour,
    )? {
        DestinationValidationAction::Continue(validated_path) => validated_path,
        DestinationValidationAction::SkipCopyOrMove => {
            return Ok(FileCopyFinished::Skipped);
        }
    };

    let ValidatedSourceFilePath {
        source_file_path, ..
    } = validated_source_file_path;


    // All checks have passed, pass the copying onto Rust's standard library.
    // Note that a time-of-check time-of-use errors are certainly possible
    // (hence [`FileError::OtherIoError`], though there may be other reasons for it as well).

    let bytes_copied = fs::copy(source_file_path, destination_file_path)
        .map_err(|error| FileError::OtherIoError { error })?;



    match destination_file_exists {
        true => Ok(FileCopyFinished::Overwritten { bytes_copied }),
        false => Ok(FileCopyFinished::Created { bytes_copied }),
    }
}



/// Options that influence the [`copy_file_with_progress`] function.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct FileCopyWithProgressOptions {
    /// How to behave when the destination file already exists.
    pub colliding_file_behaviour: CollidingFileBehaviour,

    /// Internal buffer size used for reading the source file.
    ///
    /// Defaults to 64 KiB.
    pub read_buffer_size: usize,

    /// Internal buffer size used for writing to the destination file.
    ///
    /// Defaults to 64 KiB.
    pub write_buffer_size: usize,

    /// The smallest number of bytes copied between two consecutive progress reports.
    ///
    /// Increase this value to make progress reports less frequent,
    /// and decrease it to make them more frequent. Keep in mind that
    /// decreasing the interval will likely come at some performance cost,
    /// depending on your progress handling closure.
    ///
    /// *Note that this is the minimum interval.* The actual reporting interval may be larger!
    /// Consult [`copy_file_with_progress`] documentation for more details.
    ///
    /// Defaults to 512 KiB.
    pub progress_update_byte_interval: u64,
}

impl Default for FileCopyWithProgressOptions {
    /// Constructs relatively safe defaults for copying a file:
    /// - aborts if there is an existing destination file ([`CollidingFileBehaviour::Abort`]),
    /// - sets buffer size for reading and writing to 64 KiB, and
    /// - sets the progress update closure call interval to 512 KiB.
    fn default() -> Self {
        Self {
            colliding_file_behaviour: CollidingFileBehaviour::Abort,
            read_buffer_size: DEFAULT_READ_BUFFER_SIZE,
            write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
            progress_update_byte_interval: DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
        }
    }
}


/// Copies the specified file from the source to the destination using the provided options
/// and progress reporting closure.
///
/// This is done by opening two file handles (one for reading, another for writing),
/// wrapping them in buffered readers and writers, plus our progress tracker intermediary,
/// and then finally using the [`std::io::copy`] function to copy the entire file.
///
///
/// # Invariants
/// **Be warned:** no path validation or other checks are performed before copying.
/// It is fully up to the caller to use e.g. [`validate_source_file_path`] +
/// [`validate_destination_file_path`], before passing the validated paths to this function.
pub(crate) fn copy_file_with_progress_unchecked<F>(
    source_file_path: &Path,
    destination_file_path: &Path,
    options: FileCopyWithProgressOptions,
    progress_handler: F,
) -> Result<u64, FileError>
where
    F: FnMut(&FileProgress),
{
    let bytes_total = fs::metadata(source_file_path)
        .map_err(|error| FileError::OtherIoError { error })?
        .len();

    // Open a file for reading and a file for writing,
    // wrap them in buffers and progress monitors, then copy the file.
    let input_file = fs::OpenOptions::new()
        .read(true)
        .open(source_file_path)
        .map_err(|error| FileError::OtherIoError { error })?;

    let mut input_file_buffered = BufReader::with_capacity(options.read_buffer_size, input_file);


    let output_file = fs::OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(destination_file_path)
        .map_err(|error| FileError::OtherIoError { error })?;

    let output_file_progress_monitored = ProgressWriter::new(
        output_file,
        progress_handler,
        options.progress_update_byte_interval,
        bytes_total,
    );
    let mut output_file_buffered =
        BufWriter::with_capacity(options.write_buffer_size, output_file_progress_monitored);



    let final_number_of_bytes_copied =
        std::io::copy(&mut input_file_buffered, &mut output_file_buffered)
            .map_err(|error| FileError::OtherIoError { error })?;



    // Unwrap writers and flush any remaining output.
    let (mut output_file, mut copy_progress, mut progress_handler) = output_file_buffered
        .into_inner()
        .map_err(|error| FileError::OtherIoError {
            error: error.into_error(),
        })?
        .into_inner();

    output_file
        .flush()
        .map_err(|error| FileError::OtherIoError { error })?;

    // Perform one last progress update.
    copy_progress.bytes_finished = final_number_of_bytes_copied;
    progress_handler(&copy_progress);

    Ok(final_number_of_bytes_copied)
}



/// Copies a single file from the source to the destination path, with progress reporting.
///
/// The source file path must be an existing file, or a symlink to one.
/// The destination path must be a *file* path, and must not point to a directory.
///
///
/// # Symbolic links
/// Symbolic links are not preserved.
///
/// This means the following: if `source_file_path` leads to a symbolic link that points to a file,
/// the contents of the file at the symlink target will be copied to `destination_file_path`.
///
/// This matches the behaviour of `cp` without `--no-dereference` (`-P`) on Unix[^unix-cp].
///
///
/// # Options
/// See [`FileCopyWithProgressOptions`] for available file copying options.
///
///
/// # Return value
/// If the copy succeeds, the function returns [`FileCopyFinished`],
/// which contains information about whether the file was created,
/// overwritten or skipped. The struct includes the number of bytes copied,
/// if relevant.
///
///
/// # Progress reporting
/// This function allows you to receive progress reports by passing
/// a `progress_handler` closure. It will be called with
/// a reference to [`FileProgress`] regularly.
///
/// You can control the progress reporting frequency by setting the
/// [`options.progress_update_byte_interval`] option to a sufficiently small or large value,
/// but note that smaller intervals are likely to have an impact on performance.
/// The value of this option is the minimum number of bytes written to a file between
/// two calls to the provided `progress_handler`.
///
/// This function does not guarantee a precise number of progress reports per file size
/// and progress reporting interval. However, it does guarantee at least one progress report:
/// the final one, which happens when the file has been completely copied.
/// In most cases though, the number of calls to the closure will be near the expected number,
/// which is `file_size / progress_update_byte_interval`.
///
///
/// # Errors
/// If the file cannot be copied to the destination, a [`FileError`] is returned;
/// see its documentation for more details.
/// Here is a non-exhaustive list of error causes:
/// - If the source path has issues (does not exist, does not have the correct permissions, etc.), one of
///   [`SourceFileNotFound`], [`SourcePathNotAFile`], or [`UnableToAccessSourceFile`]
///   variants will be returned.
/// - If the destination already exists, and [`options.colliding_file_behaviour`]
///   is set to [`CollidingFileBehaviour::Abort`], then a [`DestinationPathAlreadyExists`]
///   will be returned.
/// - If the source and destination paths are canonically actually the same file,
///   then copying will be aborted with [`SourceAndDestinationAreTheSame`].
/// - If the destination path has other issues (is a directory, does not have the correct permissions, etc.),
///   [`UnableToAccessDestinationFile`] will be returned.
///
/// There do exist other failure points, mostly due to unavoidable
/// [time-of-check time-of-use](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)
/// issues and other potential IO errors that can prop up.
/// These errors are grouped under the [`OtherIoError`] variant.
///
///
/// <br>
///
/// #### See also
/// If you are looking for a file copying function that does not report progress,
/// see [`copy_file`].
///
///
/// <br>
///
/// <details>
/// <summary><h4>Implementation details</h4></summary>
///
/// *This section describes internal implementations details.
/// They should not be relied on, because they are informative
/// and may change in the future.*
///
/// <br>
///
/// Unlike [`copy_file`], this function handles copying itself by opening file handles for
/// both the source and destination file, then buffering reads and writes.
///
/// </details>
///
///
/// [`options.progress_update_byte_interval`]: FileCopyWithProgressOptions::progress_update_byte_interval
/// [`options.colliding_file_behaviour`]: FileCopyOptions::colliding_file_behaviour
/// [`SourceFileNotFound`]: FileError::SourceFileNotFound
/// [`SourcePathNotAFile`]: FileError::SourcePathNotAFile
/// [`UnableToAccessSourceFile`]: FileError::UnableToAccessSourceFile
/// [`DestinationPathAlreadyExists`]: FileError::DestinationPathAlreadyExists
/// [`UnableToAccessDestinationFile`]: FileError::UnableToAccessDestinationFile
/// [`SourceAndDestinationAreTheSame`]: FileError::SourceAndDestinationAreTheSame
/// [`OtherIoError`]: FileError::OtherIoError
/// [^unix-cp]: Source for coreutils' `cp` is available
///     [here](https://github.com/coreutils/coreutils/blob/ccf47cad93bc0b85da0401b0a9d4b652e4c930e4/src/cp.c).
pub fn copy_file_with_progress<P, T, F>(
    source_file_path: P,
    destination_file_path: T,
    options: FileCopyWithProgressOptions,
    progress_handler: F,
) -> Result<FileCopyFinished, FileError>
where
    P: AsRef<Path>,
    T: AsRef<Path>,
    F: FnMut(&FileProgress),
{
    let source_file_path = source_file_path.as_ref();
    let destination_file_path = destination_file_path.as_ref();


    let validated_source_file_path = validate_source_file_path(source_file_path)?;

    let ValidatedDestinationFilePath {
        destination_file_path,
        exists: destination_file_exists,
    } = match validate_destination_file_path(
        &validated_source_file_path,
        destination_file_path,
        options.colliding_file_behaviour,
    )? {
        DestinationValidationAction::Continue(validated_path) => validated_path,
        DestinationValidationAction::SkipCopyOrMove => {
            return Ok(FileCopyFinished::Skipped);
        }
    };

    let ValidatedSourceFilePath {
        source_file_path, ..
    } = validated_source_file_path;


    // All checks have passed, we must now copy the file.
    // Unlike in the `copy_file` function, we must copy the file ourselves, as we
    // can't report progress otherwise. This is delegated to the `copy_file_with_progress_unchecked`
    // function which is used in other parts of the library as well.

    let bytes_copied = copy_file_with_progress_unchecked(
        &source_file_path,
        &destination_file_path,
        options,
        progress_handler,
    )?;

    match destination_file_exists {
        true => Ok(FileCopyFinished::Overwritten { bytes_copied }),
        false => Ok(FileCopyFinished::Created { bytes_copied }),
    }
}