1 /**
2 	File handling functions and types.
3 
4 	Copyright: © 2012-2019 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibe.core.file;
9 
10 import eventcore.core : NativeEventDriver, eventDriver;
11 import eventcore.driver;
12 import vibe.core.internal.release;
13 import vibe.core.log;
14 import vibe.core.path;
15 import vibe.core.stream;
16 import vibe.core.task : Task, TaskSettings;
17 import vibe.internal.async : asyncAwait, asyncAwaitUninterruptible;
18 
19 import core.stdc.stdio;
20 import core.sys.posix.unistd;
21 import core.sys.posix.fcntl;
22 import core.sys.posix.sys.stat;
23 import core.time;
24 import std.conv : octal;
25 import std.datetime;
26 import std.exception;
27 import std.file;
28 import std.path;
29 import std..string;
30 import std.typecons : Flag, No;
31 
32 
33 version(Posix){
34 	private extern(C) int mkstemps(char* templ, int suffixlen);
35 }
36 
37 @safe:
38 
39 
40 /**
41 	Opens a file stream with the specified mode.
42 */
43 FileStream openFile(NativePath path, FileMode mode = FileMode.read)
44 {
45 	auto fil = eventDriver.files.open(path.toNativeString(), cast(FileOpenMode)mode);
46 	enforce(fil != FileFD.invalid, "Failed to open file '"~path.toNativeString~"'");
47 	return FileStream(fil, path, mode);
48 }
49 /// ditto
50 FileStream openFile(string path, FileMode mode = FileMode.read)
51 {
52 	return openFile(NativePath(path), mode);
53 }
54 
55 
56 /**
57 	Read a whole file into a buffer.
58 
59 	If the supplied buffer is large enough, it will be used to store the
60 	contents of the file. Otherwise, a new buffer will be allocated.
61 
62 	Params:
63 		path = The path of the file to read
64 		buffer = An optional buffer to use for storing the file contents
65 */
66 ubyte[] readFile(NativePath path, ubyte[] buffer = null, size_t max_size = size_t.max)
67 {
68 	auto fil = openFile(path);
69 	scope (exit) fil.close();
70 	enforce(fil.size <= max_size, "File is too big.");
71 	auto sz = cast(size_t)fil.size;
72 	auto ret = sz <= buffer.length ? buffer[0 .. sz] : new ubyte[sz];
73 	fil.read(ret);
74 	return ret;
75 }
76 /// ditto
77 ubyte[] readFile(string path, ubyte[] buffer = null, size_t max_size = size_t.max)
78 {
79 	return readFile(NativePath(path), buffer, max_size);
80 }
81 
82 
83 /**
84 	Write a whole file at once.
85 */
86 void writeFile(NativePath path, in ubyte[] contents)
87 {
88 	auto fil = openFile(path, FileMode.createTrunc);
89 	scope (exit) fil.close();
90 	fil.write(contents);
91 }
92 /// ditto
93 void writeFile(string path, in ubyte[] contents)
94 {
95 	writeFile(NativePath(path), contents);
96 }
97 
98 /**
99 	Convenience function to append to a file.
100 */
101 void appendToFile(NativePath path, string data) {
102 	auto fil = openFile(path, FileMode.append);
103 	scope(exit) fil.close();
104 	fil.write(data);
105 }
106 /// ditto
107 void appendToFile(string path, string data)
108 {
109 	appendToFile(NativePath(path), data);
110 }
111 
112 /**
113 	Read a whole UTF-8 encoded file into a string.
114 
115 	The resulting string will be sanitized and will have the
116 	optional byte order mark (BOM) removed.
117 */
118 string readFileUTF8(NativePath path)
119 {
120 	import vibe.internal..string;
121 
122 	auto data = readFile(path);
123 	auto idata = () @trusted { return data.assumeUnique; } ();
124 	return stripUTF8Bom(sanitizeUTF8(idata));
125 }
126 /// ditto
127 string readFileUTF8(string path)
128 {
129 	return readFileUTF8(NativePath(path));
130 }
131 
132 
133 /**
134 	Write a string into a UTF-8 encoded file.
135 
136 	The file will have a byte order mark (BOM) prepended.
137 */
138 void writeFileUTF8(NativePath path, string contents)
139 {
140 	static immutable ubyte[] bom = [0xEF, 0xBB, 0xBF];
141 	auto fil = openFile(path, FileMode.createTrunc);
142 	scope (exit) fil.close();
143 	fil.write(bom);
144 	fil.write(contents);
145 }
146 
147 /**
148 	Creates and opens a temporary file for writing.
149 */
150 FileStream createTempFile(string suffix = null)
151 {
152 	version(Windows){
153 		import std.conv : to;
154 		string tmpname;
155 		() @trusted {
156 			auto fn = tmpnam(null);
157 			enforce(fn !is null, "Failed to generate temporary name.");
158 			tmpname = to!string(fn);
159 		} ();
160 		if (tmpname.startsWith("\\")) tmpname = tmpname[1 .. $];
161 		tmpname ~= suffix;
162 		return openFile(tmpname, FileMode.createTrunc);
163 	} else {
164 		enum pattern ="/tmp/vtmp.XXXXXX";
165 		scope templ = new char[pattern.length+suffix.length+1];
166 		templ[0 .. pattern.length] = pattern;
167 		templ[pattern.length .. $-1] = (suffix)[];
168 		templ[$-1] = '\0';
169 		assert(suffix.length <= int.max);
170 		auto fd = () @trusted { return mkstemps(templ.ptr, cast(int)suffix.length); } ();
171 		enforce(fd >= 0, "Failed to create temporary file.");
172 		auto efd = eventDriver.files.adopt(fd);
173 		return FileStream(efd, NativePath(templ[0 .. $-1].idup), FileMode.createTrunc);
174 	}
175 }
176 
177 /**
178 	Moves or renames a file.
179 
180 	Params:
181 		from = Path to the file/directory to move/rename.
182 		to = The target path
183 		copy_fallback = Determines if copy/remove should be used in case of the
184 			source and destination path pointing to different devices.
185 */
186 void moveFile(NativePath from, NativePath to, bool copy_fallback = false)
187 {
188 	moveFile(from.toNativeString(), to.toNativeString(), copy_fallback);
189 }
190 /// ditto
191 void moveFile(string from, string to, bool copy_fallback = false)
192 {
193 	auto fail = performInWorker((string from, string to) {
194 		try {
195 			std.file.rename(from, to);
196 		} catch (Exception e) {
197 			return e.msg.length ? e.msg : "Failed to move file.";
198 		}
199 		return null;
200 	}, from, to);
201 
202 	if (!fail.length) return;
203 
204 	if (!copy_fallback) throw new Exception(fail);
205 
206 	copyFile(from, to);
207 	removeFile(from);
208 }
209 
210 /**
211 	Copies a file.
212 
213 	Note that attributes and time stamps are currently not retained.
214 
215 	Params:
216 		from = Path of the source file
217 		to = Path for the destination file
218 		overwrite = If true, any file existing at the destination path will be
219 			overwritten. If this is false, an exception will be thrown should
220 			a file already exist at the destination path.
221 
222 	Throws:
223 		An Exception if the copy operation fails for some reason.
224 */
225 void copyFile(NativePath from, NativePath to, bool overwrite = false)
226 {
227 	DirEntry info;
228 	static if (__VERSION__ < 2078) {
229 		() @trusted {
230 			info = DirEntry(from.toString);
231 			enforce(info.isFile, "The source path is not a file and cannot be copied.");
232 		} ();
233 	} else {
234 		info = DirEntry(from.toString);
235 		enforce(info.isFile, "The source path is not a file and cannot be copied.");
236 	}
237 
238 	{
239 		auto src = openFile(from, FileMode.read);
240 		scope(exit) src.close();
241 		enforce(overwrite || !existsFile(to), "Destination file already exists.");
242 		auto dst = openFile(to, FileMode.createTrunc);
243 		scope(exit) dst.close();
244 		dst.truncate(src.size);
245 		dst.write(src);
246 	}
247 
248 	// TODO: also retain creation time on windows
249 
250 	static if (__VERSION__ < 2078) {
251 		() @trusted {
252 			setTimes(to.toString, info.timeLastAccessed, info.timeLastModified);
253 			setAttributes(to.toString, info.attributes);
254 		} ();
255 	} else {
256 		setTimes(to.toString, info.timeLastAccessed, info.timeLastModified);
257 		setAttributes(to.toString, info.attributes);
258 	}
259 }
260 /// ditto
261 void copyFile(string from, string to)
262 {
263 	copyFile(NativePath(from), NativePath(to));
264 }
265 
266 /**
267 	Removes a file
268 */
269 void removeFile(NativePath path)
270 {
271 	removeFile(path.toNativeString());
272 }
273 /// ditto
274 void removeFile(string path)
275 {
276 	auto fail = performInWorker((string path) {
277 		try {
278 			std.file.remove(path);
279 		} catch (Exception e) {
280 			return e.msg.length ? e.msg : "Failed to delete file.";
281 		}
282 		return null;
283 	}, path);
284 
285 	if (fail.length) throw new Exception(fail);
286 }
287 
288 /**
289 	Checks if a file exists
290 */
291 bool existsFile(NativePath path) nothrow
292 {
293 	return existsFile(path.toNativeString());
294 }
295 /// ditto
296 bool existsFile(string path) nothrow
297 {
298 	// This was *annotated* nothrow in 2.067.
299 	static if (__VERSION__ < 2067)
300 		scope(failure) assert(0, "Error: existsFile should never throw");
301 
302 	try return performInWorker((string p) => std.file.exists(p), path);
303 	catch (Exception e) {
304 		logDebug("Failed to determine file existence for '%s': %s", path, e.msg);
305 		return false;
306 	}
307 }
308 
309 /** Stores information about the specified file/directory into 'info'
310 
311 	Throws: A `FileException` is thrown if the file does not exist.
312 */
313 FileInfo getFileInfo(NativePath path)
314 @trusted {
315 	return getFileInfo(path.toNativeString);
316 }
317 /// ditto
318 FileInfo getFileInfo(string path)
319 {
320 	import std.typecons : tuple;
321 
322 	auto ret = performInWorker((string p) {
323 		try {
324 			auto ent = DirEntry(p);
325 			return tuple(makeFileInfo(ent), "");
326 		} catch (Exception e) {
327 			return tuple(FileInfo.init, e.msg.length ? e.msg : "Failed to get file information");
328 		}
329 	}, path);
330 	if (ret[1].length) throw new Exception(ret[1]);
331 	return ret[0];
332 }
333 
334 /**
335 	Creates a new directory.
336 */
337 void createDirectory(NativePath path)
338 {
339 	createDirectory(path.toNativeString);
340 }
341 /// ditto
342 void createDirectory(string path, Flag!"recursive" recursive = No.recursive)
343 {
344 	auto fail = performInWorker((string p, bool rec) {
345 		try {
346 			if (rec) mkdirRecurse(p);
347 			else mkdir(p);
348 		} catch (Exception e) {
349 			return e.msg.length ? e.msg : "Failed to create directory.";
350 		}
351 		return null;
352 	}, path, !!recursive);
353 
354 	if (fail) throw new Exception(fail);
355 }
356 
357 /**
358 	Enumerates all files in the specified directory.
359 */
360 void listDirectory(NativePath path, scope bool delegate(FileInfo info) @safe del)
361 {
362 	listDirectory(path.toNativeString, del);
363 }
364 /// ditto
365 void listDirectory(string path, scope bool delegate(FileInfo info) @safe del)
366 {
367 	import vibe.core.core : runWorkerTaskH;
368 	import vibe.core.channel : Channel, createChannel;
369 
370 	struct S {
371 		FileInfo info;
372 		string error;
373 	}
374 
375 	auto ch = createChannel!S();
376 	TaskSettings ts;
377 	ts.priority = 10 * Task.basePriority;
378 	runWorkerTaskH(ioTaskSettings, (string path, Channel!S ch) nothrow {
379 		scope (exit) ch.close();
380 		try {
381 			foreach (DirEntry ent; dirEntries(path, SpanMode.shallow)) {
382 				auto nfo = makeFileInfo(ent);
383 				try ch.put(S(nfo, null));
384 				catch (Exception e) break; // channel got closed
385 			}
386 		} catch (Exception e) {
387 			try ch.put(S(FileInfo.init, e.msg.length ? e.msg : "Failed to iterate directory"));
388 			catch (Exception e) {} // channel got closed
389 		}
390 	}, path, ch);
391 
392 	S itm;
393 	while (ch.tryConsumeOne(itm)) {
394 		if (itm.error.length)
395 			throw new Exception(itm.error);
396 
397 		if (!del(itm.info)) {
398 			ch.close();
399 			break;
400 		}
401 	}
402 }
403 /// ditto
404 void listDirectory(NativePath path, scope bool delegate(FileInfo info) @system del)
405 @system {
406 	listDirectory(path, (nfo) @trusted => del(nfo));
407 }
408 /// ditto
409 void listDirectory(string path, scope bool delegate(FileInfo info) @system del)
410 @system {
411 	listDirectory(path, (nfo) @trusted => del(nfo));
412 }
413 /// ditto
414 int delegate(scope int delegate(ref FileInfo)) iterateDirectory(NativePath path)
415 {
416 	int iterator(scope int delegate(ref FileInfo) del){
417 		int ret = 0;
418 		listDirectory(path, (fi){
419 			ret = del(fi);
420 			return ret == 0;
421 		});
422 		return ret;
423 	}
424 	return &iterator;
425 }
426 /// ditto
427 int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path)
428 {
429 	return iterateDirectory(NativePath(path));
430 }
431 
432 /**
433 	Starts watching a directory for changes.
434 */
435 DirectoryWatcher watchDirectory(NativePath path, bool recursive = true)
436 {
437 	return DirectoryWatcher(path, recursive);
438 }
439 // ditto
440 DirectoryWatcher watchDirectory(string path, bool recursive = true)
441 {
442 	return watchDirectory(NativePath(path), recursive);
443 }
444 
445 /**
446 	Returns the current working directory.
447 */
448 NativePath getWorkingDirectory()
449 {
450 	return NativePath(() @trusted { return std.file.getcwd(); } ());
451 }
452 
453 
454 /** Contains general information about a file.
455 */
456 struct FileInfo {
457 	/// Name of the file (not including the path)
458 	string name;
459 
460 	/// Size of the file (zero for directories)
461 	ulong size;
462 
463 	/// Time of the last modification
464 	SysTime timeModified;
465 
466 	/// Time of creation (not available on all operating systems/file systems)
467 	SysTime timeCreated;
468 
469 	/// True if this is a symlink to an actual file
470 	bool isSymlink;
471 
472 	/// True if this is a directory or a symlink pointing to a directory
473 	bool isDirectory;
474 
475 	/// True if this is a file. On POSIX if both isFile and isDirectory are false it is a special file.
476 	bool isFile;
477 
478 	/** True if the file's hidden attribute is set.
479 
480 		On systems that don't support a hidden attribute, any file starting with
481 		a single dot will be treated as hidden.
482 	*/
483 	bool hidden;
484 }
485 
486 /**
487 	Specifies how a file is manipulated on disk.
488 */
489 enum FileMode {
490 	/// The file is opened read-only.
491 	read = FileOpenMode.read,
492 	/// The file is opened for read-write random access.
493 	readWrite = FileOpenMode.readWrite,
494 	/// The file is truncated if it exists or created otherwise and then opened for read-write access.
495 	createTrunc = FileOpenMode.createTrunc,
496 	/// The file is opened for appending data to it and created if it does not exist.
497 	append = FileOpenMode.append
498 }
499 
500 /**
501 	Accesses the contents of a file as a stream.
502 */
503 struct FileStream {
504 	@safe:
505 
506 	private struct CTX {
507 		NativePath path;
508 		ulong size;
509 		FileMode mode;
510 		ulong ptr;
511 		shared(NativeEventDriver) driver;
512 	}
513 
514 	private {
515 		FileFD m_fd;
516 		CTX* m_ctx;
517 	}
518 
519 	private this(FileFD fd, NativePath path, FileMode mode)
520 	{
521 		assert(fd != FileFD.invalid, "Constructing FileStream from invalid file descriptor.");
522 		m_fd = fd;
523 		m_ctx = new CTX; // TODO: use FD custom storage
524 		m_ctx.path = path;
525 		m_ctx.mode = mode;
526 		m_ctx.size = eventDriver.files.getSize(fd);
527 		m_ctx.driver = () @trusted { return cast(shared)eventDriver; } ();
528 
529 		if (mode == FileMode.append)
530 			m_ctx.ptr = m_ctx.size;
531 	}
532 
533 	this(this)
534 	{
535 		if (m_fd != FileFD.invalid)
536 			eventDriver.files.addRef(m_fd);
537 	}
538 
539 	~this()
540 	{
541 		if (m_fd != FileFD.invalid)
542 			releaseHandle!"files"(m_fd, m_ctx.driver);
543 	}
544 
545 	@property int fd() { return cast(int)m_fd; }
546 
547 	/// The path of the file.
548 	@property NativePath path() const { return ctx.path; }
549 
550 	/// Determines if the file stream is still open
551 	@property bool isOpen() const { return m_fd != FileFD.invalid; }
552 	@property ulong size() const nothrow { return ctx.size; }
553 	@property bool readable() const nothrow { return ctx.mode != FileMode.append; }
554 	@property bool writable() const nothrow { return ctx.mode != FileMode.read; }
555 
556 	bool opCast(T)() if (is (T == bool)) { return m_fd != FileFD.invalid; }
557 
558 	void takeOwnershipOfFD()
559 	{
560 		assert(false, "TODO!");
561 	}
562 
563 	void seek(ulong offset)
564 	{
565 		enforce(ctx.mode != FileMode.append, "File opened for appending, not random access. Cannot seek.");
566 		ctx.ptr = offset;
567 	}
568 
569 	ulong tell() nothrow { return ctx.ptr; }
570 
571 	void truncate(ulong size)
572 	{
573 		enforce(ctx.mode != FileMode.append, "File opened for appending, not random access. Cannot truncate.");
574 
575 		auto res = asyncAwaitUninterruptible!(FileIOCallback,
576 			cb => eventDriver.files.truncate(m_fd, size, cb)
577 		);
578 		enforce(res[1] == IOStatus.ok, "Failed to resize file.");
579 		m_ctx.size = size;
580 	}
581 
582 	/// Closes the file handle.
583 	void close()
584 	{
585 		if (m_fd == FileFD.invalid) return;
586 		if (!eventDriver.files.isValid(m_fd)) return;
587 
588 		auto res = asyncAwaitUninterruptible!(FileCloseCallback,
589 			cb => eventDriver.files.close(m_fd, cb)
590 		);
591 		releaseHandle!"files"(m_fd, m_ctx.driver);
592 		m_fd = FileFD.invalid;
593 		m_ctx = null;
594 
595 		if (res[1] != CloseStatus.ok)
596 			throw new Exception("Failed to close file");
597 	}
598 
599 	@property bool empty() const { assert(this.readable); return ctx.ptr >= ctx.size; }
600 	@property ulong leastSize() const { assert(this.readable); return ctx.size - ctx.ptr; }
601 	@property bool dataAvailableForRead() { return true; }
602 
603 	const(ubyte)[] peek()
604 	{
605 		return null;
606 	}
607 
608 	size_t read(ubyte[] dst, IOMode mode)
609 	{
610 		auto res = asyncAwait!(FileIOCallback,
611 			cb => eventDriver.files.read(m_fd, ctx.ptr, dst, mode, cb),
612 			cb => eventDriver.files.cancelRead(m_fd)
613 		);
614 		ctx.ptr += res[2];
615 		enforce(res[1] == IOStatus.ok, "Failed to read data from disk.");
616 		return res[2];
617 	}
618 
619 	void read(ubyte[] dst)
620 	{
621 		auto ret = read(dst, IOMode.all);
622 		assert(ret == dst.length, "File.read returned less data than requested for IOMode.all.");
623 	}
624 
625 	size_t write(in ubyte[] bytes, IOMode mode)
626 	{
627 		auto res = asyncAwait!(FileIOCallback,
628 			cb => eventDriver.files.write(m_fd, ctx.ptr, bytes, mode, cb),
629 			cb => eventDriver.files.cancelWrite(m_fd)
630 		);
631 		ctx.ptr += res[2];
632 		if (ctx.ptr > ctx.size) ctx.size = ctx.ptr;
633 		enforce(res[1] == IOStatus.ok, "Failed to write data to disk.");
634 		return res[2];
635 	}
636 
637 	void write(in ubyte[] bytes)
638 	{
639 		write(bytes, IOMode.all);
640 	}
641 
642 	void write(in char[] bytes)
643 	{
644 		write(cast(const(ubyte)[])bytes);
645 	}
646 
647 	void write(InputStream)(InputStream stream, ulong nbytes = ulong.max)
648 		if (isInputStream!InputStream)
649 	{
650 		writeDefault(this, stream, nbytes);
651 	}
652 
653 	void flush()
654 	{
655 		assert(this.writable);
656 	}
657 
658 	void finalize()
659 	{
660 		flush();
661 	}
662 
663 	private inout(CTX)* ctx() inout nothrow { return m_ctx; }
664 }
665 
666 mixin validateRandomAccessStream!FileStream;
667 
668 
669 private void writeDefault(OutputStream, InputStream)(ref OutputStream dst, InputStream stream, ulong nbytes = ulong.max)
670 	if (isOutputStream!OutputStream && isInputStream!InputStream)
671 {
672 	import vibe.internal.allocator : theAllocator, make, dispose;
673 	import std.algorithm.comparison : min;
674 
675 	static struct Buffer { ubyte[64*1024] bytes = void; }
676 	auto bufferobj = () @trusted { return theAllocator.make!Buffer(); } ();
677 	scope (exit) () @trusted { theAllocator.dispose(bufferobj); } ();
678 	auto buffer = bufferobj.bytes[];
679 
680 	//logTrace("default write %d bytes, empty=%s", nbytes, stream.empty);
681 	if (nbytes == ulong.max) {
682 		while (!stream.empty) {
683 			size_t chunk = min(stream.leastSize, buffer.length);
684 			assert(chunk > 0, "leastSize returned zero for non-empty stream.");
685 			//logTrace("read pipe chunk %d", chunk);
686 			stream.read(buffer[0 .. chunk]);
687 			dst.write(buffer[0 .. chunk]);
688 		}
689 	} else {
690 		while (nbytes > 0) {
691 			size_t chunk = min(nbytes, buffer.length);
692 			//logTrace("read pipe chunk %d", chunk);
693 			stream.read(buffer[0 .. chunk]);
694 			dst.write(buffer[0 .. chunk]);
695 			nbytes -= chunk;
696 		}
697 	}
698 }
699 
700 
701 /**
702 	Interface for directory watcher implementations.
703 
704 	Directory watchers monitor the contents of a directory (wither recursively or non-recursively)
705 	for changes, such as file additions, deletions or modifications.
706 */
707 struct DirectoryWatcher { // TODO: avoid all those heap allocations!
708 	import std.array : Appender, appender;
709 	import vibe.core.sync : LocalManualEvent, createManualEvent;
710 
711 	@safe:
712 
713 	private static struct Context {
714 		NativePath path;
715 		bool recursive;
716 		Appender!(DirectoryChange[]) changes;
717 		LocalManualEvent changeEvent;
718 		shared(NativeEventDriver) driver;
719 
720 		void onChange(WatcherID, in ref FileChange change)
721 		nothrow {
722 			DirectoryChangeType ct;
723 			final switch (change.kind) {
724 				case FileChangeKind.added: ct = DirectoryChangeType.added; break;
725 				case FileChangeKind.removed: ct = DirectoryChangeType.removed; break;
726 				case FileChangeKind.modified: ct = DirectoryChangeType.modified; break;
727 			}
728 
729 			static if (is(typeof(change.baseDirectory))) {
730 				// eventcore 0.8.23 and up
731 				this.changes ~= DirectoryChange(ct, NativePath.fromTrustedString(change.baseDirectory) ~ NativePath.fromTrustedString(change.directory) ~ NativePath.fromTrustedString(change.name.idup));
732 			} else {
733 				this.changes ~= DirectoryChange(ct, NativePath.fromTrustedString(change.directory) ~ NativePath.fromTrustedString(change.name.idup));
734 			}
735 			this.changeEvent.emit();
736 		}
737 	}
738 
739 	private {
740 		WatcherID m_watcher;
741 		Context* m_context;
742 	}
743 
744 	private this(NativePath path, bool recursive)
745 	{
746 		m_context = new Context; // FIME: avoid GC allocation (use FD user data slot)
747 		m_context.changeEvent = createManualEvent();
748 		m_watcher = eventDriver.watchers.watchDirectory(path.toNativeString, recursive, &m_context.onChange);
749 		enforce(m_watcher != WatcherID.invalid, "Failed to watch directory.");
750 		m_context.path = path;
751 		m_context.recursive = recursive;
752 		m_context.changes = appender!(DirectoryChange[]);
753 		m_context.driver = () @trusted { return cast(shared)eventDriver; } ();
754 	}
755 
756 	this(this) nothrow { if (m_watcher != WatcherID.invalid) eventDriver.watchers.addRef(m_watcher); }
757 	~this()
758 	nothrow {
759 		if (m_watcher != WatcherID.invalid)
760 			releaseHandle!"watchers"(m_watcher, m_context.driver);
761 	}
762 
763 	/// The path of the watched directory
764 	@property NativePath path() const nothrow { return m_context.path; }
765 
766 	/// Indicates if the directory is watched recursively
767 	@property bool recursive() const nothrow { return m_context.recursive; }
768 
769 	/** Fills the destination array with all changes that occurred since the last call.
770 
771 		The function will block until either directory changes have occurred or until the
772 		timeout has elapsed. Specifying a negative duration will cause the function to
773 		wait without a timeout.
774 
775 		Params:
776 			dst = The destination array to which the changes will be appended
777 			timeout = Optional timeout for the read operation. A value of
778 				`Duration.max` will wait indefinitely.
779 
780 		Returns:
781 			If the call completed successfully, true is returned.
782 	*/
783 	bool readChanges(ref DirectoryChange[] dst, Duration timeout = Duration.max)
784 	{
785 		if (timeout == Duration.max) {
786 			while (!m_context.changes.data.length)
787 				m_context.changeEvent.wait(Duration.max, m_context.changeEvent.emitCount);
788 		} else {
789 			MonoTime now = MonoTime.currTime();
790 			MonoTime final_time = now + timeout;
791 			while (!m_context.changes.data.length) {
792 				m_context.changeEvent.wait(final_time - now, m_context.changeEvent.emitCount);
793 				now = MonoTime.currTime();
794 				if (now >= final_time) break;
795 			}
796 			if (!m_context.changes.data.length) return false;
797 		}
798 
799 		dst = m_context.changes.data;
800 		m_context.changes = appender!(DirectoryChange[]);
801 		return true;
802 	}
803 }
804 
805 
806 /** Specifies the kind of change in a watched directory.
807 */
808 enum DirectoryChangeType {
809 	/// A file or directory was added
810 	added,
811 	/// A file or directory was deleted
812 	removed,
813 	/// A file or directory was modified
814 	modified
815 }
816 
817 
818 /** Describes a single change in a watched directory.
819 */
820 struct DirectoryChange {
821 	/// The type of change
822 	DirectoryChangeType type;
823 
824 	/// Path of the file/directory that was changed
825 	NativePath path;
826 }
827 
828 
829 private FileInfo makeFileInfo(DirEntry ent)
830 @trusted nothrow {
831 	import std.algorithm.comparison : among;
832 
833 	FileInfo ret;
834 	string fullname = ent.name;
835 	if (ent.name.length) {
836 		if (ent.name[$-1].among('/', '\\'))
837 			fullname = ent.name[0 .. $-1];
838 		ret.name = baseName(fullname);
839 		if (ret.name.length == 0) ret.name = fullname;
840 	}
841 
842 	try {
843 		ret.isFile = ent.isFile;
844 		ret.isDirectory = ent.isDir;
845 		ret.isSymlink = ent.isSymlink;
846 		ret.timeModified = ent.timeLastModified;
847 		version(Windows) ret.timeCreated = ent.timeCreated;
848 		else ret.timeCreated = ent.timeLastModified;
849 		ret.size = ent.size;
850 	} catch (Exception e) {
851 		logDebug("Failed to get information for file '%s': %s", fullname, e.msg);
852 	}
853 
854 	version (Windows) {
855 		import core.sys.windows.windows : FILE_ATTRIBUTE_HIDDEN;
856 		ret.hidden = (ent.attributes & FILE_ATTRIBUTE_HIDDEN) != 0;
857 	}
858 	else ret.hidden = ret.name.length > 1 && ret.name[0] == '.' && ret.name != "..";
859 
860 	return ret;
861 }
862 
863 version (Windows) {} else unittest {
864 	void test(string name_in, string name_out, bool hidden) {
865 		auto de = DirEntry(name_in);
866 		assert(makeFileInfo(de).hidden == hidden);
867 		assert(makeFileInfo(de).name == name_out);
868 	}
869 
870 	void testCreate(string name_in, string name_out, bool hidden)
871 	{
872 		if (name_in.endsWith("/"))
873 			createDirectory(name_in);
874 		else writeFileUTF8(NativePath(name_in), name_in);
875 		scope (exit) removeFile(name_in);
876 		test(name_in, name_out, hidden);
877 	}
878 
879 	test(".", ".", false);
880 	test("..", "..", false);
881 	testCreate(".test_foo", ".test_foo", true);
882 	test("./", ".", false);
883 	testCreate(".test_foo/", ".test_foo", true);
884 	test("/", "", false);
885 }
886 
887 unittest {
888 	auto name = "toAppend.txt";
889 	scope(exit) removeFile(name);
890 
891 	{
892 		auto handle = openFile(name, FileMode.createTrunc);
893 		handle.write("create,");
894 		assert(handle.tell() == "create,".length);
895 		handle.close();
896 	}
897 	{
898 		auto handle = openFile(name, FileMode.append);
899 		handle.write(" then append");
900 		assert(handle.tell() == "create, then append".length);
901 		handle.close();
902 	}
903 
904 	assert(readFile(name) == "create, then append");
905 }
906 
907 
908 private auto performInWorker(C, ARGS...)(C callable, auto ref ARGS args)
909 {
910 	version (none) {
911 		import vibe.core.concurrency : asyncWork;
912 		return asyncWork(callable, args).getResult();
913 	} else {
914 		import vibe.core.core : runWorkerTask;
915 		import core.atomic : atomicFence;
916 		import std.concurrency : Tid, send, receiveOnly, thisTid;
917 
918 		struct R {}
919 
920 		alias RET = typeof(callable(args));
921 		shared(RET) ret;
922 		runWorkerTask(ioTaskSettings, (shared(RET)* r, Tid caller, C c, ref ARGS a) nothrow {
923 			*() @trusted { return cast(RET*)r; } () = c(a);
924 			// Just as a precaution, because ManualEvent is not well defined in
925 			// terms of fence semantics
926 			atomicFence();
927 			try caller.send(R.init);
928 			catch (Exception e) assert(false, e.msg);
929 		}, () @trusted { return &ret; } (), thisTid, callable, args);
930 		() @trusted { receiveOnly!R(); } ();
931 		atomicFence();
932 		return ret;
933 	}
934 }
935 
936 private immutable TaskSettings ioTaskSettings = { priority: 20 * Task.basePriority };