1 /** 
2  * Utilities for interacting with the file system.
3  */
4 module utils.fileutils;
5 
6 import std.file;
7 import std.path;
8 
9 /** 
10  * Checks if a directory is empty.
11  * Params:
12  *   dir = The directory to check.
13  * Returns: True if the given directory exists, is a directory, and is empty.
14  */
15 public bool isDirEmpty(string dir) {
16     import std.range : empty;
17     import std.array : array;
18     if (!exists(dir) || !isDir(dir)) return false;
19     return empty(dirEntries(dir, SpanMode.shallow).array);
20 }
21 
22 unittest {
23     assert(!isDirEmpty("source"));
24     assert(!isDirEmpty("source/dsh.d"));
25     assert(!isDirEmpty("source/some-non-existant-file.blah"));
26     auto d = ".test_empty_dir";
27     d.mkdir;
28     scope(exit) d.rmdir;
29     assert(isDirEmpty(d));
30 }
31 
32 /** 
33  * Walks through all the entries in a directory, and applies the given visitor
34  * function to all entries.
35  * Params:
36  *   dir = The directory to walk through.
37  *   visitor = A visitor delegate function to apply to all entries discovered.
38  *   recursive = Whether to recursively walk through subdirectories.
39  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
40  */
41 public void walkDir(string dir, void delegate(DirEntry entry) visitor, bool recursive = true, int maxDepth = -1) {
42     if (!exists(dir) || !isDir(dir)) return;
43     foreach (DirEntry entry; dirEntries(dir, SpanMode.shallow)) {
44         visitor(entry);
45         if (recursive && entry.isDir && (maxDepth > 0 || maxDepth == -1)) {
46             walkDir(entry.name, visitor, recursive, maxDepth - 1);
47         }
48     }
49 }
50 
51 unittest {
52     ulong totalSize = 0;
53     uint filesVisited = 0;
54     void addSize(DirEntry entry) {
55         totalSize += entry.size;
56         filesVisited++;
57     }
58     walkDir(".", &addSize);
59     assert(totalSize > 0);
60     assert(filesVisited > 10);
61 }
62 
63 /** 
64  * Walks through all the entries in a directory, and applies the given visitor
65  * function to all entries for which a given filter function returns true.
66  * Params:
67  *   dir = The directory to walk through.
68  *   visitor = A visitor delegate function to apply to all entries discovered.
69  *   filter = A filter delegate that determines if an entry should be visited.
70  *   recursive = Whether to recursively walk through subdirectories.
71  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
72  */
73 public void walkDirFiltered(
74     string dir,
75     void delegate(DirEntry entry) visitor,
76     bool delegate(DirEntry entry) filter,
77     bool recursive = true,
78     int maxDepth = -1
79 ) {
80     walkDir(dir, delegate(DirEntry entry) {
81         if (filter(entry)) visitor(entry);
82     }, recursive, maxDepth);
83 }
84 
85 /** 
86  * Walks through all files in a directory.
87  * Params:
88  *   dir = The directory to walk through.
89  *   visitor = A visitor delegate function to apply to all entries discovered.
90  *   recursive = Whether to recursively walk through subdirectories.
91  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
92  */
93 public void walkDirFiles(
94     string dir,
95     void delegate(DirEntry entry) visitor,
96     bool recursive = true,
97     int maxDepth = -1
98 ) {
99     walkDirFiltered(dir, visitor, (entry) {return entry.isFile;}, recursive, maxDepth);
100 }
101 
102 /** 
103  * Finds matching files in a directory.
104  * Params:
105  *   dir = The directory to search in.
106  *   pattern = A regex pattern to match against each filename.
107  *   recursive = Whether to recursively search subdirectories.
108  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
109  * Returns: A list of matching filenames.
110  */
111 public string[] findFiles(string dir, string pattern, bool recursive = true, int maxDepth = -1) {
112     import std.regex;
113     string[] matches = [];
114     walkDirFiles(dir, (entry) {
115         string filename = baseName(entry.name);
116         Captures!string c = matchFirst(filename, pattern);
117         if (!c.empty && c.hit.length == filename.length) matches ~= entry.name;
118     }, recursive, maxDepth);
119     return matches;
120 }
121 
122 unittest {
123     assert(findFiles("source", ".*\\.d", true, 0) == ["source/dsh.d"]);
124     assert(findFiles(".", "dub.*", false).length == 2);
125 }
126 
127 /** 
128  * Finds all files in a directory that end with the given extension text.
129  * Params:
130  *   dir = The directory to search in.
131  *   extension = The extension for matching files, such as ".txt" or ".d"
132  *   recursive = Whether to recursively search subdirectories.
133  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
134  * Returns: The list of matching files.
135  */
136 public string[] findFilesByExtension(string dir, string extension, bool recursive = true, int maxDepth = -1) {
137     return findFiles(dir, ".*" ~ extension, recursive, maxDepth);
138 }
139 
140 unittest {
141     assert(findFilesByExtension(".", "json").length == 2);
142 }
143 
144 /** 
145  * Tries to find a single file matching the given pattern.
146  * Params:
147  *   dir = The directory to search in.
148  *   pattern = A regex pattern to match against each filename.
149  *   recursive = Whether to recursively search subdirectories.
150  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
151  * Returns: The single matching file that was found, or null if no match was
152  * found, or if multiple matches were found.
153  */
154 public string findFile(string dir, string pattern, bool recursive = true, int maxDepth = -1) {
155     auto matches = findFiles(dir, pattern, recursive, maxDepth);
156     if (matches.length != 1) return null;
157     return matches[0];
158 }
159 
160 unittest {
161     assert(findFile(".", "dub\\.json") !is null);
162     assert(findFile(".", ".*\\.d") is null);
163 }
164 
165 /** 
166  * Copies all files from the given source directory, to the given destination
167  * directory. Will create the destination directory if it doesn't exist yet.
168  * Overwrites any files that already exist in the destination directory.
169  * Params:
170  *   sourceDir = The source directory to copy from.
171  *   destDir = The destination directory to copy to.
172  */
173 public void copyDir(string sourceDir, string destDir) {
174     if (!isDir(sourceDir)) return;
175     if (exists(destDir) && !isDir(destDir)) return;
176     if (!exists(destDir)) mkdirRecurse(destDir);
177     foreach (DirEntry entry; dirEntries(sourceDir, SpanMode.shallow)) {
178         string destPath = buildPath(destDir, baseName(entry.name));
179         if (entry.isDir) {
180             copyDir(entry.name, destPath);
181         } else if (entry.isFile) {
182             copy(entry.name, destPath);
183         }
184     }
185 }
186 
187 unittest {
188     copyDir("source", "source2");
189     assert(exists("source2") && isDir("source2"));
190     assert(exists("source2/dsh.d"));
191     rmdirRecurse("source2");
192 }