Skip to content

Commit efda4f1

Browse files
committed
cp: break up copy_directory() into helper funcs
Add some additional structs and helper functions to make the code in `copydir.rs` easier to read and maintain. This commit changes only the organization of the code, not its function.
1 parent bf7a3f6 commit efda4f1

File tree

1 file changed

+196
-92
lines changed

1 file changed

+196
-92
lines changed

src/uu/cp/src/copydir.rs

Lines changed: 196 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,15 @@ use std::collections::HashSet;
1010
use std::env;
1111
use std::fs;
1212
use std::io;
13-
use std::path::Path;
13+
use std::path::{Path, PathBuf, StripPrefixError};
1414

1515
use uucore::display::Quotable;
1616
use uucore::fs::{canonicalize, FileInformation, MissingHandling, ResolveMode};
17-
use walkdir::WalkDir;
17+
use walkdir::{DirEntry, WalkDir};
1818

1919
use crate::{copy_file, copy_link, preserve_hardlinks, CopyResult, Options, TargetSlice};
2020

21-
/// Continue next iteration of loop if result of expression is error
22-
macro_rules! or_continue(
23-
($expr:expr) => (match $expr {
24-
Ok(temp) => temp,
25-
Err(error) => {
26-
show_error!("{}", error);
27-
continue
28-
},
29-
})
30-
);
31-
21+
/// Ensure a Windows path starts with a `\\?`.
3222
#[cfg(target_os = "windows")]
3323
fn adjust_canonicalization(p: &Path) -> Cow<Path> {
3424
// In some cases, \\? can be missing on some Windows paths. Add it at the
@@ -50,6 +40,181 @@ fn adjust_canonicalization(p: &Path) -> Cow<Path> {
5040
}
5141
}
5242

43+
/// Get a descendant path relative to the given parent directory.
44+
///
45+
/// If `root_parent` is `None`, then this just returns the `path`
46+
/// itself. Otherwise, this function strips the parent prefix from the
47+
/// given `path`, leaving only the portion of the path relative to the
48+
/// parent.
49+
fn get_local_to_root_parent(
50+
path: &Path,
51+
root_parent: Option<&Path>,
52+
) -> Result<PathBuf, StripPrefixError> {
53+
match root_parent {
54+
Some(parent) => {
55+
// On Windows, some paths are starting with \\?
56+
// but not always, so, make sure that we are consistent for strip_prefix
57+
// See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file for more info
58+
#[cfg(windows)]
59+
let (path, parent) = (
60+
adjust_canonicalization(path),
61+
adjust_canonicalization(parent),
62+
);
63+
let path = path.strip_prefix(&parent)?;
64+
Ok(path.to_path_buf())
65+
}
66+
None => Ok(path.to_path_buf()),
67+
}
68+
}
69+
70+
/// Paths that are invariant throughout the traversal when copying a directory.
71+
struct Context<'a> {
72+
/// The current working directory at the time of starting the traversal.
73+
current_dir: PathBuf,
74+
75+
/// The path to the parent of the source directory, if any.
76+
root_parent: Option<PathBuf>,
77+
78+
/// The target path to which the directory will be copied.
79+
target: &'a Path,
80+
}
81+
82+
impl<'a> Context<'a> {
83+
fn new(root: &'a Path, target: &'a Path) -> std::io::Result<Self> {
84+
let current_dir = env::current_dir()?;
85+
let root_path = current_dir.join(root);
86+
let root_parent = if target.exists() {
87+
root_path.parent().map(|p| p.to_path_buf())
88+
} else {
89+
Some(root_path)
90+
};
91+
Ok(Self {
92+
current_dir,
93+
root_parent,
94+
target,
95+
})
96+
}
97+
}
98+
99+
/// Data needed to perform a single copy operation while traversing a directory.
100+
///
101+
/// For convenience while traversing a directory, the [`Entry::new`]
102+
/// function allows creating an entry from a [`Context`] and a
103+
/// [`walkdir::DirEntry`].
104+
///
105+
/// # Examples
106+
///
107+
/// For example, if the source directory structure is `a/b/c`, the
108+
/// target is `d/`, a directory that already exists, and the copy
109+
/// command is `cp -r a/b/c d`, then the overall set of copy
110+
/// operations could be represented as three entries,
111+
///
112+
/// ```rust,ignore
113+
/// let operations = [
114+
/// Entry {
115+
/// source: "a".into(),
116+
/// local_to_target: "d/a".into(),
117+
/// target_is_file: false,
118+
/// }
119+
/// Entry {
120+
/// source: "a/b".into(),
121+
/// local_to_target: "d/a/b".into(),
122+
/// target_is_file: false,
123+
/// }
124+
/// Entry {
125+
/// source: "a/b/c".into(),
126+
/// local_to_target: "d/a/b/c".into(),
127+
/// target_is_file: false,
128+
/// }
129+
/// ];
130+
/// ```
131+
struct Entry {
132+
/// The path to file or directory to copy.
133+
source: PathBuf,
134+
135+
/// The path to the destination, relative to the target.
136+
local_to_target: PathBuf,
137+
138+
/// Whether the destination is a file.
139+
target_is_file: bool,
140+
}
141+
142+
impl Entry {
143+
fn new(context: &Context, direntry: &DirEntry) -> Result<Self, StripPrefixError> {
144+
let source = context.current_dir.join(direntry.path());
145+
let descendant = get_local_to_root_parent(&source, context.root_parent.as_deref())?;
146+
let local_to_target = context.target.join(descendant);
147+
let target_is_file = context.target.is_file();
148+
Ok(Self {
149+
source,
150+
local_to_target,
151+
target_is_file,
152+
})
153+
}
154+
}
155+
156+
/// Copy a single entry during a directory traversal.
157+
fn copy_direntry(
158+
entry: Entry,
159+
options: &Options,
160+
symlinked_files: &mut HashSet<FileInformation>,
161+
preserve_hard_links: bool,
162+
hard_links: &mut Vec<(String, u64)>,
163+
) -> CopyResult<()> {
164+
let Entry {
165+
source,
166+
local_to_target,
167+
target_is_file,
168+
} = entry;
169+
170+
// If the source is a symbolic link and the options tell us not to
171+
// dereference the link, then copy the link object itself.
172+
if source.is_symlink() && !options.dereference {
173+
return copy_link(&source, &local_to_target, symlinked_files);
174+
}
175+
176+
// If the source is a directory and the destination does not
177+
// exist, ...
178+
if source.is_dir() && !local_to_target.exists() {
179+
if target_is_file {
180+
return Err("cannot overwrite non-directory with directory".into());
181+
} else {
182+
// TODO Since the calling code is traversing from the root
183+
// of the directory structure, I don't think
184+
// `create_dir_all()` will have any benefit over
185+
// `create_dir()`, since all the ancestor directories
186+
// should have already been created.
187+
fs::create_dir_all(local_to_target)?;
188+
return Ok(());
189+
}
190+
}
191+
192+
// If the source is not a directory, then we need to copy the file.
193+
if !source.is_dir() {
194+
if preserve_hard_links {
195+
let mut found_hard_link = false;
196+
preserve_hardlinks(hard_links, &source, &local_to_target, &mut found_hard_link)?;
197+
if found_hard_link {
198+
return Ok(());
199+
}
200+
match copy_file(&source, &local_to_target, options, symlinked_files, false) {
201+
Ok(_) => return Ok(()),
202+
// silent the error with a symlink
203+
// In case we do --archive, we might copy the symlink
204+
// before the file itself
205+
Err(_) if source.is_symlink() => return Ok(()),
206+
Err(err) => return Err(err),
207+
}
208+
} else {
209+
return copy_file(&source, &local_to_target, options, symlinked_files, false);
210+
}
211+
}
212+
213+
// In any other case, there is nothing to do, so we just return to
214+
// continue the traversal.
215+
Ok(())
216+
}
217+
53218
/// Read the contents of the directory `root` and recursively copy the
54219
/// contents to `target`.
55220
///
@@ -87,96 +252,35 @@ pub(crate) fn copy_directory(
87252
.into());
88253
}
89254

90-
let current_dir =
91-
env::current_dir().unwrap_or_else(|e| crash!(1, "failed to get current directory {}", e));
92-
93-
let root_path = current_dir.join(root);
94-
95-
let root_parent = if target.exists() {
96-
root_path.parent()
97-
} else {
98-
Some(root_path.as_path())
99-
};
100-
101-
#[cfg(unix)]
102255
let mut hard_links: Vec<(String, u64)> = vec![];
103256
let preserve_hard_links = options.preserve_hard_links();
104257

105-
// This should be changed once Redox supports hardlinks
106-
#[cfg(any(windows, target_os = "redox"))]
107-
let mut hard_links: Vec<(String, u64)> = vec![];
258+
// Collect some paths here that are invariant during the traversal
259+
// of the given directory, like the current working directory and
260+
// the target directory.
261+
let context = match Context::new(root, target) {
262+
Ok(c) => c,
263+
Err(e) => return Err(format!("failed to get current directory {}", e).into()),
264+
};
108265

109-
for path in WalkDir::new(root)
266+
// Traverse the contents of the directory, copying each one.
267+
for direntry_result in WalkDir::new(root)
110268
.same_file_system(options.one_file_system)
111269
.follow_links(options.dereference)
112270
{
113-
let p = or_continue!(path);
114-
let path = current_dir.join(&p.path());
115-
116-
let local_to_root_parent = match root_parent {
117-
Some(parent) => {
118-
#[cfg(windows)]
119-
{
120-
// On Windows, some paths are starting with \\?
121-
// but not always, so, make sure that we are consistent for strip_prefix
122-
// See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file for more info
123-
let parent_can = adjust_canonicalization(parent);
124-
let path_can = adjust_canonicalization(&path);
125-
126-
or_continue!(&path_can.strip_prefix(&parent_can)).to_path_buf()
127-
}
128-
#[cfg(not(windows))]
129-
{
130-
or_continue!(path.strip_prefix(&parent)).to_path_buf()
131-
}
132-
}
133-
None => path.clone(),
134-
};
135-
136-
let local_to_target = target.join(&local_to_root_parent);
137-
if p.path().is_symlink() && !options.dereference {
138-
copy_link(&path, &local_to_target, symlinked_files)?;
139-
} else if path.is_dir() && !local_to_target.exists() {
140-
if target.is_file() {
141-
return Err("cannot overwrite non-directory with directory".into());
142-
}
143-
fs::create_dir_all(local_to_target)?;
144-
} else if !path.is_dir() {
145-
if preserve_hard_links {
146-
let mut found_hard_link = false;
147-
let source = path.to_path_buf();
148-
let dest = local_to_target.as_path().to_path_buf();
149-
preserve_hardlinks(&mut hard_links, &source, &dest, &mut found_hard_link)?;
150-
if !found_hard_link {
151-
match copy_file(
152-
path.as_path(),
153-
local_to_target.as_path(),
154-
options,
155-
symlinked_files,
156-
false,
157-
) {
158-
Ok(_) => Ok(()),
159-
Err(err) => {
160-
if source.is_symlink() {
161-
// silent the error with a symlink
162-
// In case we do --archive, we might copy the symlink
163-
// before the file itself
164-
Ok(())
165-
} else {
166-
Err(err)
167-
}
168-
}
169-
}?;
170-
}
171-
} else {
172-
copy_file(
173-
path.as_path(),
174-
local_to_target.as_path(),
271+
match direntry_result {
272+
Ok(direntry) => {
273+
let entry = Entry::new(&context, &direntry)?;
274+
copy_direntry(
275+
entry,
175276
options,
176277
symlinked_files,
177-
false,
278+
preserve_hard_links,
279+
&mut hard_links,
178280
)?;
179281
}
282+
// Print an error message, but continue traversing the directory.
283+
Err(e) => show_error!("{}", e),
180284
}
181285
}
182286

0 commit comments

Comments
 (0)