fs_more/file/
mod.rs

1//! File copying, moving, sizing and removal operations.
2//! *Includes progress monitoring variants.*
3//!
4//! <br>
5//!
6//! ##### Feature Overview
7//!
8//! | | <span style="font-weight:normal"><i>configured by</i></span> | <span style="font-weight:normal"><i>returns</i></span> |
9//! |-----------------------------|---------------------------------|:--------------------:|
10//! | [`copy_file`]               | [`FileCopyOptions`]             | [`FileCopyFinished`] <br><sup style="text-align: right">(or [`FileError`])</sup> |
11//! | [`copy_file_with_progress`] | [`FileCopyWithProgressOptions`] | [`FileCopyFinished`] <br><sup style="text-align: right">(or [`FileError`])</sup> |
12//! | [`move_file`]               | [`FileMoveOptions`]             | [`FileMoveFinished`] <br><sup style="text-align: right">(or [`FileError`])</sup> |
13//! | [`move_file_with_progress`] | [`FileMoveWithProgressOptions`] | [`FileMoveFinished`] <br><sup style="text-align: right">(or [`FileError`])</sup> |
14//! | [`remove_file`]             |                                 | `()` <br><sup style="text-align: right">(or [`FileRemoveError`])</sup> |
15//! | [`file_size_in_bytes`]      |                                 | [`u64`] <br><sup style="text-align: right">(or [`FileSizeError`])</sup> |
16//!
17//!
18//! [`FileError`]: crate::error::FileError
19//! [`FileRemoveError`]: crate::error::FileRemoveError
20//! [`FileSizeError`]: crate::error::FileSizeError
21
22use std::path::{Path, PathBuf};
23
24use_enabled_fs_module!();
25
26mod copy;
27mod r#move;
28mod progress;
29mod remove;
30mod size;
31
32pub use copy::*;
33pub use progress::*;
34pub use r#move::*;
35pub use remove::*;
36pub use size::*;
37
38use crate::{directory::try_exists_without_follow, error::FileError};
39
40
41/// Controls behaviour for existing files on the destination side
42/// that collide with the one we're trying to copy or move there.
43///
44/// See also: [`FileCopyOptions`] and [`FileMoveOptions`].
45#[derive(Clone, Copy, PartialEq, Eq, Debug)]
46pub enum CollidingFileBehaviour {
47    /// Ensures that an error will be returned from the corresponding function
48    /// when the destination file already exists.
49    Abort,
50
51    /// Ensures that an existing destination file will not be overwritten
52    /// by the corresponding copy or move operation.
53    ///
54    /// However, the function will skip the file silently; no error will be returned.
55    Skip,
56
57    /// Ensures that an existing destination file *can* be overwritten
58    /// by the corresponding copying or moving function.
59    Overwrite,
60}
61
62
63
64/// A set of paths and auxiliary information about a source file path.
65pub(crate) struct ValidatedSourceFilePath {
66    /// Canonical source file path.
67    ///
68    /// If the original file path was a symlink leading to some target file,
69    /// this path points to that target file.
70    pub(crate) source_file_path: PathBuf,
71
72    /// Indicates whether the original source file path (before canonicalization)
73    /// was a symlink to a file.
74    ///
75    /// **This flag is relevant only if the operation happens to be moving a file.**
76    ///
77    /// This flag is be `true` when the original `source_file_path` was a symlink to a file and we
78    /// canonicalized the path in [`validate_source_file_path`].
79    ///
80    /// This means the path in this struct no longer points to the symlink,
81    /// but to the file that link itself points to. In that case, we must not move the file,
82    /// but copy it and then delete the original symlink the user wanted to move.
83    pub(crate) original_was_symlink_to_file: bool,
84}
85
86
87/// Given a source file path, validate that it exists on the file system and is truly a file.
88///
89/// If the given path is a symlink to a file, the returned path will be a resolved (canonical) one,
90/// i.e. pointing to the real file.
91fn validate_source_file_path(
92    source_file_path: &Path,
93) -> Result<ValidatedSourceFilePath, FileError> {
94    // Ensure the source file path exists. We use `try_exists`
95    // instead of `exists` to catch permission and other IO errors
96    // as distinct from the `FileError::NotFound` error.
97
98    let source_file_exists = match try_exists_without_follow(source_file_path) {
99        Ok(exists) => exists,
100        Err(error) => {
101            return Err(FileError::UnableToAccessSourceFile {
102                path: source_file_path.to_path_buf(),
103                error,
104            });
105        }
106    };
107
108
109    if !source_file_exists {
110        return Err(FileError::SourceFileNotFound {
111            path: source_file_path.to_path_buf(),
112        });
113    }
114
115    if !source_file_path.is_file() {
116        return Err(FileError::SourcePathNotAFile {
117            path: source_file_path.to_path_buf(),
118        });
119    }
120
121
122    let canonical_path = fs::canonicalize(source_file_path).map_err(|error| {
123        FileError::UnableToAccessSourceFile {
124            path: source_file_path.to_path_buf(),
125            error,
126        }
127    })?;
128
129    #[cfg(feature = "dunce")]
130    {
131        let de_unced_canonical_path = dunce::simplified(&canonical_path).to_path_buf();
132
133        Ok(ValidatedSourceFilePath {
134            source_file_path: de_unced_canonical_path,
135            original_was_symlink_to_file: true,
136        })
137    }
138
139    #[cfg(not(feature = "dunce"))]
140    {
141        Ok(ValidatedSourceFilePath {
142            source_file_path: canonical_path,
143            original_was_symlink_to_file: true,
144        })
145    }
146}
147
148
149/// A set of paths and auxiliary information about a destination file path.
150pub(crate) struct ValidatedDestinationFilePath {
151    /// Canonical destination file path.
152    ///
153    /// If the original file path was a symlink leading to some target file,
154    /// this path points to that target file.
155    pub(crate) destination_file_path: PathBuf,
156
157    /// Whether the destination already exists.
158    pub(crate) exists: bool,
159}
160
161pub(crate) enum DestinationValidationAction {
162    /// The validation logic concluded that no action should be taken
163    /// (the file should not be copied or moved) since the destination file already exists,
164    /// and `colliding_file_behaviour` is set to [`CollidingFileBehaviour::Skip`].
165    SkipCopyOrMove,
166
167    /// The validation logic found no path validation errors.
168    Continue(ValidatedDestinationFilePath),
169}
170
171
172/// Given a destination file path, validate that it respects `colliding_file_behaviour`,
173/// and that if it is a symlink, that it points to a file.
174///
175/// If the given path is a symlink to a file, the returned path will be a resolved (canonical) one,
176/// i.e. pointing to the real file.
177fn validate_destination_file_path(
178    validated_source_file_path: &ValidatedSourceFilePath,
179    destination_file_path: &Path,
180    colliding_file_behaviour: CollidingFileBehaviour,
181) -> Result<DestinationValidationAction, FileError> {
182    // Ensure the destination file path doesn't exist yet
183    // (unless `options.colliding_file_behaviour` allows that),
184    // and that it isn't a directory.
185
186    let destination_file_exists =
187        try_exists_without_follow(destination_file_path).map_err(|error| {
188            FileError::UnableToAccessDestinationFile {
189                path: destination_file_path.to_path_buf(),
190                error,
191            }
192        })?;
193
194
195    if destination_file_exists {
196        let canonical_destination_path = {
197            let canonical_destination_path =
198                destination_file_path.canonicalize().map_err(|error| {
199                    FileError::UnableToAccessDestinationFile {
200                        path: destination_file_path.to_path_buf(),
201                        error,
202                    }
203                })?;
204
205            #[cfg(feature = "dunce")]
206            {
207                dunce::simplified(&canonical_destination_path).to_path_buf()
208            }
209
210            #[cfg(not(feature = "dunce"))]
211            {
212                canonical_destination_path
213            }
214        };
215
216
217        // Ensure we don't try to copy the file into itself.
218
219        if validated_source_file_path
220            .source_file_path
221            .eq(&canonical_destination_path)
222        {
223            return Err(FileError::SourceAndDestinationAreTheSame {
224                path: canonical_destination_path,
225            });
226        }
227
228
229        // Ensure we respect the [`CollidingFileBehaviour`] option if
230        // the destination file already exists.
231        if destination_file_exists {
232            match colliding_file_behaviour {
233                CollidingFileBehaviour::Abort => {
234                    return Err(FileError::DestinationPathAlreadyExists {
235                        path: destination_file_path.to_path_buf(),
236                    })
237                }
238                CollidingFileBehaviour::Skip => {
239                    return Ok(DestinationValidationAction::SkipCopyOrMove);
240                }
241                CollidingFileBehaviour::Overwrite => {}
242            };
243        }
244
245
246        Ok(DestinationValidationAction::Continue(ValidatedDestinationFilePath {
247            destination_file_path: canonical_destination_path,
248            exists: true,
249        }))
250    } else {
251        Ok(DestinationValidationAction::Continue(ValidatedDestinationFilePath {
252            destination_file_path: destination_file_path.to_path_buf(),
253            exists: false,
254        }))
255    }
256}