1 /*
2 Copyright (c) 2014-2021 Martin Cejp, Timur Gafarov
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 
6 Permission is hereby granted, free of charge, to any person or organization
7 obtaining a copy of the software and accompanying documentation covered by
8 this license (the "Software") to use, reproduce, display, distribute,
9 execute, and transmit the Software, and to prepare derivative works of the
10 Software, and to permit third-parties to whom the Software is furnished to
11 do so, all subject to the following:
12 
13 The copyright notices in the Software and this entire statement, including
14 the above license grant, this restriction and the following disclaimer,
15 must be included in all copies of the Software, in whole or in part, and
16 all derivative works of the Software, unless such copies or derivative
17 works are solely in the form of machine-executable object code generated by
18 a source language processor.
19 
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 DEALINGS IN THE SOFTWARE.
27 */
28 
29 /**
30  * Copyright: Martin Cejp, Timur Gafarov 2014-2021.
31  * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0).
32  * Authors: Martin Cejp, Timur Gafarov
33  */
34 module dlib.filesystem.filesystem;
35 
36 import std.datetime;
37 import std.range;
38 import std.regex;
39 import std.algorithm;
40 import std..string;
41 
42 import dlib.core.stream;
43 import dlib.filesystem.dirrange;
44 
45 /// File size type
46 alias FileSize = StreamSize;
47 
48 /// If a file is readable, FileStat.permissions will have PRead bits set
49 enum PRead = 0x01;
50 
51 /// If a file is writable, FileStat.permissions will have PWrite bits set
52 enum PWrite = 0x02;
53 
54 /// If a file is executable, FileStat.permissions will have PExecute bits set
55 enum PExecute = 0x04;
56 
57 /// Holds general information about a file or directory.
58 struct FileStat
59 {
60     /// True if a file is not a directory
61     bool isFile;
62     
63     /// True if a file is a directory
64     bool isDirectory;
65     
66     /// File size. Valid only if isFile is true
67     FileSize sizeInBytes;
68     
69     /// File creation date/time
70     SysTime creationTimestamp;
71     
72     /// File modification date/time
73     SysTime modificationTimestamp;
74     
75     /// Permissions of a file in a given filesystem. Bit combination of PRead, PWrite, PExecute
76     int permissions;
77 }
78 
79 /// A filesystem entry - file or directory
80 struct DirEntry
81 {
82     /// Entry base name (relative to its containing directory)
83     string name;
84     
85     /// True if an entry is a file
86     bool isFile;
87     
88     /// True if an entry is a directory
89     bool isDirectory;
90 }
91 
92 /// A directory in the file system.
93 interface Directory
94 {
95     /// 
96     void close();
97 
98     /// Get directory contents as a range.
99     /// This range $(I should) be lazily evaluated when practical.
100     /// The entries "." and ".." are skipped.
101     InputRange!DirEntry contents();
102 }
103 
104 /// A filesystem limited to read access.
105 interface ReadOnlyFileSystem
106 {
107     /** Get file or directory stats.
108         Example:
109         ---
110         void printFileInfo(ReadOnlyFileSystem fs, string filename)
111         {
112             FileStat stat;
113 
114             writef("'%s'\t", filename);
115 
116             if (!fs.stat(filename, stat))
117             {
118                 writeln("ERROR");
119                 return;
120             }
121 
122             if (stat.isFile)
123                 writefln("%u", stat.sizeInBytes);
124             else if (stat.isDirectory)
125                 writeln("DIR");
126 
127             writefln("  created: %s", to!string(stat.creationTimestamp));
128             writefln("  modified: %s", to!string(stat.modificationTimestamp));
129         }
130         ---
131     */
132     bool stat(string filename, out FileStat stat);
133 
134     /** Open a file for input.
135         Returns: a valid InputStream on success, null on failure
136     */
137     InputStream openForInput(string filename);
138 
139     /** Open a directory.
140     */
141     Directory openDir(string path);
142 }
143 
144 /// A file system with read/write access.
145 interface FileSystem: ReadOnlyFileSystem
146 {
147     // TODO: Use exceptions or not?
148     
149     /// File access flags.
150     enum
151     {
152         read = 1,
153         write = 2,
154     }
155 
156     /// File creation flags.
157     enum
158     {
159         create = 1,
160         truncate = 2,
161     }
162 
163     // TODO: Keep it this way? (strongly-typed)
164 
165     /** Open a file for output.
166         Returns: a valid OutputStream on success, null on failure
167     */
168     OutputStream openForOutput(string filename, uint creationFlags);
169 
170     /** Open a file for input & output.
171         Returns: a valid IOStream on success, null on failure
172     */
173     IOStream openForIO(string filename, uint creationFlags);
174 
175     //IOStream openFile(string filename, uint accessFlags, uint creationFlags);
176 
177     /** Create a new directory.
178         Returns: true if a new directory was created
179         Examples:
180         ---
181         fs.createDir("New Directory", false);
182         fs.createDir("nested/directories/are/easy", true);
183         ---
184     */
185     bool createDir(string path, bool recursive);
186 
187     // BROKEN API. Must define semantics for non-atomic move cases (e.g. moving a file to a different drive)
188     //bool move(string path, string newPath);
189 
190     /** Permanently delete a file or directory.
191     */
192     bool remove(string path, bool recursive);
193 }
194 
195 /**
196     Find files in the specified directory
197 
198     Params:
199     rofs = filesystem to scan
200     baseDir = path to the base directory (if empty, defaults to current working directory)
201     recursive = if true, the search will recurse into subdirectories
202 
203     Examples:
204     ---
205     void listImagesInDirectory(ReadOnlyFileSystem fs, string baseDir = "")
206     {
207         foreach (entry; fs.findFiles(baseDir, true)
208                 .filter!(entry => entry.isFile)
209                 .filter!(entry => !matchFirst(entry.name, `.*\.(gif|jpg|png)$`).empty))
210         {
211             writefln("%s", entry.name);
212         }
213     }
214     ---
215 */
216 InputRange!DirEntry findFiles(ReadOnlyFileSystem rofs, string baseDir, bool recursive)
217 {
218     // Do some magic so that we don't have to keep our own stack
219 
220     import core.thread;
221 
222     //baseDir = normalizePath(baseDir);
223 
224     DirEntry entry;
225 
226     // findFiles insists on calling us back (it's recursive), but we can trap it in a Fiber
227     auto search = new Fiber(delegate void()
228     {
229         findFiles(rofs, baseDir, recursive, delegate int(ref DirEntry de)
230         {
231             // save the data (D doesn't allow to yield it directly)
232             // and jump outside of the .call() (see below)
233             entry = de;
234             Fiber.yield();
235 
236             // after resuming, return to findFiles for another round
237             return 0;
238         });
239 
240         // state becomes TERM after we're resumed after returning the last entry
241     });
242 
243     return new DirRange(delegate bool(out DirEntry de)
244     {
245         // terminated before?
246         if (search.state == Fiber.State.TERM)
247             return false;
248 
249         // jumps into our search delegate
250         search.call();
251 
252         // last entry had been returned last time?
253         // (even findFiles didn't know until we returned to it again)
254         if (search.state == Fiber.State.TERM)
255             return false;
256 
257         de = entry;
258         return true;
259     });
260 }
261 
262 private int findFiles(ReadOnlyFileSystem rofs, string baseDir, bool recursive, int delegate(ref DirEntry entry) dg)
263 {
264     Directory dir = rofs.openDir(baseDir);
265 
266     if (dir is null)
267         return 0;
268 
269     int result = 0;
270 
271     try
272     {
273         foreach (entry; dir.contents)
274         {
275             if (!baseDir.empty)
276                 entry.name = baseDir ~ "/" ~ entry.name;
277 
278             result = dg(entry);
279 
280             if (result != 0)
281                 return result;
282 
283             if (recursive && entry.isDirectory)
284             {
285                 result = findFiles(rofs, entry.name, recursive, dg);
286 
287                 if (result != 0)
288                     return result;
289             }
290         }
291     }
292     
293     finally
294     {
295         dir.close();
296     }
297 
298     return result;
299 }
300