fs_more/directory/
common.rs

1use std::path::{Path, PathBuf};
2
3use crate::{error::SourceSubPathNotUnderBaseSourceDirectory, file::CollidingFileBehaviour};
4
5
6/// Rules that dictate how existing sub-directories inside the
7/// directory copy or move destination are handled when they collide with the
8/// ones we're trying to copy or move from the source.
9///
10/// See also: [`DestinationDirectoryRule`].
11#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
12pub enum CollidingSubDirectoryBehaviour {
13    /// An existing (colliding) destination sub-directory will cause an error.
14    Abort,
15
16    /// An existing (colliding) destination sub-directory will have no effect.
17    Continue,
18}
19
20
21/// Specifies whether you allow the destination directory to exist
22/// before copying or moving files or directories into it.
23///
24/// If you allow the destination directory to exist, you can also specify whether it must be empty;
25/// if not, you may also specify how to behave for existing destination files and directories.
26///
27///
28/// # Defaults
29/// [`Default`] is implemented for this enum. The default value is [`DestinationDirectoryRule::AllowEmpty`].
30///
31///
32/// # Examples
33/// If you want the associated directory copying or moving function to
34/// return an error if the provided destination directory already exists,
35/// use [`DestinationDirectoryRule::DisallowExisting`]. This is the strictest rule,
36/// requiring the destination to not exist.
37///
38/// <br>
39///
40/// If you at most want to copy into an empty destination directory, use [`DestinationDirectoryRule::AllowEmpty`].
41/// This rule is slightly more relaxed than the previous one.
42/// It, however, does not require the destination directory to exist - it will be created if missing.
43///
44/// <br>
45///
46/// If the destination directory is allowed to exist *and* contain existing files or sub-directories,
47/// but you don't want to overwrite any of the existing files, you can use the following rule:
48///
49/// ```no_run
50/// # use fs_more::directory::DestinationDirectoryRule;
51/// # use fs_more::directory::CollidingSubDirectoryBehaviour;
52/// # use fs_more::file::CollidingFileBehaviour;
53/// let rules = DestinationDirectoryRule::AllowNonEmpty {
54///     colliding_file_behaviour: CollidingFileBehaviour::Abort,
55///     colliding_subdirectory_behaviour: CollidingSubDirectoryBehaviour::Continue,
56/// };
57/// ```
58///
59/// This will create any missing destination sub-directories and ignore the ones that already exist,
60/// even if their counterparts also exist in the source directory. Also, this will still not overwrite
61/// existing destination files - it will effectively be a merge without overwrites.
62///
63/// <br>
64///
65/// If you want files to be overwritten, you may set the behaviour this way:
66///
67/// ```no_run
68/// # use fs_more::directory::DestinationDirectoryRule;
69/// # use fs_more::directory::CollidingSubDirectoryBehaviour;
70/// # use fs_more::file::CollidingFileBehaviour;
71/// let rules = DestinationDirectoryRule::AllowNonEmpty {
72///     colliding_file_behaviour: CollidingFileBehaviour::Overwrite,
73///     colliding_subdirectory_behaviour: CollidingSubDirectoryBehaviour::Continue,
74/// };
75/// ```
76///
77///
78/// # A word of caution
79/// **Do not use [`DestinationDirectoryRule::AllowNonEmpty`] as a default
80/// unless you're sure you are okay with merged directories.**
81///
82/// If the destination directory already has some content, this would
83/// allow a copy or move that results in a destination directory
84/// with *merged* source and destination directory contents.
85/// Unless this is precisely what you're after, you may want to avoid this option.
86#[derive(Clone, Copy, PartialEq, Eq, Debug)]
87pub enum DestinationDirectoryRule {
88    /// Indicates the associated directory function should return an error,
89    /// if the destination directory already exists.
90    DisallowExisting,
91
92    /// Indicates the associated function should return an error,
93    /// if the destination directory exists *and is not empty*.
94    ///
95    /// **This is the default.**
96    AllowEmpty,
97
98    /// Indicates that an existing (colliding) destination directory should
99    /// not cause an error, even if non-empty.
100    ///
101    /// **Do not use this as a default if you're not sure what rule to choose.**
102    ///
103    /// If the destination directory already has some content, this would
104    /// allow a copy or move that results in a destination directory
105    /// with *merged* source and destination directory contents.
106    /// Unless this is precisely what you're after, you may want to avoid this option.
107    ///
108    /// Missing destination directories will always be created,
109    /// regardless of the `colliding_subdirectory_behaviour` option.
110    /// Setting it to [`CollidingSubDirectoryBehaviour::Continue`] simply means that
111    /// if they already exist on the destination, they will not need to be created.
112    AllowNonEmpty {
113        /// How to behave when encountering existing (colliding) destination files.
114        ///
115        /// This option has no effect on existing destination files
116        /// that don't collide with the ones we're copying or moving.
117        colliding_file_behaviour: CollidingFileBehaviour,
118
119        /// How to behave when encountering existing (colliding) destination subdirectories.
120        ///
121        /// This option has no effect on existing destination subdirectories
122        /// that don't collide with the ones we're copying or moving.
123        colliding_subdirectory_behaviour: CollidingSubDirectoryBehaviour,
124    },
125}
126
127impl Default for DestinationDirectoryRule {
128    /// The default value for this struct is [`Self::AllowEmpty`].
129    fn default() -> Self {
130        Self::AllowEmpty
131    }
132}
133
134impl DestinationDirectoryRule {
135    pub(crate) fn allows_overwriting_existing_destination_files(&self) -> bool {
136        matches!(
137            self,
138            Self::AllowNonEmpty {
139                colliding_file_behaviour: CollidingFileBehaviour::Overwrite,
140                ..
141            }
142        )
143    }
144
145    pub(crate) fn allows_existing_destination_subdirectories(&self) -> bool {
146        matches!(
147            self,
148            Self::AllowNonEmpty {
149                colliding_subdirectory_behaviour: CollidingSubDirectoryBehaviour::Continue,
150                ..
151            }
152        )
153    }
154}
155
156
157
158/// Computes a relative path of `source_sub_path` relative to `source_base_directory_path`,
159/// and applies it onto `target_base_directory_path`.
160///
161/// `source_base_directory_path` is the base source directory path,
162/// and `source_sub_path` *must* be a descendant of that path.
163/// `target_base_directory_path` can be an arbitrary target directory path.
164///
165/// Returns [`SourceSubPathNotUnderBaseSourceDirectory`]
166/// if `source_sub_path` is not a sub-path of `source_base_directory_path`.
167///
168///
169/// # Example
170/// ```ignore
171/// # use std::path::Path;
172/// # use fs_more::directory::copy::join_relative_source_path_onto_destination;
173///
174/// let foo = Path::new("/foo");
175/// let foo_hello_world = Path::new("/foo/abc/hello-world.txt");
176/// let bar = Path::new("/bar");
177///
178/// assert_eq!(
179///     join_relative_source_path_onto_destination(
180///         foo,
181///         foo_hello_world,
182///         bar
183///     ).unwrap(),
184///     Path::new("/bar/abc/hello-world.txt")
185/// );
186/// ```
187pub(crate) fn join_relative_source_path_onto_destination(
188    source_base_directory_path: &Path,
189    source_sub_path: &Path,
190    target_base_directory_path: &Path,
191) -> Result<PathBuf, SourceSubPathNotUnderBaseSourceDirectory> {
192    // Strip the base source directory path from the full source path
193    // and place it on top of the target base directory path.
194
195    if source_base_directory_path.eq(source_sub_path) {
196        return Ok(target_base_directory_path.to_path_buf());
197    }
198
199    let source_sub_path_relative_to_base = source_sub_path
200        .strip_prefix(source_base_directory_path)
201        .map_err(|_| SourceSubPathNotUnderBaseSourceDirectory {
202            path: source_base_directory_path.join(source_sub_path),
203        })?;
204
205    Ok(target_base_directory_path.join(source_sub_path_relative_to_base))
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn properly_rejoin_source_subpath_onto_target() {
214        let root_a = Path::new("/hello/there");
215        let foo = Path::new("/hello/there/some/content");
216        let root_b = Path::new("/different/root");
217
218        assert_eq!(
219            join_relative_source_path_onto_destination(root_a, foo, root_b).unwrap(),
220            Path::new("/different/root/some/content"),
221            "rejoin_source_subpath_onto_target did not rejoin the path properly."
222        );
223
224        let foo = Path::new("/foo");
225        let foo_hello_world = Path::new("/foo/abc/hello-world.txt");
226        let bar = Path::new("/bar");
227
228        assert_eq!(
229            join_relative_source_path_onto_destination(foo, foo_hello_world, bar).unwrap(),
230            Path::new("/bar/abc/hello-world.txt")
231        );
232    }
233
234    #[test]
235    fn error_on_subpath_not_being_under_source_root() {
236        let root_a = Path::new("/hello/there");
237        let foo = Path::new("/completely/different/path");
238        let root_b = Path::new("/different/root");
239
240        let rejoin_result = join_relative_source_path_onto_destination(root_a, foo, root_b);
241
242        assert!(
243            rejoin_result.is_err(),
244            "rejoin_source_subpath_onto_target did not return Err when \
245            the source path to rejoin wasn't under the source root path"
246        );
247    }
248}