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(©_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}