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