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 }