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 }