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, const(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 }