1 /**
2 	File handling functions and types.
3 
4 	Copyright: © 2012-2021 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 import taggedalgebraic.taggedunion;
32 
33 
34 version(Posix){
35 	private extern(C) int mkstemps(char* templ, int suffixlen);
36 }
37 
38 @safe:
39 
40 
41 /**
42 	Opens a file stream with the specified mode.
43 */
44 FileStream openFile(NativePath path, FileMode mode = FileMode.read)
45 {
46 	auto fil = eventDriver.files.open(path.toNativeString(), cast(FileOpenMode)mode);
47 	enforce(fil != FileFD.invalid, "Failed to open file '"~path.toNativeString~"'");
48 	return FileStream(fil, path, mode);
49 }
50 /// ditto
51 FileStream openFile(string path, FileMode mode = FileMode.read)
52 {
53 	return openFile(NativePath(path), mode);
54 }
55 
56 
57 /**
58 	Read a whole file into a buffer.
59 
60 	If the supplied buffer is large enough, it will be used to store the
61 	contents of the file. Otherwise, a new buffer will be allocated.
62 
63 	Params:
64 		path = The path of the file to read
65 		buffer = An optional buffer to use for storing the file contents
66 */
67 ubyte[] readFile(NativePath path, ubyte[] buffer = null, size_t max_size = size_t.max)
68 {
69 	auto fil = openFile(path);
70 	scope (exit) fil.close();
71 	enforce(fil.size <= max_size, "File is too big.");
72 	auto sz = cast(size_t)fil.size;
73 	auto ret = sz <= buffer.length ? buffer[0 .. sz] : new ubyte[sz];
74 	fil.read(ret);
75 	return ret;
76 }
77 /// ditto
78 ubyte[] readFile(string path, ubyte[] buffer = null, size_t max_size = size_t.max)
79 {
80 	return readFile(NativePath(path), buffer, max_size);
81 }
82 
83 
84 /**
85 	Write a whole file at once.
86 */
87 void writeFile(NativePath path, in ubyte[] contents)
88 {
89 	auto fil = openFile(path, FileMode.createTrunc);
90 	scope (exit) fil.close();
91 	fil.write(contents);
92 }
93 /// ditto
94 void writeFile(string path, in ubyte[] contents)
95 {
96 	writeFile(NativePath(path), contents);
97 }
98 
99 /**
100 	Convenience function to append to a file.
101 */
102 void appendToFile(NativePath path, string data) {
103 	auto fil = openFile(path, FileMode.append);
104 	scope(exit) fil.close();
105 	fil.write(data);
106 }
107 /// ditto
108 void appendToFile(string path, string data)
109 {
110 	appendToFile(NativePath(path), data);
111 }
112 
113 /**
114 	Read a whole UTF-8 encoded file into a string.
115 
116 	The resulting string will be sanitized and will have the
117 	optional byte order mark (BOM) removed.
118 */
119 string readFileUTF8(NativePath path)
120 {
121 	import vibe.internal.string;
122 
123 	auto data = readFile(path);
124 	auto idata = () @trusted { return data.assumeUnique; } ();
125 	return stripUTF8Bom(sanitizeUTF8(idata));
126 }
127 /// ditto
128 string readFileUTF8(string path)
129 {
130 	return readFileUTF8(NativePath(path));
131 }
132 
133 
134 /**
135 	Write a string into a UTF-8 encoded file.
136 
137 	The file will have a byte order mark (BOM) prepended.
138 */
139 void writeFileUTF8(NativePath path, string contents)
140 {
141 	static immutable ubyte[] bom = [0xEF, 0xBB, 0xBF];
142 	auto fil = openFile(path, FileMode.createTrunc);
143 	scope (exit) fil.close();
144 	fil.write(bom);
145 	fil.write(contents);
146 }
147 
148 /**
149 	Creates and opens a temporary file for writing.
150 */
151 FileStream createTempFile(string suffix = null)
152 {
153 	version(Windows){
154 		import std.conv : to;
155 		string tmpname;
156 		() @trusted {
157 			auto fn = tmpnam(null);
158 			enforce(fn !is null, "Failed to generate temporary name.");
159 			tmpname = to!string(fn);
160 		} ();
161 		if (tmpname.startsWith("\\")) tmpname = tmpname[1 .. $];
162 		tmpname ~= suffix;
163 		return openFile(tmpname, FileMode.createTrunc);
164 	} else {
165 		enum pattern ="/tmp/vtmp.XXXXXX";
166 		scope templ = new char[pattern.length+suffix.length+1];
167 		templ[0 .. pattern.length] = pattern;
168 		templ[pattern.length .. $-1] = (suffix)[];
169 		templ[$-1] = '\0';
170 		assert(suffix.length <= int.max);
171 		auto fd = () @trusted { return mkstemps(templ.ptr, cast(int)suffix.length); } ();
172 		enforce(fd >= 0, "Failed to create temporary file.");
173 		auto efd = eventDriver.files.adopt(fd);
174 		return FileStream(efd, NativePath(templ[0 .. $-1].idup), FileMode.createTrunc);
175 	}
176 }
177 
178 /**
179 	Moves or renames a file.
180 
181 	Params:
182 		from = Path to the file/directory to move/rename.
183 		to = The target path
184 		copy_fallback = Determines if copy/remove should be used in case of the
185 			source and destination path pointing to different devices.
186 */
187 void moveFile(NativePath from, NativePath to, bool copy_fallback = false)
188 {
189 	moveFile(from.toNativeString(), to.toNativeString(), copy_fallback);
190 }
191 /// ditto
192 void moveFile(string from, string to, bool copy_fallback = false)
193 {
194 	auto fail = performInWorker((string from, string to) {
195 		try {
196 			std.file.rename(from, to);
197 		} catch (Exception e) {
198 			return e.msg.length ? e.msg : "Failed to move file.";
199 		}
200 		return null;
201 	}, from, to);
202 
203 	if (!fail.length) return;
204 
205 	if (!copy_fallback) throw new Exception(fail);
206 
207 	copyFile(from, to);
208 	removeFile(from);
209 }
210 
211 /**
212 	Copies a file.
213 
214 	Note that attributes and time stamps are currently not retained.
215 
216 	Params:
217 		from = Path of the source file
218 		to = Path for the destination file
219 		overwrite = If true, any file existing at the destination path will be
220 			overwritten. If this is false, an exception will be thrown should
221 			a file already exist at the destination path.
222 
223 	Throws:
224 		An Exception if the copy operation fails for some reason.
225 */
226 void copyFile(NativePath from, NativePath to, bool overwrite = false)
227 {
228 	DirEntry info;
229 	static if (__VERSION__ < 2078) {
230 		() @trusted {
231 			info = DirEntry(from.toString);
232 			enforce(info.isFile, "The source path is not a file and cannot be copied.");
233 		} ();
234 	} else {
235 		info = DirEntry(from.toString);
236 		enforce(info.isFile, "The source path is not a file and cannot be copied.");
237 	}
238 
239 	{
240 		auto src = openFile(from, FileMode.read);
241 		scope(exit) src.close();
242 		enforce(overwrite || !existsFile(to), "Destination file already exists.");
243 		auto dst = openFile(to, FileMode.createTrunc);
244 		scope(exit) dst.close();
245 		dst.truncate(src.size);
246 		src.pipe(dst, PipeMode.concurrent);
247 	}
248 
249 	// TODO: also retain creation time on windows
250 
251 	static if (__VERSION__ < 2078) {
252 		() @trusted {
253 			setTimes(to.toString, info.timeLastAccessed, info.timeLastModified);
254 			setAttributes(to.toString, info.attributes);
255 		} ();
256 	} else {
257 		setTimes(to.toString, info.timeLastAccessed, info.timeLastModified);
258 		setAttributes(to.toString, info.attributes);
259 	}
260 }
261 /// ditto
262 void copyFile(string from, string to)
263 {
264 	copyFile(NativePath(from), NativePath(to));
265 }
266 
267 /**
268 	Removes a file
269 */
270 void removeFile(NativePath path)
271 {
272 	removeFile(path.toNativeString());
273 }
274 /// ditto
275 void removeFile(string path)
276 {
277 	auto fail = performInWorker((string path) {
278 		try {
279 			std.file.remove(path);
280 		} catch (Exception e) {
281 			return e.msg.length ? e.msg : "Failed to delete file.";
282 		}
283 		return null;
284 	}, path);
285 
286 	if (fail.length) throw new Exception(fail);
287 }
288 
289 /**
290 	Checks if a file exists
291 */
292 bool existsFile(NativePath path) nothrow
293 {
294 	return existsFile(path.toNativeString());
295 }
296 /// ditto
297 bool existsFile(string path) nothrow
298 {
299 	// This was *annotated* nothrow in 2.067.
300 	static if (__VERSION__ < 2067)
301 		scope(failure) assert(0, "Error: existsFile should never throw");
302 
303 	try return performInWorker((string p) => std.file.exists(p), path);
304 	catch (Exception e) {
305 		logDebug("Failed to determine file existence for '%s': %s", path, e.msg);
306 		return false;
307 	}
308 }
309 
310 /** Stores information about the specified file/directory into 'info'
311 
312 	Throws: A `FileException` is thrown if the file does not exist.
313 */
314 FileInfo getFileInfo(NativePath path)
315 @trusted {
316 	return getFileInfo(path.toNativeString);
317 }
318 /// ditto
319 FileInfo getFileInfo(string path)
320 {
321 	import std.typecons : tuple;
322 
323 	auto ret = performInWorker((string p) {
324 		try {
325 			auto ent = DirEntry(p);
326 			return tuple(makeFileInfo(ent), "");
327 		} catch (Exception e) {
328 			return tuple(FileInfo.init, e.msg.length ? e.msg : "Failed to get file information");
329 		}
330 	}, path);
331 	if (ret[1].length) throw new Exception(ret[1]);
332 	return ret[0];
333 }
334 
335 /** Returns file information about multiple files at once.
336 
337 	This version of `getFileInfo` is more efficient than many individual calls
338 	to the singular version.
339 */
340 FileInfoResult[] getFileInfo(const(NativePath)[] paths)
341 nothrow {
342 	import vibe.core.channel : Channel, ChannelConfig, ChannelPriority, createChannel;
343 	import vibe.core.core : runTask, runWorkerTask;
344 
345 	if (!paths.length) return null;
346 
347 	ChannelConfig cc;
348 	cc.priority = ChannelPriority.overhead;
349 
350 	Channel!NativePath inch;
351 	Channel!FileInfoResult outch;
352 
353 	try {
354 		inch = createChannel!NativePath(cc);
355 		outch = createChannel!FileInfoResult(cc);
356 	} catch (Exception e) assert(false, e.msg);
357 
358 	static void getInfos(Channel!NativePath inch, Channel!FileInfoResult outch) nothrow {
359 		NativePath p;
360 		while (inch.tryConsumeOne(p)) {
361 			FileInfoResult fi;
362 			if (!existsFile(p)) fi = FileInfoResult.missing;
363 			else {
364 				try {
365 					auto ent = DirEntry(p.toString);
366 					fi = FileInfoResult.info(makeFileInfo(ent));
367 				} catch (Exception e) {
368 					fi = FileInfoResult.error(e.msg.length ? e.msg : "Failed to get file information");
369 				}
370 			}
371 			try outch.put(fi);
372 			catch (Exception e) assert(false, e.msg);
373 		}
374 		outch.close();
375 	}
376 
377 	try runWorkerTask(&getInfos, inch, outch);
378 	catch (Exception e) assert(false, e.msg);
379 
380 	runTask(() nothrow {
381 		foreach (p; paths) {
382 			try inch.put(p);
383 			catch (Exception e) assert(false, e.msg);
384 		}
385 		inch.close();
386 	});
387 
388 	auto ret = new FileInfoResult[](paths.length);
389 	size_t i = 0;
390 	foreach (ref fi; ret) {
391 		if (!outch.tryConsumeOne(fi))
392 			assert(false);
393 	}
394 	assert(outch.empty);
395 
396 	return ret;
397 }
398 
399 struct FileInfoResultFields {
400 	Void missing;
401 	string error;
402 	FileInfo info;
403 }
404 alias FileInfoResult = TaggedUnion!FileInfoResultFields;
405 
406 
407 /**
408 	Creates a new directory.
409 */
410 void createDirectory(NativePath path)
411 {
412 	createDirectory(path.toNativeString);
413 }
414 /// ditto
415 void createDirectory(NativePath path, Flag!"recursive" recursive)
416 {
417 	createDirectory(path.toNativeString, recursive);
418 }
419 /// ditto
420 void createDirectory(string path, Flag!"recursive" recursive = No.recursive)
421 {
422 	auto fail = performInWorker((string p, bool rec) {
423 		try {
424 			if (rec) mkdirRecurse(p);
425 			else mkdir(p);
426 		} catch (Exception e) {
427 			return e.msg.length ? e.msg : "Failed to create directory.";
428 		}
429 		return null;
430 	}, path, !!recursive);
431 
432 	if (fail) throw new Exception(fail);
433 }
434 
435 /** Enumerates all files in the specified directory.
436 
437 	Note that unless an explicit `mode` is given, `DirectoryMode.shallow` is the
438 	default and only items directly contained in the specified folder will be
439 	returned.
440 
441 	Params:
442 		path = Path to the (root) folder to list
443 		mode = Defines how files and sub directories are treated during the enumeration
444 		del = Callback to invoke for each directory entry
445 		directory_predicate = Optional predicate used to determine whether to
446 			descent into a sub directory (only available in the recursive
447 			`DirectoryListMode` modes)
448 */
449 void listDirectory(NativePath path, DirectoryListMode mode,
450 	scope bool delegate(FileInfo info) @safe del,
451 	scope bool function(ref const FileInfo) @safe nothrow directory_predicate = null)
452 {
453 	import vibe.core.channel : ChannelConfig, ChannelPriority, createChannel;
454 	import vibe.core.core : runWorkerTask;
455 
456 	ChannelConfig cc;
457 	cc.priority = ChannelPriority.overhead;
458 
459 	ListDirectoryRequest req;
460 	req.path = path;
461 	req.channel = createChannel!ListDirectoryData(cc);
462 	req.spanMode = mode;
463 	req.directoryPredicate = directory_predicate;
464 
465 	runWorkerTask(ioTaskSettings, &performListDirectory, req);
466 
467 	ListDirectoryData itm;
468 	while (req.channel.tryConsumeOne(itm)) {
469 		if (itm.error.length)
470 			throw new Exception(itm.error);
471 
472 		if (!del(itm.info)) {
473 			req.channel.close();
474 			// makes sure that the directory handle is closed before returning
475 			while (!req.channel.empty) req.channel.tryConsumeOne(itm);
476 			break;
477 		}
478 	}
479 }
480 /// ditto
481 void listDirectory(string path, DirectoryListMode mode,
482 	scope bool delegate(FileInfo info) @safe del)
483 {
484 	listDirectory(NativePath(path), mode, del);
485 }
486 void listDirectory(NativePath path, scope bool delegate(FileInfo info) @safe del)
487 {
488 	listDirectory(path, DirectoryListMode.shallow, del);
489 }
490 /// ditto
491 void listDirectory(string path, scope bool delegate(FileInfo info) @safe del)
492 {
493 	listDirectory(path, DirectoryListMode.shallow, del);
494 }
495 /// ditto
496 void listDirectory(NativePath path, DirectoryListMode mode, scope bool delegate(FileInfo info) @system del,
497 	scope bool function(ref const FileInfo) @safe nothrow directory_predicate = null)
498 @system {
499 	listDirectory(path, mode, (nfo) @trusted => del(nfo), directory_predicate);
500 }
501 /// ditto
502 void listDirectory(string path, DirectoryListMode mode, scope bool delegate(FileInfo info) @system del)
503 @system {
504 	listDirectory(path, mode, (nfo) @trusted => del(nfo));
505 }
506 /// ditto
507 void listDirectory(NativePath path, scope bool delegate(FileInfo info) @system del)
508 @system {
509 	listDirectory(path, (nfo) @trusted => del(nfo));
510 }
511 /// ditto
512 void listDirectory(string path, scope bool delegate(FileInfo info) @system del)
513 @system {
514 	listDirectory(path, (nfo) @trusted => del(nfo));
515 }
516 /// ditto
517 int delegate(scope int delegate(ref FileInfo)) iterateDirectory(NativePath path,
518 	DirectoryListMode mode = DirectoryListMode.shallow,
519 	bool function(ref const FileInfo) @safe nothrow directory_predicate = null)
520 {
521 	int iterator(scope int delegate(ref FileInfo) del){
522 		int ret = 0;
523 		listDirectory(path, mode, (fi) {
524 			ret = del(fi);
525 			return ret == 0;
526 		}, directory_predicate);
527 		return ret;
528 	}
529 	return &iterator;
530 }
531 /// ditto
532 int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path,
533 	DirectoryListMode mode = DirectoryListMode.shallow)
534 {
535 	return iterateDirectory(NativePath(path), mode);
536 }
537 
538 /**
539 	Starts watching a directory for changes.
540 */
541 DirectoryWatcher watchDirectory(NativePath path, bool recursive = true)
542 {
543 	return DirectoryWatcher(path, recursive);
544 }
545 // ditto
546 DirectoryWatcher watchDirectory(string path, bool recursive = true)
547 {
548 	return watchDirectory(NativePath(path), recursive);
549 }
550 
551 /**
552 	Returns the current working directory.
553 */
554 NativePath getWorkingDirectory()
555 {
556 	return NativePath(() @trusted { return std.file.getcwd(); } ());
557 }
558 
559 
560 /** Contains general information about a file.
561 */
562 struct FileInfo {
563 	/// Name of the file (not including the path)
564 	string name;
565 
566 	/// The directory containing the file
567 	NativePath directory;
568 
569 	/// Size of the file (zero for directories)
570 	ulong size;
571 
572 	/// Time of the last modification
573 	SysTime timeModified;
574 
575 	/// Time of creation (not available on all operating systems/file systems)
576 	SysTime timeCreated;
577 
578 	/// True if this is a symlink to an actual file
579 	bool isSymlink;
580 
581 	/// True if this is a directory or a symlink pointing to a directory
582 	bool isDirectory;
583 
584 	/// True if this is a file. On POSIX if both isFile and isDirectory are false it is a special file.
585 	bool isFile;
586 
587 	/** True if the file's hidden attribute is set.
588 
589 		On systems that don't support a hidden attribute, any file starting with
590 		a single dot will be treated as hidden.
591 	*/
592 	bool hidden;
593 }
594 
595 /**
596 	Specifies how a file is manipulated on disk.
597 */
598 enum FileMode {
599 	/// The file is opened read-only.
600 	read = FileOpenMode.read,
601 	/// The file is opened for read-write random access.
602 	readWrite = FileOpenMode.readWrite,
603 	/// The file is truncated if it exists or created otherwise and then opened for read-write access.
604 	createTrunc = FileOpenMode.createTrunc,
605 	/// The file is opened for appending data to it and created if it does not exist.
606 	append = FileOpenMode.append
607 }
608 
609 enum DirectoryListMode {
610 	/// Only iterate the directory itself
611 	shallow = 0,
612 	/// Only iterate over directories directly within the given directory
613 	shallowDirectories = 1<<1,
614 	/// Iterate recursively (depth-first, pre-order)
615 	recursive = 1<<0,
616 	/// Iterate only directories recursively (depth-first, pre-order)
617 	recursiveDirectories = recursive | shallowDirectories,
618 }
619 
620 
621 /**
622 	Accesses the contents of a file as a stream.
623 */
624 struct FileStream {
625 	@safe:
626 
627 	private struct CTX {
628 		NativePath path;
629 		ulong size;
630 		FileMode mode;
631 		ulong ptr;
632 		shared(NativeEventDriver) driver;
633 	}
634 
635 	private {
636 		FileFD m_fd;
637 		CTX* m_ctx;
638 	}
639 
640 	private this(FileFD fd, NativePath path, FileMode mode)
641 	nothrow {
642 		assert(fd != FileFD.invalid, "Constructing FileStream from invalid file descriptor.");
643 		m_fd = fd;
644 		m_ctx = new CTX; // TODO: use FD custom storage
645 		m_ctx.path = path;
646 		m_ctx.mode = mode;
647 		m_ctx.size = eventDriver.files.getSize(fd);
648 		m_ctx.driver = () @trusted { return cast(shared)eventDriver; } ();
649 
650 		if (mode == FileMode.append)
651 			m_ctx.ptr = m_ctx.size;
652 	}
653 
654 	this(this)
655 	nothrow {
656 		if (m_fd != FileFD.invalid)
657 			eventDriver.files.addRef(m_fd);
658 	}
659 
660 	~this()
661 	nothrow {
662 		if (m_fd != FileFD.invalid)
663 			releaseHandle!"files"(m_fd, m_ctx.driver);
664 	}
665 
666 	@property int fd() const nothrow { return cast(int)m_fd; }
667 
668 	/// The path of the file.
669 	@property NativePath path() const nothrow { return ctx.path; }
670 
671 	/// Determines if the file stream is still open
672 	@property bool isOpen() const nothrow { return m_fd != FileFD.invalid; }
673 	@property ulong size() const nothrow { return ctx.size; }
674 	@property bool readable() const nothrow { return ctx.mode != FileMode.append; }
675 	@property bool writable() const nothrow { return ctx.mode != FileMode.read; }
676 
677 	bool opCast(T)() if (is (T == bool)) { return m_fd != FileFD.invalid; }
678 
679 	void takeOwnershipOfFD()
680 	{
681 		assert(false, "TODO!");
682 	}
683 
684 	void seek(ulong offset)
685 	{
686 		enforce(ctx.mode != FileMode.append, "File opened for appending, not random access. Cannot seek.");
687 		ctx.ptr = offset;
688 	}
689 
690 	ulong tell() nothrow { return ctx.ptr; }
691 
692 	void truncate(ulong size)
693 	{
694 		enforce(ctx.mode != FileMode.append, "File opened for appending, not random access. Cannot truncate.");
695 
696 		auto res = asyncAwaitUninterruptible!(FileIOCallback,
697 			cb => eventDriver.files.truncate(m_fd, size, cb)
698 		);
699 		enforce(res[1] == IOStatus.ok, "Failed to resize file.");
700 		m_ctx.size = size;
701 	}
702 
703 	/// Closes the file handle.
704 	void close()
705 	{
706 		if (m_fd == FileFD.invalid) return;
707 		if (!eventDriver.files.isValid(m_fd)) return;
708 
709 		auto res = asyncAwaitUninterruptible!(FileCloseCallback,
710 			cb => eventDriver.files.close(m_fd, cb)
711 		);
712 		releaseHandle!"files"(m_fd, m_ctx.driver);
713 		m_fd = FileFD.invalid;
714 		m_ctx = null;
715 
716 		if (res[1] != CloseStatus.ok)
717 			throw new Exception("Failed to close file");
718 	}
719 
720 	@property bool empty() const { assert(this.readable); return ctx.ptr >= ctx.size; }
721 	@property ulong leastSize()
722 	const {
723 		assert(this.readable);
724 		return ctx.ptr < ctx.size ? ctx.size - ctx.ptr : 0;
725 	}
726 	@property bool dataAvailableForRead() { return true; }
727 
728 	const(ubyte)[] peek()
729 	{
730 		return null;
731 	}
732 
733 	size_t read(ubyte[] dst, IOMode mode)
734 	{
735 		// NOTE: cancelRead is currently not behaving as specified and cannot
736 		//       be relied upon. For this reason, we MUST use the uninterruptible
737 		//       version of asyncAwait here!
738 		auto res = asyncAwaitUninterruptible!(FileIOCallback,
739 			cb => eventDriver.files.read(m_fd, ctx.ptr, dst, mode, cb)
740 		);
741 		ctx.ptr += res[2];
742 		enforce(res[1] == IOStatus.ok, "Failed to read data from disk.");
743 		return res[2];
744 	}
745 
746 	void read(ubyte[] dst)
747 	{
748 		auto ret = read(dst, IOMode.all);
749 		assert(ret == dst.length, "File.read returned less data than requested for IOMode.all.");
750 	}
751 
752 	size_t write(in ubyte[] bytes, IOMode mode)
753 	{
754 		// NOTE: cancelWrite is currently not behaving as specified and cannot
755 		//       be relied upon. For this reason, we MUST use the uninterruptible
756 		//       version of asyncAwait here!
757 		auto res = asyncAwaitUninterruptible!(FileIOCallback,
758 			cb => eventDriver.files.write(m_fd, ctx.ptr, bytes, mode, cb)
759 		);
760 		ctx.ptr += res[2];
761 		if (ctx.ptr > ctx.size) ctx.size = ctx.ptr;
762 		enforce(res[1] == IOStatus.ok, "Failed to write data to disk.");
763 		return res[2];
764 	}
765 
766 	void write(in ubyte[] bytes)
767 	{
768 		write(bytes, IOMode.all);
769 	}
770 
771 	void write(in char[] bytes)
772 	{
773 		write(cast(const(ubyte)[])bytes);
774 	}
775 
776 	void write(InputStream)(InputStream stream, ulong nbytes = ulong.max)
777 		if (isInputStream!InputStream)
778 	{
779 		pipe(stream, this, nbytes, PipeMode.concurrent);
780 	}
781 
782 	void flush()
783 	{
784 		assert(this.writable);
785 	}
786 
787 	void finalize()
788 	{
789 		flush();
790 	}
791 
792 	private inout(CTX)* ctx() inout nothrow { return m_ctx; }
793 }
794 
795 mixin validateClosableRandomAccessStream!FileStream;
796 
797 
798 /**
799 	Interface for directory watcher implementations.
800 
801 	Directory watchers monitor the contents of a directory (wither recursively or non-recursively)
802 	for changes, such as file additions, deletions or modifications.
803 */
804 struct DirectoryWatcher { // TODO: avoid all those heap allocations!
805 	import std.array : Appender, appender;
806 	import vibe.core.sync : LocalManualEvent, createManualEvent;
807 
808 	@safe:
809 
810 	private static struct Context {
811 		NativePath path;
812 		bool recursive;
813 		Appender!(DirectoryChange[]) changes;
814 		LocalManualEvent changeEvent;
815 		shared(NativeEventDriver) driver;
816 
817 		// Support for `-preview=in`
818 		static if (!is(typeof(mixin(q{(in ref int a) => a}))))
819 		{
820 			void onChange(WatcherID id, const scope ref FileChange change) nothrow {
821 				this.onChangeImpl(id, change);
822 			}
823 		} else {
824 			mixin(q{
825 			void onChange(WatcherID id, in ref FileChange change) nothrow {
826 				this.onChangeImpl(id, change);
827 			}});
828 		}
829 
830 		void onChangeImpl(WatcherID, const scope ref FileChange change)
831 		nothrow {
832 			DirectoryChangeType ct;
833 			final switch (change.kind) {
834 				case FileChangeKind.added: ct = DirectoryChangeType.added; break;
835 				case FileChangeKind.removed: ct = DirectoryChangeType.removed; break;
836 				case FileChangeKind.modified: ct = DirectoryChangeType.modified; break;
837 			}
838 
839 			static if (is(typeof(change.baseDirectory))) {
840 				// eventcore 0.8.23 and up
841 				this.changes ~= DirectoryChange(ct, NativePath.fromTrustedString(change.baseDirectory) ~ NativePath.fromTrustedString(change.directory) ~ NativePath.fromTrustedString(change.name.idup));
842 			} else {
843 				this.changes ~= DirectoryChange(ct, NativePath.fromTrustedString(change.directory) ~ NativePath.fromTrustedString(change.name.idup));
844 			}
845 			this.changeEvent.emit();
846 		}
847 	}
848 
849 	private {
850 		WatcherID m_watcher;
851 		Context* m_context;
852 	}
853 
854 	private this(NativePath path, bool recursive)
855 	{
856 		m_context = new Context; // FIME: avoid GC allocation (use FD user data slot)
857 		m_context.changeEvent = createManualEvent();
858 		m_watcher = eventDriver.watchers.watchDirectory(path.toNativeString, recursive, &m_context.onChange);
859 		enforce(m_watcher != WatcherID.invalid, "Failed to watch directory.");
860 		m_context.path = path;
861 		m_context.recursive = recursive;
862 		m_context.changes = appender!(DirectoryChange[]);
863 		m_context.driver = () @trusted { return cast(shared)eventDriver; } ();
864 	}
865 
866 	this(this) nothrow { if (m_watcher != WatcherID.invalid) eventDriver.watchers.addRef(m_watcher); }
867 	~this()
868 	nothrow {
869 		if (m_watcher != WatcherID.invalid)
870 			releaseHandle!"watchers"(m_watcher, m_context.driver);
871 	}
872 
873 	/// The path of the watched directory
874 	@property NativePath path() const nothrow { return m_context.path; }
875 
876 	/// Indicates if the directory is watched recursively
877 	@property bool recursive() const nothrow { return m_context.recursive; }
878 
879 	/** Fills the destination array with all changes that occurred since the last call.
880 
881 		The function will block until either directory changes have occurred or until the
882 		timeout has elapsed. Specifying a negative duration will cause the function to
883 		wait without a timeout.
884 
885 		Params:
886 			dst = The destination array to which the changes will be appended
887 			timeout = Optional timeout for the read operation. A value of
888 				`Duration.max` will wait indefinitely.
889 
890 		Returns:
891 			If the call completed successfully, true is returned.
892 	*/
893 	bool readChanges(ref DirectoryChange[] dst, Duration timeout = Duration.max)
894 	{
895 		if (timeout == Duration.max) {
896 			while (!m_context.changes.data.length)
897 				m_context.changeEvent.wait(Duration.max, m_context.changeEvent.emitCount);
898 		} else {
899 			MonoTime now = MonoTime.currTime();
900 			MonoTime final_time = now + timeout;
901 			while (!m_context.changes.data.length) {
902 				m_context.changeEvent.wait(final_time - now, m_context.changeEvent.emitCount);
903 				now = MonoTime.currTime();
904 				if (now >= final_time) break;
905 			}
906 			if (!m_context.changes.data.length) return false;
907 		}
908 
909 		dst = m_context.changes.data;
910 		m_context.changes = appender!(DirectoryChange[]);
911 		return true;
912 	}
913 }
914 
915 
916 /** Specifies the kind of change in a watched directory.
917 */
918 enum DirectoryChangeType {
919 	/// A file or directory was added
920 	added,
921 	/// A file or directory was deleted
922 	removed,
923 	/// A file or directory was modified
924 	modified
925 }
926 
927 
928 /** Describes a single change in a watched directory.
929 */
930 struct DirectoryChange {
931 	/// The type of change
932 	DirectoryChangeType type;
933 
934 	/// Path of the file/directory that was changed
935 	NativePath path;
936 }
937 
938 
939 private FileInfo makeFileInfo(DirEntry ent)
940 @trusted nothrow {
941 	import std.algorithm.comparison : among;
942 
943 	FileInfo ret;
944 	string fullname = ent.name;
945 	if (fullname.length) {
946 		if (ent.name[$-1].among('/', '\\'))
947 			fullname = ent.name[0 .. $-1];
948 		ret.name = baseName(fullname);
949 		ret.directory = NativePath.fromTrustedString(dirName(fullname));
950 	}
951 
952 	try {
953 		ret.isFile = ent.isFile;
954 		ret.isDirectory = ent.isDir;
955 		ret.isSymlink = ent.isSymlink;
956 		ret.timeModified = ent.timeLastModified;
957 		version(Windows) ret.timeCreated = ent.timeCreated;
958 		else ret.timeCreated = ent.timeLastModified;
959 		ret.size = ent.size;
960 	} catch (Exception e) {
961 		logDebug("Failed to get information for file '%s': %s", fullname, e.msg);
962 	}
963 
964 	version (Windows) {
965 		import core.sys.windows.windows : FILE_ATTRIBUTE_HIDDEN;
966 		ret.hidden = (ent.attributes & FILE_ATTRIBUTE_HIDDEN) != 0;
967 	}
968 	else ret.hidden = ret.name.length > 1 && ret.name[0] == '.' && ret.name != "..";
969 
970 	return ret;
971 }
972 
973 version (Windows) {} else unittest {
974 	void test(string name_in, string name_out, bool hidden) {
975 		auto de = DirEntry(name_in);
976 		assert(makeFileInfo(de).hidden == hidden);
977 		assert(makeFileInfo(de).name == name_out);
978 	}
979 
980 	void testCreate(string name_in, string name_out, bool hidden)
981 	{
982 		if (name_in.endsWith("/"))
983 			createDirectory(name_in);
984 		else writeFileUTF8(NativePath(name_in), name_in);
985 		scope (exit) removeFile(name_in);
986 		test(name_in, name_out, hidden);
987 	}
988 
989 	test(".", ".", false);
990 	test("..", "..", false);
991 	testCreate(".test_foo", ".test_foo", true);
992 	test("./", ".", false);
993 	testCreate(".test_foo/", ".test_foo", true);
994 	test("/", "", false);
995 }
996 
997 unittest {
998 	auto name = "toAppend.txt";
999 	scope(exit) removeFile(name);
1000 
1001 	{
1002 		auto handle = openFile(name, FileMode.createTrunc);
1003 		handle.write("create,");
1004 		assert(handle.tell() == "create,".length);
1005 		handle.close();
1006 	}
1007 	{
1008 		auto handle = openFile(name, FileMode.append);
1009 		handle.write(" then append");
1010 		assert(handle.tell() == "create, then append".length);
1011 		handle.close();
1012 	}
1013 
1014 	assert(readFile(name) == "create, then append");
1015 }
1016 
1017 
1018 private auto performInWorker(C, ARGS...)(C callable, auto ref ARGS args)
1019 {
1020 	version (none) {
1021 		import vibe.core.concurrency : asyncWork;
1022 		return asyncWork(callable, args).getResult();
1023 	} else {
1024 		import vibe.core.core : runWorkerTask;
1025 		import core.atomic : atomicFence;
1026 		import std.concurrency : Tid, send, receiveOnly, thisTid;
1027 
1028 		struct R {}
1029 
1030 		alias RET = typeof(callable(args));
1031 		shared(RET) ret;
1032 		runWorkerTask(ioTaskSettings, (shared(RET)* r, Tid caller, C c, ref ARGS a) nothrow {
1033 			*() @trusted { return cast(RET*)r; } () = c(a);
1034 			// Just as a precaution, because ManualEvent is not well defined in
1035 			// terms of fence semantics
1036 			atomicFence();
1037 			try caller.send(R.init);
1038 			catch (Exception e) assert(false, e.msg);
1039 		}, () @trusted { return &ret; } (), thisTid, callable, args);
1040 		() @trusted { receiveOnly!R(); } ();
1041 		atomicFence();
1042 		return ret;
1043 	}
1044 }
1045 
1046 private void performListDirectory(ListDirectoryRequest req)
1047 @trusted nothrow {
1048 	scope (exit) req.channel.close();
1049 
1050 	auto dirs_only = !!(req.spanMode & DirectoryListMode.shallowDirectories);
1051 	auto rec = !!(req.spanMode & DirectoryListMode.recursive);
1052 
1053 	bool scanRec(NativePath path)
1054 	{
1055 		import std.algorithm.comparison : among;
1056 		import std.algorithm.searching : countUntil;
1057 
1058 		version (Windows) {
1059 			import core.sys.windows.windows : FILE_ATTRIBUTE_DIRECTORY,
1060 				FILE_ATTRIBUTE_DEVICE, FILE_ATTRIBUTE_HIDDEN,
1061 				FILE_ATTRIBUTE_REPARSE_POINT, FINDEX_INFO_LEVELS, FINDEX_SEARCH_OPS,
1062 				INVALID_HANDLE_VALUE, WIN32_FIND_DATAW,
1063 				FindFirstFileExW, FindNextFileW, FindClose;
1064 			import std.conv : to;
1065 			import std.utf : toUTF16z;
1066 			import std.windows.syserror : wenforce;
1067 
1068 			static immutable timebase = SysTime(DateTime(1601, 1, 1), UTC());
1069 
1070 			WIN32_FIND_DATAW fd;
1071 			FINDEX_INFO_LEVELS lvl;
1072 			static if (is(typeof(FINDEX_INFO_LEVELS.FindExInfoBasic)))
1073 				lvl = FINDEX_INFO_LEVELS.FindExInfoBasic;
1074 			else lvl = cast(FINDEX_INFO_LEVELS)1;
1075 			auto fh = FindFirstFileExW((path.toString ~ "\\*").toUTF16z,
1076 				lvl, &fd, dirs_only ? FINDEX_SEARCH_OPS.FindExSearchLimitToDirectories
1077 					: FINDEX_SEARCH_OPS.FindExSearchNameMatch,
1078 				null, 2/*FIND_FIRST_EX_LARGE_FETCH*/);
1079 			wenforce(fh != INVALID_HANDLE_VALUE, path.toString);
1080 			scope (exit) FindClose(fh);
1081 			do {
1082 				// skip non-directories if requested
1083 				if (dirs_only && !(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
1084 					continue;
1085 
1086 				FileInfo fi;
1087 				auto zi = fd.cFileName[].representation.countUntil(0);
1088 				if (zi < 0) zi = fd.cFileName.length;
1089 				if (fd.cFileName[0 .. zi].among("."w, ".."w))
1090 					continue;
1091 				fi.name = fd.cFileName[0 .. zi].to!string;
1092 				fi.directory = path;
1093 				fi.size = (ulong(fd.nFileSizeHigh) << 32) + fd.nFileSizeLow;
1094 				fi.timeModified = timebase + hnsecs((ulong(fd.ftLastWriteTime.dwHighDateTime) << 32) + fd.ftLastWriteTime.dwLowDateTime);
1095 				fi.timeCreated = timebase + hnsecs((ulong(fd.ftCreationTime.dwHighDateTime) << 32) + fd.ftCreationTime.dwLowDateTime);
1096 				fi.isSymlink = !!(fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT);
1097 				fi.isDirectory = !!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
1098 				fi.isFile = !fi.isDirectory && !(fd.dwFileAttributes & FILE_ATTRIBUTE_DEVICE);
1099 				fi.hidden = !!(fd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN);
1100 
1101 				try req.channel.put(ListDirectoryData(fi, null));
1102 				catch (Exception e) return false; // channel got closed
1103 
1104 				if (fi.isDirectory && req.directoryPredicate)
1105 					if (!req.directoryPredicate(fi))
1106 						continue;
1107 
1108 				if (rec && fi.isDirectory) {
1109 					if (fi.isSymlink && !req.followSymlinks)
1110 						continue;
1111 					try {
1112 						if (!scanRec(path ~ NativePath.Segment2(fi.name)))
1113 							return false;
1114 					} catch (Exception e) {}
1115 				}
1116 			} while (FindNextFileW(fh, &fd));
1117 		} else {
1118 			import core.sys.posix.dirent : DT_DIR, DT_LNK, DT_UNKNOWN,
1119 				dirent, opendir, closedir, readdir;
1120 			import std.string : toStringz;
1121 
1122 			static immutable timebase = SysTime(DateTime(1970, 1, 1), UTC());
1123 
1124 			auto dir = opendir(path.toString.toStringz);
1125 			errnoEnforce(dir !is null, path.toString);
1126 			scope (exit) closedir(dir);
1127 
1128 			auto dfd = dirfd(dir);
1129 
1130 			dirent* de;
1131 			while ((de = readdir(dir)) !is null) {
1132 				// skip non-directories early, if possible
1133 				if (dirs_only && !de.d_type.among(DT_DIR, DT_LNK, DT_UNKNOWN))
1134 					continue;
1135 
1136 				FileInfo fi;
1137 				auto zi = de.d_name[].representation.countUntil(0);
1138 				if (zi < 0) zi = de.d_name.length;
1139 				if (de.d_name[0 .. zi].among(".", ".."))
1140 					continue;
1141 
1142 				fi.name = de.d_name[0 .. zi].idup;
1143 				fi.directory = path;
1144 				fi.hidden = de.d_name[0] == '.';
1145 
1146 				static SysTime getTimeField(string f)(ref const stat_t st)
1147 				{
1148 					long secs, nsecs;
1149 					static if (is(typeof(__traits(getMember, st, f)))) {
1150 						secs = __traits(getMember, st, f).tv_sec;
1151 						nsecs = __traits(getMember, st, f).tv_nsec;
1152 					} else {
1153 						secs = __traits(getMember, st, f ~ "e");
1154 						static if (is(typeof(__traits(getMember, st, f ~ "ensec"))))
1155 							nsecs = __traits(getMember, st, f ~ "ensec");
1156 						else static if (is(typeof(__traits(getMember, st, "__" ~ f ~ "ensec"))))
1157 							nsecs = __traits(getMember, st, "__" ~ f ~ "ensec");
1158 						else static if (is(typeof(__traits(getMember, st, f ~ "e_nsec"))))
1159 							nsecs = __traits(getMember, st, f ~ "e_nsec");
1160 						else static if (is(typeof(__traits(getMember, st, "__" ~ f ~ "e_nsec"))))
1161 							nsecs = __traits(getMember, st, "__" ~ f ~ "e_nsec");
1162 						else static assert(false, "Found no nanoseconds fields in struct stat");
1163 					}
1164 					return timebase + secs.seconds + (nsecs / 100).hnsecs;
1165 				}
1166 
1167 				stat_t st;
1168 				if (fstatat(dfd, fi.name.toStringz, &st, AT_SYMLINK_NOFOLLOW) == 0) {
1169 					fi.isSymlink = S_ISLNK(st.st_mode);
1170 
1171 					// apart from the symlink flag, get the rest of the information from the link target
1172 					if (fi.isSymlink) fstatat(dfd, fi.name.toStringz, &st, 0);
1173 
1174 					fi.size = st.st_size;
1175 					fi.timeModified = getTimeField!"st_mtim"(st);
1176 					fi.timeCreated = getTimeField!"st_ctim"(st);
1177 					fi.isDirectory = S_ISDIR(st.st_mode);
1178 					fi.isFile = S_ISREG(st.st_mode);
1179 				}
1180 
1181 				// skip non-directories if requested
1182 				if (dirs_only && !fi.isDirectory)
1183 					continue;
1184 
1185 				try req.channel.put(ListDirectoryData(fi, null));
1186 				catch (Exception e) return false; // channel got closed
1187 
1188 				if (fi.isDirectory && req.directoryPredicate)
1189 					if (!req.directoryPredicate(fi))
1190 						continue;
1191 
1192 				if (rec && fi.isDirectory) {
1193 					if (fi.isSymlink && !req.followSymlinks)
1194 						continue;
1195 					try {
1196 						if (!scanRec(path ~ NativePath.Segment2(fi.name)))
1197 							return false;
1198 					} catch (Exception e) {}
1199 				}
1200 			}
1201 		}
1202 
1203 		return true;
1204 	}
1205 
1206 	try scanRec(req.path);
1207 	catch (Exception e) {
1208 		logException(e, "goo");
1209 		try req.channel.put(ListDirectoryData(FileInfo.init, e.msg.length ? e.msg : "Failed to iterate directory"));
1210 		catch (Exception e2) {} // channel got closed
1211 	}
1212 }
1213 
1214 version (Posix) {
1215 	import core.sys.posix.dirent : DIR;
1216 	import core.sys.posix.sys.stat : stat;
1217 	extern(C) @safe nothrow @nogc {
1218 		static if (!is(typeof(dirfd)))
1219 			 int dirfd(DIR*);
1220 		static if (!is(typeof(fstatat))) {
1221 			version (OSX) {
1222     				version (AArch64) {
1223         				int fstatat(int dirfd, const(char)* pathname, stat_t *statbuf, int flags);
1224         			} else {
1225 						pragma(mangle, "fstatat$INODE64")
1226 						int fstatat(int dirfd, const(char)* pathname, stat_t *statbuf, int flags);
1227     				}
1228 			} else int fstatat(int dirfd, const(char)* pathname, stat_t *statbuf, int flags);
1229 		}
1230 	}
1231 
1232 	version (darwin) {
1233 		static if (!is(typeof(AT_SYMLINK_NOFOLLOW)))
1234 			enum AT_SYMLINK_NOFOLLOW = 0x0020;
1235 	}
1236 
1237 	version (CRuntime_Musl) {
1238 		static if (!is(typeof(AT_SYMLINK_NOFOLLOW)))
1239 			enum AT_SYMLINK_NOFOLLOW = 0x0100;
1240 	}
1241 
1242 	version (Android) {
1243 		static if (!is(typeof(AT_SYMLINK_NOFOLLOW)))
1244 			enum AT_SYMLINK_NOFOLLOW = 0x0100;
1245 	}
1246 }
1247 
1248 private immutable TaskSettings ioTaskSettings = { priority: 20 * Task.basePriority };
1249 
1250 private struct ListDirectoryData {
1251 	FileInfo info;
1252 	string error;
1253 }
1254 
1255 private struct ListDirectoryRequest {
1256 	import vibe.core.channel : Channel;
1257 
1258 	NativePath path;
1259 	DirectoryListMode spanMode;
1260 	Channel!ListDirectoryData channel;
1261 	bool followSymlinks;
1262 	bool function(ref const FileInfo) @safe nothrow directoryPredicate;
1263 }