1 
2 //          Copyright Ferdinand Majerech 2011 - 2012.
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          http://www.boost.org/LICENSE_1_0.txt)
6 
7 
8 ///Stacked files/directories for seamless access to multiple directories as if they were a single directory.
9 module dgamevfs.stack;
10 
11 
12 import std.algorithm;
13 import std.exception;
14 import std.typecons;
15 import std.range;
16 
17 import dgamevfs.exceptions;
18 import dgamevfs.vfs;
19 import dgamevfs.util;
20 
21 
22 /**
23  * A directory seamlessly working on a stack of multiple directories.
24  *
25  * Directories can be mounted using the $(D mount()) method.
26  *
27  * When looking for a file or directory in a $(D StackDir), the last directory
28  * is searched first, then the second last, and so on. This means that directories
29  * mounted later override those mounted before.
30  *
31  * Example:
32  *
33  * We have a directory called $(I data) with the following contents:
34  * --------------------
35  * shaders:
36  *     font.frag 
37  *     font.vert 
38  * logs:
39  *     (empty)
40  * main.cfg
41  * --------------------
42  * and a directory called $(I user_data) with the following contents:
43  * --------------------
44  * shaders: 
45  *      font.frag
46  * logs:
47  *      (empty)
48  * custom.cfg
49  * --------------------
50  * the following code will work as specified in the comments:
51  * --------------------
52  * VFSDir data, user_data; //initialized somewhere before
53  * 
54  * auto stack = new StackDir("stack");
55  * stack.mount(data);
56  * stack.mount(user_data);
57  *
58  * //This will access user_data/shaders/font.frag
59  * auto frag = stack.file("shaders/font.frag");
60  * //This will access data/shaders/font.vert
61  * auto vert = stack.file("shaders/font.vert");
62  * //This will return a StackDir (as VFSDir) with "data/logs" and "user_data/logs"
63  * //mounted, in that order:
64  * auto logs = stack.dir("logs");
65  * --------------------
66  *
67  * Accessing a file in a $(D StackDir) will actually return a $(D StackFile),
68  * which decides which file to access on read, write and other operations.
69  * The $(D StackFile) is a stack of all files that map to the same path in
70  * the $(D StackDir) in the same order as $(D StackDir)'s mounted directories.
71  *
72  * For example, when reading or determining file size, the directories in the 
73  * stack will be searched from newest to oldest and the first file found will 
74  * be used.
75  *
76  * When writing, the file in the newest writable directory will be written to.
77  *
78  *
79  * In some cases, it might be required to access a particular directory in the 
80  * stack. E.g. a game might have multiple packages stacked on top of each other,
81  * but sometimes default, non-overridden version of a file could be needed.
82  * This can be done using the $(B ::) separator. 
83  *
84  * In the context of the previous example:
85  *
86  * --------------------
87  * //This will access data/shaders/font.frag even though user_data/shaders/font.frag exists
88  * auto default_frag = stack.file("data::shaders/font.frag");
89  * --------------------
90  *
91  * $(D StackDir) is considered writable when any directory in the stack is writable.
92  * Similarly, it exists when any directory in the stack exists.
93  *
94  * When we have a $(D StackDir) that does not exist and we $(D create()) it,
95  * the newest directory that is writable will be created.
96  * (This can happen when getting a nonexistent subdirectory of a $(D StackDir).)
97  */
98 class StackDir : VFSDir
99 {
100     private:
101         //Directory stack.
102         VFSDir[] stack_;
103 
104     public:
105         /**
106          * Construct a  $(D StackDir).
107          * 
108          * Params:  name = Name of the  $(D StackDir).
109          *
110          * Throws:  $(D VFSInvalidPathException) if name is not valid (contains '/' or "::").
111          */
112         this(string name) @safe pure
113         {
114             enforce(noSeparators(name),
115                     invalidPath("Invalid directory name: ", name));
116             super(null, name);
117         }
118 
119         override @property bool writable() @safe pure nothrow const @nogc 
120         {
121             foreach(pkg; stack_) if(pkg.writable)
122             {
123                 return true;
124             }
125             return false;
126         }
127 
128         override @property bool exists() @safe nothrow const
129         {
130             foreach(pkg; stack_) if(pkg.exists)
131             {
132                 return true;
133             }
134             return false;
135         }
136 
137         override VFSFile file(string path)
138         {
139             enforce(exists, 
140                     notFound("Trying to access file ", path, " in stack directory ",
141                               this.path, " that does not exist"));
142             enforce(stack_. length > 0, 
143                     notFound("Trying to access file ", path, " in stack directory",
144                               this.path, " which has no mounted directories"));
145 
146             string rest;
147             const pkg = expectPackage(path, rest);
148             //explicit package
149             if(pkg !is null)
150             {
151                 foreach_reverse(dir; stack_) if(dir.name == pkg)
152                 {
153                     return dir.file(rest);
154                 }
155             }
156 
157             //no package
158             VFSFile[] stack;
159             foreach(dir; stack_)
160             {
161                 VFSFile file;
162                 try
163                 {
164                     file = dir.file(path);
165                 }
166                 catch(VFSNotFoundException e){continue;}
167                 stack ~= file;
168             }
169             enforce(!stack.empty,
170                     notFound("Unable to find file ", path, " in stack directory ", this.path));
171 
172             return new StackFile(this, path, stack);
173         }
174 
175         override VFSDir dir(string path) @safe
176         {
177             enforce(exists, 
178                     notFound("Trying to access subdirectory ", path, 
179                               " in stack directory ", this.path, " that does not exist"));
180             enforce(stack_. length > 0, 
181                     notFound("Trying to access subdirectory ", path, " in stack directory",
182                               this.path, " which has no mounted directories"));
183 
184             string rest;
185             const pkg = expectPackage(path, rest);
186             //explicit package
187             if(pkg !is null)
188             {
189                 foreach_reverse(dir; stack_) if(dir.name == pkg)
190                 {
191                     return dir.dir(rest);
192                 }
193             }
194 
195             //no package
196             VFSDir[] stack;
197             foreach(dir; stack_)
198             {
199                 VFSDir subdir;
200                 try{subdir = dir.dir(path);}
201                 catch(VFSNotFoundException e){continue;}
202                 stack ~= subdir;
203             }
204             enforce(!stack.empty,
205                     notFound("Unable to find directory ", path, " in stack directory ", this.path));
206 
207             //Note that dirs in returned StackDir don't have that StackDir as parent.
208             //Their paths are still correctly resolved through their respective parents.
209             return new StackDir(this, path, stack);
210         }
211 
212         override VFSFiles files(Flag!"deep" deep = No.deep, string glob = null) @trusted
213         {
214             enforce(exists, 
215                     notFound("Trying to access files of stack directory ", 
216                               this.path, " that does not exist"));
217 
218             auto files = new VFSFiles.Items;
219             //items inserted earlier to a RBTree override ones inserted later
220             //so we insert directories in reverse order.
221             //Then the newer directories override the older ones.
222             foreach_reverse(dir; stack_)
223             {
224                 if(!dir.exists){continue;}
225                 foreach(file; dir.files(deep, glob))
226                 {
227                     files.insert(file);
228                 }
229             }
230 
231             return filesRange(files);
232         }
233 
234         override VFSDirs dirs(Flag!"deep" deep = No.deep, string glob = null) @trusted
235         {
236             enforce(exists, 
237                     notFound("Trying to access subdirectories of stack directory ", 
238                               this.path, " that does not exist"));
239 
240             auto dirs = new VFSDirs.Items;
241             //Items inserted earlier to a RBTree override ones inserted later
242             //so we insert directories in reverse order.
243             //Then the newer directories override the older ones.
244             foreach_reverse(dir; stack_)
245             {
246                 if(!dir.exists){continue;}
247                 foreach(subdir; dir.dirs(deep, glob))
248                 {
249                     dirs.insert(subdir);
250                 }
251             }
252 
253             return dirsRange(dirs);
254         }
255 
256         /**
257          * Mount a directory.
258          *
259          * Files and directories of a directory mounted later will override
260          * those of a directory mounted earlier.
261          *
262          * If dir has a parent in the VFS, a parent-less copy will be created and 
263          * mounted. (This has no effect whatsoever on the underlying filesystem -
264          * it just removes the need for directories to have multiple parents).
265          *
266          * Params:  dir = Directory to _mount.
267          *
268          * Throws:  $(D VFSMountException) if a directory with the same name is
269          *          already mounted, or if dir has this directory as its child 
270          *          or a child of any of its subdirectories (circular mounting).
271          */ 
272         void mount(VFSDir dir) @safe
273         {
274             enforce(!canFind!((a, b){return a.name == b.name;})(stack_, dir),
275                     mountError("Could not mount directory ", dir.path, " to stacked "
276                                 "directory ", this.path, " as there is already a "
277                                 "mounted directory  with the same name"));
278             if(dir.parent !is null)
279             {
280                 dir = getCopyWithoutParent(dir);
281             }
282 
283             VFSDir parent = this.parent;
284             while(parent !is null)
285             {
286                 if(parent is dir)
287                 {
288                     throw mountError("Attemted to circularly mount directory ",
289                                       dir.path, " to stacked directory ", this.path);
290                 }
291                 parent = parent.parent;
292             }
293             stack_ ~= dir;
294             dir.parent = this;
295         }
296 
297         override void remove()
298         {
299             const removable = !stack_.canFind!((d) => !d.writable)();
300             enforce(removable,
301                     ioError("Couldn't remove stack directory ", path, " at ",
302                             "least one directory in the stack is not writable"));
303             foreach(dir; stack_)
304             {
305                 dir.remove();
306             }
307         }
308 
309     protected:
310         override string composePath(const VFSDir child) @safe const pure nothrow
311         {
312             //child is in stack_ - override its path:
313             foreach(pkg; stack_) if (pkg is child)
314             {
315                 return path;
316             }
317             //child was returned by dir():
318             return path ~ "/" ~ child.name;
319         }
320 
321         override void create_()
322         {
323             foreach_reverse(dir; stack_) if(dir.writable)
324             {
325                 dir.create();
326                 return;
327             }
328             assert(false, "create_() called on a non-writable StackDir");
329         }
330 
331         override VFSDir copyWithoutParent()
332         {
333             auto result = new StackDir(name);
334             foreach(dir; stack_)
335             {
336                 result.mount(getCopyWithoutParent(dir));
337             }
338             return result;
339         }
340 
341     private:
342         /**
343          * Construct a stack directory as a subdirectory of parent.
344          *
345          * Params:  parent       = Parent directory.
346          *          pathInParent = Path of the subdir in all directories of parent's stack.
347          *          stack        = Directory stack of the subdirectory.
348          */
349         this(StackDir parent, string pathInParent, VFSDir[] stack) @safe pure nothrow @nogc
350         {
351             super(parent, pathInParent);
352             stack_ = stack;
353         }
354 }
355 
356 /**
357  * A file seamlessly working on a stack of multiple files.
358  *
359  * This is the file implementation returned by  $(D StackDir) methods.
360  *
361  * It has one file from each directory in the  $(D StackDir) - all of these
362  * files map to the same path in the $(D StackDir).
363  *
364  * When reading from $(D StackFile), it will read from the newest file
365  * in the stack that exists.
366  *
367  * When writing, it will write to the newest file that is writable
368  * regardless of whether it already exists or not.
369  */
370 class StackFile : VFSFile 
371 {
372     private:
373         ///File stack.
374         VFSFile[] stack_;
375 
376         ///Currently open file, if any.
377         VFSFile openFile_ = null;
378 
379     public:
380         override @property ulong bytes() const
381         {
382             //Get size of the newest file that exists.
383             foreach_reverse(file; stack_) if(file.exists)
384             {
385                 return file.bytes;
386             }
387             throw notFound("Trying to get size of a non-existent file: ", path);
388         }
389 
390         override @property bool exists() const
391         {
392             //If any file in the stack exists, this file exists.
393             foreach_reverse(file; stack_) if(file.exists)
394             {
395                 return true;
396             }
397             return false;
398         }
399 
400         override @property bool open() const {return openFile_ !is null;}
401 
402     protected:
403         override void openRead()
404         {
405             assert(openFile_ is null, "Trying to open a file that is already open: " ~ path);
406 
407             //Choose the file to read from - will read from the newest file that exists.
408             foreach_reverse(file; stack_) if(file.exists)
409             {
410                 openFile_ = file;
411                 openReadProxy(openFile_);
412                 return;
413             }
414             assert(false, "Trying to open a non-existent file for reading: " ~ path);
415         }
416 
417         override void openWrite(Flag!"append" append)
418         {
419             assert(openFile_ is null, "Trying to open a file that is already open: " ~ path);
420             assert(writable, "Trying open a non-writable file for writing: " ~ path);
421                                      
422             //Choose the file to write to - will write to the newest file that is writable.
423             foreach_reverse(file; stack_) if(file.writable)
424             {
425                 openFile_ = file;
426                 openWriteProxy(openFile_, append);
427                 return;
428             }
429             //At least one file must be writable, so this can not be reached.
430             assert(false);
431         }
432 
433         override void[] read(void[] target)
434         {
435             assert(openFile_ !is null, "Trying to read from an unopened file: " ~ path);
436             return readProxy(openFile_, target);
437         }
438 
439         override void write(in void[] data)
440         {
441             assert(openFile_ !is null, "Trying to write to an unopened file: " ~ path);
442             assert(writable, "Trying to write to a non-writable file");
443             writeProxy(openFile_, data);
444         }
445 
446         override void seek(long offset, Seek origin)
447         {
448             assert(openFile_ !is null, "Trying to seek in an unopened file: " ~ path);
449             seekProxy(openFile_, offset, origin);
450         }
451 
452         override void close()
453         {
454             assert(openFile_ !is null, "Trying to close an unopened file: " ~ path);
455             closeProxy(openFile_);
456             openFile_ = null;
457         }
458 
459     private:
460         /**
461          * Construct a $(D StackFile).
462          *
463          * Params:  parent       = Parent directory of the file.
464          *          pathInParent = Path of the file within the parent directory.
465          *          stack        = File stack.
466          */
467         this(StackDir parent, string pathInParent, VFSFile[] stack)
468         {
469             super(parent, pathInParent);
470             stack_ = stack;
471         }
472 }