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 an empty directory. 34 void mkdir(string path) { return getVFS(path).mkdir(path); } 35 36 /// Create directory (and parents as necessary) at location, if it does not exist. 37 void mkdirRecurse(string path) { return getVFS(path).mkdirRecurse(path); } 38 39 /// Rename file at location. Clobber destination, if it exists. 40 void rename(string from, string to) 41 { 42 if (getVFSName(from) == getVFSName(to)) 43 return getVFS(from).rename(from, to); 44 else 45 throw new Exception("Cannot rename across VFS"); 46 } 47 48 /// Enumerate directory entries. 49 /// Returns an array of file/directory names only. 50 string[] listDir(string path) { return getVFS(path).listDir(path); } 51 52 /// Remove a directory and all its contents recursively. 53 void rmdirRecurse(string path) { return getVFS(path).rmdirRecurse(path); } 54 55 /// Get MD5 digest of file at location. 56 ubyte[16] mdFile(string path) { return getVFS(path).mdFile(path); } 57 58 /// std.file shims 59 S readText(S = string)(string name) 60 { 61 auto result = cast(S) read(name); 62 import std.utf; 63 validate(result); 64 return result; 65 } 66 67 /// ditto 68 void copy(string from, string to) 69 { 70 if (getVFSName(from) == getVFSName(to)) 71 getVFS(from).copy(from, to); 72 else 73 write(to, read(from)); 74 } 75 76 /// ae.sys.file shims 77 void move(string src, string dst) 78 { 79 try 80 src.rename(dst); 81 catch (Exception e) 82 { 83 auto tmp = dst ~ ".ae-tmp"; 84 if (tmp.exists) tmp.remove(); 85 scope(exit) if (tmp.exists) tmp.remove(); 86 src.copy(tmp); 87 tmp.rename(dst); 88 src.remove(); 89 } 90 } 91 92 /// ditto 93 void ensurePathExists(string fn) 94 { 95 import std.path; 96 auto path = dirName(fn); 97 if (!exists(path)) 98 mkdirRecurse(path); 99 } 100 101 /// ditto 102 void safeWrite(string fn, in void[] data) 103 { 104 auto tmp = fn ~ ".ae-tmp"; 105 write(tmp, data); 106 if (fn.exists) fn.remove(); 107 tmp.rename(fn); 108 } 109 110 /// ditto 111 void touch(string path) 112 { 113 if (!getVFSName(path)) 114 return ae.sys.file.touch(path); 115 else 116 safeWrite(path, read(path)); 117 } 118 119 120 // Implementer interface: 121 122 /// Abstract VFS driver base class. 123 class VFS 124 { 125 /// Read entire file at given location. 126 abstract void[] read(string path); 127 128 /// Write entire file at given location (overwrite if exists). 129 abstract void write(string path, const(void)[] data); 130 131 /// Check if file/directory exists at location. 132 abstract bool exists(string path); 133 134 /// Delete file at location. 135 abstract void remove(string path); 136 137 /// Copy file from one location to another (same VFS driver). 138 void copy(string from, string to) { write(to, read(from)); } 139 140 /// Rename file at location. Clobber destination, if it exists. 141 void rename(string from, string to) { copy(from, to); remove(from); } 142 143 /// Create an empty directory. 144 void mkdir(string path) { assert(false, "Not implemented"); } 145 146 /// Create directory (and parents as necessary) at location, if it does not exist. 147 abstract void mkdirRecurse(string path); 148 149 /// Get MD5 digest of file at location. 150 ubyte[16] mdFile(string path) { import std.digest.md; return md5Of(read(path)); } 151 152 /// Enumerate directory entries. 153 string[] listDir(string path) { assert(false, "Not implemented"); } 154 155 /// Remove a directory and all its contents recursively. 156 void rmdirRecurse(string path) { assert(false, "Not implemented"); } 157 } 158 159 VFS[string] registry; 160 161 /// Test a VFS at a certain path. Must end with directory separator. 162 void testVFS(string base) 163 { 164 import std.exception; 165 166 auto testPath0 = base ~ "ae-test0.txt"; 167 auto testPath1 = base ~ "ae-test1.txt"; 168 169 scope(exit) if (testPath0.exists) testPath0.remove(); 170 scope(exit) if (testPath1.exists) testPath1.remove(); 171 172 write(testPath0, "Hello"); 173 assert(testPath0.exists); 174 assert(readText(testPath0) == "Hello"); 175 176 copy(testPath0, testPath1); 177 assert(testPath1.exists); 178 assert(readText(testPath1) == "Hello"); 179 180 remove(testPath0); 181 assert(!testPath0.exists); 182 assertThrown(testPath0.readText()); 183 184 rename(testPath1, testPath0); 185 assert(testPath0.exists); 186 assert(readText(testPath0) == "Hello"); 187 assert(!testPath1.exists); 188 assertThrown(testPath1.readText()); 189 } 190 191 // Other: 192 193 bool isVFSPath(string path) 194 { 195 import ae.utils.text; 196 return path.contains("://"); 197 } 198 199 string getVFSName(string path) 200 { 201 import std..string; 202 auto index = indexOf(path, "://"); 203 return index > 0 ? path[0..index] : null; 204 } 205 206 VFS getVFS(string path) 207 { 208 auto vfsName = getVFSName(path); 209 auto pvfs = vfsName in registry; 210 assert(pvfs, "Unknown VFS: " ~ vfsName); 211 return *pvfs; 212 } 213 214 private: 215 216 static import std.file, ae.sys.file; 217 218 ///////////////////////////////////////////////////////////////////////////// 219 220 /// Pass-thru native filesystem driver. 221 class FS : VFS 222 { 223 override void[] read(string path) { return std.file.read(path); } 224 override void write(string path, const(void)[] data) { return std.file.write(path, data); } 225 override bool exists(string path) { return std.file.exists(path); } 226 override void remove(string path) { return std.file.remove(path); } 227 override void copy(string from, string to) { std.file.copy(from, to); } 228 override void rename(string from, string to) { std.file.rename(from, to); } 229 override void mkdirRecurse(string path) { std.file.mkdirRecurse(path); } 230 override ubyte[16] mdFile(string path) { return ae.sys.file.mdFile(path); } 231 232 override string[] listDir(string path) 233 { 234 import std.algorithm, std.path, std.array; 235 return std.file.dirEntries(path, std.file.SpanMode.shallow).map!(de => de.baseName).array; 236 } 237 238 override void mkdir(string path) { std.file.mkdir(path); } 239 override void rmdirRecurse(string path) { std.file.rmdirRecurse(path); } 240 241 static this() 242 { 243 registry[null] = new FS(); 244 } 245 } 246 247 unittest 248 { 249 testVFS(""); 250 }