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}