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