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}