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