1 /**
2  * Authors: Szabo Bogdan <szabobogdan@yahoo.com>
3  * Date: 2 25, 2015
4  * License: Subject to the	 terms of the MIT license, as written in the included LICENSE.txt file.
5  * Copyright: Public Domain
6  */
7 module vibedav.plugins.filedav;
8 
9 import vibedav.base;
10 
11 import vibe.core.log;
12 import vibe.core.file;
13 import vibe.inet.mimetypes;
14 import vibe.inet.message;
15 import vibe.http.server;
16 import vibe.http.fileserver;
17 import vibe.http.router : URLRouter;
18 import vibe.stream.operations;
19 import vibe.utils.dictionarylist;
20 import vibe.stream.stdio;
21 import vibe.stream.memory;
22 import vibe.utils.memory;
23 
24 import std.conv : to;
25 import std.file;
26 import std.path;
27 import std.digest.md;
28 import std.datetime;
29 import std..string;
30 import std.stdio;
31 import std.typecons;
32 import std.uri;
33 import std.uuid;
34 import std.exception;
35 
36 static if (__traits(compiles, { import std.algorithm.comparison : max; }))
37     import std.algorithm.comparison : max;
38 else
39     import std.algorithm;
40 
41 /// Compute a file etag
42 string eTag(string path) {
43 	import std.digest.crc;
44 	import std.stdio;
45 
46 	string fileHash = path;
47 
48 	if(!path.isDir) {
49 		auto f = File(path, "r");
50 		foreach (ubyte[] buffer; f.byChunk(4096)) {
51 			ubyte[4] hash = crc32Of(buffer);
52 			fileHash ~= crcHexString(hash);
53 		}
54 	}
55 
56 	fileHash ~= path.lastModified.toISOExtString ~ path.contentLength.to!string;
57 
58 	auto etag = hexDigest!MD5(path ~ fileHash);
59 	return etag.to!string;
60 }
61 
62 SysTime lastModified(string path) {
63 	FileInfo dirent = getFileInfo(path);
64 	return dirent.timeModified.toUTC;
65 }
66 
67 SysTime creationDate(string path) {
68 	FileInfo dirent = getFileInfo(path);
69 	return dirent.timeCreated.toUTC;
70 }
71 
72 ulong contentLength(string path) {
73 	FileInfo dirent = getFileInfo(path);
74 	return dirent.size;
75 }
76 
77 FileStream toStream(string path) {
78 	return openFile(path);
79 }
80 
81 Path[] getFolderContent(string format = "*")(string path, Path rootPath, Path rootUrl) {
82 	Path[] list;
83 	rootPath.endsWithSlash = true;
84 	string strRootPath = rootPath.toString;
85 
86 	auto p = Path(path);
87 	p.endsWithSlash = true;
88 	path = p.toString;
89 
90 	enforce(path.isDir, "I was expecting to get a dir content");
91 	enforce(strRootPath.length <= path.length);
92 	enforce(strRootPath == path[0..strRootPath.length]);
93 
94 	auto fileList = dirEntries(path, format, SpanMode.shallow);
95 
96   foreach(file; fileList) {
97 		auto filePath = Path(file[strRootPath.length..$]);
98 		filePath.endsWithSlash = false;
99 
100     list ~= filePath;
101 	}
102 
103 	return list;
104 }
105 
106 class DirectoryResourcePlugin : IDavResourcePlugin {
107 	private {
108 		Path baseUrlPath;
109 		Path basePath;
110 	}
111 
112 	this(Path baseUrlPath, Path basePath) {
113 		this.baseUrlPath = baseUrlPath;
114 		this.basePath = basePath;
115 	}
116 
117 	Path filePath(URL url) {
118 		return getFilePath(baseUrlPath, basePath, url);
119 	}
120 
121 	pure nothrow {
122 		bool canSetContent(DavResource resource) {
123 			return false;
124 		}
125 
126 		bool canGetStream(DavResource resource) {
127 			return false;
128 		}
129 
130 		bool canGetProperty(DavResource resource, string name) {
131 			return false;
132 		}
133 
134 		bool canSetProperty(DavResource resource, string name) {
135 			return false;
136 		}
137 
138 		bool canRemoveProperty(DavResource resource, string name) {
139 			return false;
140 		}
141 	}
142 
143 	void setContent(DavResource resource, const ubyte[] content) {
144 		throw new DavException(HTTPStatus.internalServerError, "Can't set directory stream.");
145 	}
146 
147 	void setContent(DavResource resource, InputStream content, ulong size) {
148 		throw new DavException(HTTPStatus.internalServerError, "Can't set directory stream.");
149 	}
150 
151 	InputStream stream(DavResource resource) {
152 		throw new DavException(HTTPStatus.internalServerError, "Can't get directory stream.");
153 	}
154 
155 	void copyPropertiesTo(URL source, URL destination) {
156 
157 	}
158 
159 	DavProp property(DavResource resource, string name) {
160 		throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
161 	}
162 
163 	HTTPStatus setProperty(DavResource resource, string name, DavProp prop) {
164 		throw new DavException(HTTPStatus.internalServerError, "Can't set property.");
165 	}
166 
167 	HTTPStatus removeProperty(DavResource resource, string name) {
168 		throw new DavException(HTTPStatus.internalServerError, "Can't remove property.");
169 	}
170 
171 	pure nothrow @property {
172 		string name() {
173 			return "DirectoryResourcePlugin";
174 		}
175 	}
176 }
177 
178 class FileResourcePlugin : IDavResourcePlugin {
179 	private {
180 		Path baseUrlPath;
181 		Path basePath;
182 	}
183 
184 	this(Path baseUrlPath, Path basePath) {
185 		this.baseUrlPath = baseUrlPath;
186 		this.basePath = basePath;
187 	}
188 
189 	Path filePath(URL url) {
190 		return getFilePath(baseUrlPath, basePath, url);
191 	}
192 
193 	bool canSetContent(DavResource resource) {
194 		auto filePath = filePath(resource.url).toString;
195 		return filePath.exists;
196 	}
197 
198 	bool canGetStream(DavResource resource) {
199 		return canSetContent(resource);
200 	}
201 
202 	bool canGetProperty(DavResource resource, string name) {
203 		return false;
204 	}
205 
206 	bool canSetProperty(DavResource resource, string name) {
207 		return false;
208 	}
209 
210 	bool canRemoveProperty(DavResource resource, string name) {
211 		return false;
212 	}
213 
214 	void setContent(DavResource resource, const ubyte[] content) {
215 		auto filePath = filePath(resource.url).toString;
216 		std.stdio.write(filePath, content);
217 	}
218 
219 	void setContent(DavResource resource, InputStream content, ulong size) {
220 		auto nativePath = filePath(resource.url).toString;
221 
222 		auto tmpPath = nativePath ~ ".tmp";
223 		auto tmpFile = File(tmpPath, "w");
224 
225 		while(!content.empty) {
226 			auto leastSize = content.leastSize;
227 			ubyte[] buf;
228 			buf.length = leastSize;
229 			content.read(buf);
230 			tmpFile.rawWrite(buf);
231 		}
232 
233 		tmpFile.flush;
234 
235 		std.file.copy(tmpPath, nativePath);
236 		std.file.remove(tmpPath);
237 	}
238 
239 	InputStream stream(DavResource resource) {
240 		auto nativePath = filePath(resource.url).toString;
241 		return nativePath.toStream;
242 	}
243 
244 	DavProp property(DavResource resource, string name) {
245 		throw new DavException(HTTPStatus.internalServerError, "Can't get property.");
246 	}
247 
248 	void copyPropertiesTo(URL source, URL destination) {
249 
250 	}
251 
252 	HTTPStatus setProperty(DavResource resource, string name, DavProp prop) {
253 		throw new DavException(HTTPStatus.internalServerError, "Can't set property.");
254 	}
255 
256 	HTTPStatus removeProperty(DavResource resource, string name) {
257 		throw new DavException(HTTPStatus.internalServerError, "Can't remove property.");
258 	}
259 
260 	pure nothrow @property {
261 		string name() {
262 			return "FileResourcePlugin";
263 		}
264 	}
265 }
266 
267 /// File Dav impplementation
268 class FileDav : BaseDavPlugin {
269 
270 	protected {
271 		Path baseUrlPath;
272 		Path basePath;
273 	}
274 
275 	this(IDav dav, Path baseUrlPath, Path basePath) {
276 		super(dav);
277 		this.baseUrlPath = baseUrlPath;
278 		this.basePath = basePath;
279 	}
280 
281 	protected {
282 		void setResourceProperties(DavResource resource) {
283 			string path = filePath(resource.url).toString;
284 			assert(path.exists, "Can't set basic properties. The path does not exist.");
285 
286 			setResourceInfoProperties(resource);
287 
288 			if(path.isDir)
289 				setCollection(resource);
290 		}
291 
292 		void setCollection(DavResource resource) {
293 			resource.resourceType ~= "collection:DAV:";
294 		}
295 
296 		void setResourceInfoProperties(DavResource resource) {
297 			string path = filePath(resource.url).toString;
298 
299 			assert(path.exists);
300 
301 			resource.creationDate = creationDate(path);
302 			resource.lastModified = lastModified(path);
303 			resource.eTag = eTag(path);
304 			resource.contentType = getMimeTypeForFile(path);
305 			resource.contentLength = contentLength(path);
306 			resource.name = baseName(path);
307 		}
308 	}
309 
310 	Path filePath(URL url) {
311 		return getFilePath(baseUrlPath, basePath, url);
312 	}
313 
314 	override {
315 		bool exists(URL url, string username) {
316 			auto filePath = filePath(url);
317 
318 			return filePath.toString.exists;
319 		}
320 
321 
322   	Path[] childList(URL url, string username) {
323   		Path[] list;
324 
325   		auto nativePath = filePath(url).toString;
326 
327   		if(nativePath.exists)
328   			list = getFolderContent!"*"(nativePath, basePath, baseUrlPath);
329 
330   		return list;
331   	}
332 
333 
334 		bool canCreateResource(URL url, string username) {
335 			return !exists(url, username);
336 		}
337 
338 		bool canCreateCollection(URL url, string username) {
339 			return !exists(url, username);
340 		}
341 
342 		void removeResource(URL url, string username) {
343 			if(!exists(url, username))
344 				throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found.");
345 
346 			auto filePath = filePath(url).toString;
347 
348 			if(filePath.isDir)
349 				filePath.rmdirRecurse;
350 			else
351 				filePath.remove;
352 		}
353 
354 		DavResource getResource(URL url, string username) {
355 			if(!exists(url, username))
356 				throw new DavException(HTTPStatus.notFound, "`" ~ url.toString ~ "` not found.");
357 
358 			auto filePath = filePath(url);
359 
360 			DavResource resource = new DavResource(_dav, url);
361 			resource.username = username;
362 			setResourceProperties(resource);
363 
364 			return resource;
365 		}
366 
367 		DavResource createResource(URL url, string username) {
368 			auto filePath = filePath(url).toString;
369 
370 			File(filePath, "w");
371 
372 			return getResource(url, username);
373 		}
374 
375 		DavResource createCollection(URL url, string username) {
376 			auto filePath = filePath(url);
377 
378 			if(filePath.toString.exists)
379 				throw new DavException(HTTPStatus.methodNotAllowed, "Resource already exists.");
380 
381 			filePath.toString.mkdirRecurse;
382 
383 			return getResource(url, username);
384 		}
385 
386 		void bindResourcePlugins(DavResource resource) {
387 			if(resource.isCollection)
388 				resource.registerPlugin(new DirectoryResourcePlugin(baseUrlPath, basePath));
389 			else
390 				resource.registerPlugin(new FileResourcePlugin(baseUrlPath, basePath));
391 
392 			resource.registerPlugin(new ResourceCustomProperties);
393 			resource.registerPlugin(new ResourceBasicProperties);
394 		}
395 
396 		@property {
397 			IDav dav() {
398 				return _dav;
399 			}
400 
401 			string[] support(URL url, string username) {
402 				return ["1", "2", "3"];
403 			}
404 		}
405 	}
406 
407 	@property {
408 		string name() {
409 			return "FileDav";
410 		}
411 	}
412 }
413 
414 IDav serveFileDav(URLRouter router, string rootUrl, string rootPath) {
415 	rootUrl = rootUrl.stripSlashes;
416 	rootPath = rootPath.stripSlashes;
417 
418 	auto dav = new Dav(rootUrl);
419 	auto fileDav = new FileDav(dav, Path(rootUrl), Path(rootPath));
420 
421 	if(rootUrl != "") rootUrl = "/"~rootUrl~"/";
422 
423 	router.any(rootUrl ~ "*", serveDav(dav));
424 
425 	return dav;
426 }