copy_glob/
lib.rs

1//! # About it
2//!
3//! **copy-glob** is a small utility to copy files to an output directory using glob syntax. Inspired by [copy_to_output](https://crates.io/crates/copy_to_output).
4//!
5//! # How to use
6//!
7//! Add a crate to the build dependencies in `Cargo.toml`.
8//!
9//! ```toml
10//! [build-dependencies]
11//! copy-glob = "0.1"
12//! ```
13//!
14//! Create `build.rs` and add the following to it:
15//!
16//! ```rust
17//! use copy_glob::{get_target_folder, copy_glob};
18//!
19//! fn main() {
20//!     let output = get_target_folder().join("copies"); // target/{debug,release}/copies
21//!     copy_glob("**/for_copy/*", output);
22//! }
23//! ```
24//!
25//! This will copy all the files in the directory, preserving the structure, since the copying starts from the root of the project (where the `Cargo.toml` file is located).
26//!
27//! To localize the file search somehow, you can use the following code:
28//!
29//! ```rust,no_run
30//! use copy_glob::{get_target_folder, get_root_path, CopyGlobOptionsBuilder, copy_glob_with};
31//!
32//! fn main() {
33//!     let output = get_target_folder().join("copies");
34//!     let root = get_root_path().join("for_copy"); // same level as Cargo.toml
35//!     let options = CopyGlobOptionsBuilder::new().set_root_path(root).build();
36//!     copy_glob_with("*.toml", output, &options);
37//! }
38//! ```
39//!
40//! This will copy all files with the `.toml` extension in the `for_copy` folder without preserving the structure.
41//!
42//! In addition, you can add exceptions from where files will be copied. By default, the `target` directory hangs in the excludes.
43//!
44//! ```rust
45//! # use copy_glob::CopyGlobOptionsBuilder;
46//! let options = CopyGlobOptionsBuilder::new().add_exclude("**/some_pattern/*").build();
47//! ```
48
49use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
50use std::env;
51use std::fs::create_dir_all;
52use std::path::{Path, PathBuf};
53use walkdir::WalkDir;
54
55pub fn copy_glob(glob: impl AsRef<str>, output_dir: impl AsRef<Path>) {
56    let options = CopyGlobOptionsBuilder::new().build();
57    copy_glob_with(glob, output_dir, &options);
58}
59
60pub fn copy_glob_with(
61    glob: impl AsRef<str>,
62    output_dir: impl AsRef<Path>,
63    options: &CopyGlobOptions,
64) {
65    let glob = glob.as_ref();
66    let matcher = new_glob(glob);
67    let current_dir = options.root.as_ref();
68
69    if !output_dir.as_ref().exists() {
70        create_dir_all(&output_dir).unwrap();
71    }
72
73    for entry in WalkDir::new(&current_dir) {
74        let entry = entry.expect("Unable to read dir");
75        if options.exclude.is_match(entry.path()) || !matcher.is_match(entry.path()) {
76            continue;
77        }
78
79        let path = entry.path().display();
80        println!("cargo:rerun-if-changed={path}");
81
82        let metadata = entry.metadata().expect("Unable to get metadata");
83        let dirname = get_str(entry.path())
84            .strip_prefix(get_str(current_dir))
85            .expect("Unable to strip a string")
86            .trim_start_matches("\\")
87            .trim_start_matches("/");
88        let output_path = output_dir.as_ref().join(dirname);
89
90        if metadata.is_dir() {
91            create_dir_all(output_path).expect("Unable to create output dir");
92        } else if metadata.is_file() {
93            let parent = output_path.parent().unwrap();
94
95            if parent.exists() {
96                std::fs::copy(entry.path(), output_path).expect("Unable to copy file");
97            } else {
98                std::fs::copy(entry.path(), output_dir.as_ref().join(entry.file_name()))
99                    .expect("Unable to copy file");
100            }
101        }
102    }
103}
104
105pub struct CopyGlobOptions {
106    root: PathBuf,
107    exclude: GlobSet,
108}
109
110pub struct CopyGlobOptionsBuilder {
111    root: PathBuf,
112    exclude: GlobSetBuilder,
113}
114
115impl CopyGlobOptionsBuilder {
116    pub fn new() -> Self {
117        let root = get_root_path();
118        let mut exclude = GlobSet::builder();
119
120        exclude.add(Glob::new("**/target/*").unwrap());
121
122        Self { root, exclude }
123    }
124}
125
126impl CopyGlobOptionsBuilder {
127    pub fn set_root_path(mut self, root: impl AsRef<Path>) -> Self {
128        self.root = root.as_ref().to_path_buf();
129        self
130    }
131
132    pub fn add_exclude(mut self, glob: impl AsRef<str>) -> Self {
133        self.exclude.add(Glob::new(glob.as_ref()).unwrap());
134        self
135    }
136
137    pub fn build(self) -> CopyGlobOptions {
138        CopyGlobOptions {
139            root: self.root,
140            exclude: self.exclude.build().unwrap(),
141        }
142    }
143}
144
145pub fn new_glob(glob: impl AsRef<str>) -> GlobMatcher {
146    Glob::new(glob.as_ref())
147        .expect("Wrong pattern")
148        .compile_matcher()
149}
150
151pub fn get_target_folder() -> PathBuf {
152    let path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("target");
153
154    #[cfg(debug_assertions)]
155    return path.join("debug");
156
157    #[cfg(not(debug_assertions))]
158    return path.join("release");
159}
160
161pub fn get_root_path() -> PathBuf {
162    PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
163}
164
165fn get_str(path: &Path) -> &str {
166    path.to_str().expect("Unable to convert path to str")
167}