1 /** 2 * A basic virtual filesystem API. 3 * Intended as a drop-in std.file replacement. 4 * VFS driver is indicated by "driver://" prefix 5 * ("//" cannot exist in a valid filesystem path). 6 * 7 * License: 8 * This Source Code Form is subject to the terms of 9 * the Mozilla Public License, v. 2.0. If a copy of 10 * the MPL was not distributed with this file, You 11 * can obtain one at http://mozilla.org/MPL/2.0/. 12 * 13 * Authors: 14 * Vladimir Panteleev <vladimir@thecybershadow.net> 15 */ 16 17 module ae.sys.vfs; 18 19 // User interface: 20 21 /// Read entire file at given location. 22 void[] read(string path) { return getVFS(path).read(path); } 23 24 /// Write entire file at given location (overwrite if exists). 25 void write(string path, const(void)[] data) { return getVFS(path).write(path, data); } 26 27 /// Check if file/directory exists at location. 28 @property bool exists(string path) { return getVFS(path).exists(path); } 29 30 /// Delete file at location. 31 void remove(string path) { return getVFS(path).remove(path); } 32 33 /// Create directory ( and parents as necessary) at location, if it does not exist. 34 void mkdirRecurse(string path) { return getVFS(path).mkdirRecurse(path); } 35 36 /// Rename file at location. Clobber destination, if it exists. 37 void rename(string from, string to) 38 { 39 if (getVFSName(from) == getVFSName(to)) 40 return getVFS(from).rename(from, to); 41 else 42 throw new Exception("Cannot rename across VFS"); 43 } 44 45 /// Get MD5 digest of file at location. 46 ubyte[16] mdFile(string path) { return getVFS(path).mdFile(path); } 47 48 /// std.file shims 49 S readText(S = string)(string name) 50 { 51 auto result = cast(S) read(name); 52 import std.utf; 53 validate(result); 54 return result; 55 } 56 57 /// ditto 58 void copy(string from, string to) 59 { 60 if (getVFSName(from) == getVFSName(to)) 61 getVFS(from).copy(from, to); 62 else 63 write(to, read(from)); 64 } 65 66 /// ae.sys.file shims 67 void move(string src, string dst) 68 { 69 try 70 src.rename(dst); 71 catch (Exception e) 72 { 73 auto tmp = dst ~ ".ae-tmp"; 74 if (tmp.exists) tmp.remove(); 75 scope(exit) if (tmp.exists) tmp.remove(); 76 src.copy(tmp); 77 tmp.rename(dst); 78 src.remove(); 79 } 80 } 81 82 /// ditto 83 void ensurePathExists(string fn) 84 { 85 import std.path; 86 auto path = dirName(fn); 87 if (!exists(path)) 88 mkdirRecurse(path); 89 } 90 91 /// ditto 92 void safeWrite(string fn, in void[] data) 93 { 94 auto tmp = fn ~ ".ae-tmp"; 95 write(tmp, data); 96 if (fn.exists) fn.remove(); 97 tmp.rename(fn); 98 } 99 100 /// ditto 101 void touch(string path) 102 { 103 if (!getVFSName(path)) 104 return ae.sys.file.touch(path); 105 else 106 safeWrite(path, read(path)); 107 } 108 109 110 // Implementer interface: 111 112 /// Abstract VFS driver base class. 113 class VFS 114 { 115 /// Read entire file at given location. 116 abstract void[] read(string path); 117 118 /// Write entire file at given location (overwrite if exists). 119 abstract void write(string path, const(void)[] data); 120 121 /// Check if file/directory exists at location. 122 abstract bool exists(string path); 123 124 /// Delete file at location. 125 abstract void remove(string path); 126 127 /// Copy file from one location to another (same VFS driver). 128 void copy(string from, string to) { write(to, read(from)); } 129 130 /// Rename file at location. Clobber destination, if it exists. 131 void rename(string from, string to) { copy(from, to); remove(from); } 132 133 /// Create directory (and parents as necessary) at location, if it does not exist. 134 abstract void mkdirRecurse(string path); 135 136 /// Get MD5 digest of file at location. 137 ubyte[16] mdFile(string path) { import std.digest.md; return md5Of(read(path)); } 138 } 139 140 VFS[string] registry; 141 142 /// Test a VFS at a certain path. Must end with directory separator. 143 void testVFS(string base) 144 { 145 import std.exception; 146 147 auto testPath0 = base ~ "ae-test0.txt"; 148 auto testPath1 = base ~ "ae-test1.txt"; 149 150 scope(exit) if (testPath0.exists) testPath0.remove(); 151 scope(exit) if (testPath1.exists) testPath1.remove(); 152 153 write(testPath0, "Hello"); 154 assert(testPath0.exists); 155 assert(readText(testPath0) == "Hello"); 156 157 copy(testPath0, testPath1); 158 assert(testPath1.exists); 159 assert(readText(testPath1) == "Hello"); 160 161 remove(testPath0); 162 assert(!testPath0.exists); 163 assertThrown(testPath0.readText()); 164 165 rename(testPath1, testPath0); 166 assert(testPath0.exists); 167 assert(readText(testPath0) == "Hello"); 168 assert(!testPath1.exists); 169 assertThrown(testPath1.readText()); 170 } 171 172 // Other: 173 174 bool isVFSPath(string path) 175 { 176 import ae.utils.text; 177 return path.contains("://"); 178 } 179 180 string getVFSName(string path) 181 { 182 import std.string; 183 auto index = indexOf(path, "://"); 184 return index > 0 ? path[0..index] : null; 185 } 186 187 VFS getVFS(string path) 188 { 189 auto vfsName = getVFSName(path); 190 auto pvfs = vfsName in registry; 191 assert(pvfs, "Unknown VFS: " ~ vfsName); 192 return *pvfs; 193 } 194 195 private: 196 197 static import std.file, ae.sys.file; 198 199 ///////////////////////////////////////////////////////////////////////////// 200 201 /// Pass-thru native filesystem driver. 202 class FS : VFS 203 { 204 override void[] read(string path) { return std.file.read(path); } 205 override void write(string path, const(void)[] data) { return std.file.write(path, data); } 206 override bool exists(string path) { return std.file.exists(path); } 207 override void remove(string path) { return std.file.remove(path); } 208 override void copy(string from, string to) { std.file.copy(from, to); } 209 override void rename(string from, string to) { std.file.rename(from, to); } 210 override void mkdirRecurse(string path) { std.file.mkdirRecurse(path); } 211 override ubyte[16] mdFile(string path) { return ae.sys.file.mdFile(path); } 212 213 static this() 214 { 215 registry[null] = new FS(); 216 } 217 } 218 219 unittest 220 { 221 testVFS(""); 222 }