/** File handling functions and types. Copyright: © 2012-2019 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ module vibe.core.file; import eventcore.core : NativeEventDriver, eventDriver; import eventcore.driver; import vibe.core.internal.release; import vibe.core.log; import vibe.core.path; import vibe.core.stream; import vibe.core.task : Task, TaskSettings; import vibe.internal.async : asyncAwait, asyncAwaitUninterruptible; import core.stdc.stdio; import core.sys.posix.unistd; import core.sys.posix.fcntl; import core.sys.posix.sys.stat; import core.time; import std.conv : octal; import std.datetime; import std.exception; import std.file; import std.path; import std.string; import std.typecons : Flag, No; version(Posix){ private extern(C) int mkstemps(char* templ, int suffixlen); } @safe: /** Opens a file stream with the specified mode. */ FileStream openFile(NativePath path, FileMode mode = FileMode.read) { auto fil = eventDriver.files.open(path.toNativeString(), cast(FileOpenMode)mode); enforce(fil != FileFD.invalid, "Failed to open file '"~path.toNativeString~"'"); return FileStream(fil, path, mode); } /// ditto FileStream openFile(string path, FileMode mode = FileMode.read) { return openFile(NativePath(path), mode); } /** Read a whole file into a buffer. If the supplied buffer is large enough, it will be used to store the contents of the file. Otherwise, a new buffer will be allocated. Params: path = The path of the file to read buffer = An optional buffer to use for storing the file contents */ ubyte[] readFile(NativePath path, ubyte[] buffer = null, size_t max_size = size_t.max) { auto fil = openFile(path); scope (exit) fil.close(); enforce(fil.size <= max_size, "File is too big."); auto sz = cast(size_t)fil.size; auto ret = sz <= buffer.length ? buffer[0 .. sz] : new ubyte[sz]; fil.read(ret); return ret; } /// ditto ubyte[] readFile(string path, ubyte[] buffer = null, size_t max_size = size_t.max) { return readFile(NativePath(path), buffer, max_size); } /** Write a whole file at once. */ void writeFile(NativePath path, in ubyte[] contents) { auto fil = openFile(path, FileMode.createTrunc); scope (exit) fil.close(); fil.write(contents); } /// ditto void writeFile(string path, in ubyte[] contents) { writeFile(NativePath(path), contents); } /** Convenience function to append to a file. */ void appendToFile(NativePath path, string data) { auto fil = openFile(path, FileMode.append); scope(exit) fil.close(); fil.write(data); } /// ditto void appendToFile(string path, string data) { appendToFile(NativePath(path), data); } /** Read a whole UTF-8 encoded file into a string. The resulting string will be sanitized and will have the optional byte order mark (BOM) removed. */ string readFileUTF8(NativePath path) { import vibe.internal.string; auto data = readFile(path); auto idata = () @trusted { return data.assumeUnique; } (); return stripUTF8Bom(sanitizeUTF8(idata)); } /// ditto string readFileUTF8(string path) { return readFileUTF8(NativePath(path)); } /** Write a string into a UTF-8 encoded file. The file will have a byte order mark (BOM) prepended. */ void writeFileUTF8(NativePath path, string contents) { static immutable ubyte[] bom = [0xEF, 0xBB, 0xBF]; auto fil = openFile(path, FileMode.createTrunc); scope (exit) fil.close(); fil.write(bom); fil.write(contents); } /** Creates and opens a temporary file for writing. */ FileStream createTempFile(string suffix = null) { version(Windows){ import std.conv : to; string tmpname; () @trusted { auto fn = tmpnam(null); enforce(fn !is null, "Failed to generate temporary name."); tmpname = to!string(fn); } (); if (tmpname.startsWith("\\")) tmpname = tmpname[1 .. $]; tmpname ~= suffix; return openFile(tmpname, FileMode.createTrunc); } else { enum pattern ="/tmp/vtmp.XXXXXX"; scope templ = new char[pattern.length+suffix.length+1]; templ[0 .. pattern.length] = pattern; templ[pattern.length .. $-1] = (suffix)[]; templ[$-1] = '\0'; assert(suffix.length <= int.max); auto fd = () @trusted { return mkstemps(templ.ptr, cast(int)suffix.length); } (); enforce(fd >= 0, "Failed to create temporary file."); auto efd = eventDriver.files.adopt(fd); return FileStream(efd, NativePath(templ[0 .. $-1].idup), FileMode.createTrunc); } } /** Moves or renames a file. Params: from = Path to the file/directory to move/rename. to = The target path copy_fallback = Determines if copy/remove should be used in case of the source and destination path pointing to different devices. */ void moveFile(NativePath from, NativePath to, bool copy_fallback = false) { moveFile(from.toNativeString(), to.toNativeString(), copy_fallback); } /// ditto void moveFile(string from, string to, bool copy_fallback = false) { auto fail = performInWorker((string from, string to) { try { std.file.rename(from, to); } catch (Exception e) { return e.msg.length ? e.msg : "Failed to move file."; } return null; }, from, to); if (!fail.length) return; if (!copy_fallback) throw new Exception(fail); copyFile(from, to); removeFile(from); } /** Copies a file. Note that attributes and time stamps are currently not retained. Params: from = Path of the source file to = Path for the destination file overwrite = If true, any file existing at the destination path will be overwritten. If this is false, an exception will be thrown should a file already exist at the destination path. Throws: An Exception if the copy operation fails for some reason. */ void copyFile(NativePath from, NativePath to, bool overwrite = false) { DirEntry info; static if (__VERSION__ < 2078) { () @trusted { info = DirEntry(from.toString); enforce(info.isFile, "The source path is not a file and cannot be copied."); } (); } else { info = DirEntry(from.toString); enforce(info.isFile, "The source path is not a file and cannot be copied."); } { auto src = openFile(from, FileMode.read); scope(exit) src.close(); enforce(overwrite || !existsFile(to), "Destination file already exists."); auto dst = openFile(to, FileMode.createTrunc); scope(exit) dst.close(); dst.truncate(src.size); src.pipe(dst, PipeMode.concurrent); } // TODO: also retain creation time on windows static if (__VERSION__ < 2078) { () @trusted { setTimes(to.toString, info.timeLastAccessed, info.timeLastModified); setAttributes(to.toString, info.attributes); } (); } else { setTimes(to.toString, info.timeLastAccessed, info.timeLastModified); setAttributes(to.toString, info.attributes); } } /// ditto void copyFile(string from, string to) { copyFile(NativePath(from), NativePath(to)); } /** Removes a file */ void removeFile(NativePath path) { removeFile(path.toNativeString()); } /// ditto void removeFile(string path) { auto fail = performInWorker((string path) { try { std.file.remove(path); } catch (Exception e) { return e.msg.length ? e.msg : "Failed to delete file."; } return null; }, path); if (fail.length) throw new Exception(fail); } /** Checks if a file exists */ bool existsFile(NativePath path) nothrow { return existsFile(path.toNativeString()); } /// ditto bool existsFile(string path) nothrow { // This was *annotated* nothrow in 2.067. static if (__VERSION__ < 2067) scope(failure) assert(0, "Error: existsFile should never throw"); try return performInWorker((string p) => std.file.exists(p), path); catch (Exception e) { logDebug("Failed to determine file existence for '%s': %s", path, e.msg); return false; } } /** Stores information about the specified file/directory into 'info' Throws: A `FileException` is thrown if the file does not exist. */ FileInfo getFileInfo(NativePath path) @trusted { return getFileInfo(path.toNativeString); } /// ditto FileInfo getFileInfo(string path) { import std.typecons : tuple; auto ret = performInWorker((string p) { try { auto ent = DirEntry(p); return tuple(makeFileInfo(ent), ""); } catch (Exception e) { return tuple(FileInfo.init, e.msg.length ? e.msg : "Failed to get file information"); } }, path); if (ret[1].length) throw new Exception(ret[1]); return ret[0]; } /** Creates a new directory. */ void createDirectory(NativePath path) { createDirectory(path.toNativeString); } /// ditto void createDirectory(string path, Flag!"recursive" recursive = No.recursive) { auto fail = performInWorker((string p, bool rec) { try { if (rec) mkdirRecurse(p); else mkdir(p); } catch (Exception e) { return e.msg.length ? e.msg : "Failed to create directory."; } return null; }, path, !!recursive); if (fail) throw new Exception(fail); } /** Enumerates all files in the specified directory. */ void listDirectory(NativePath path, DirectoryListMode mode, scope bool delegate(FileInfo info) @safe del) { import vibe.core.channel : ChannelConfig, ChannelPriority, createChannel; import vibe.core.core : runWorkerTask; ChannelConfig cc; cc.priority = ChannelPriority.overhead; ListDirectoryRequest req; req.path = path; req.channel = createChannel!ListDirectoryData(cc); req.spanMode = mode; runWorkerTask(ioTaskSettings, &performListDirectory, req); ListDirectoryData itm; while (req.channel.tryConsumeOne(itm)) { if (itm.error.length) throw new Exception(itm.error); if (!del(itm.info)) { req.channel.close(); // makes sure that the directory handle is closed before returning while (!req.channel.empty) req.channel.tryConsumeOne(itm); break; } } } /// ditto void listDirectory(string path, DirectoryListMode mode, scope bool delegate(FileInfo info) @safe del) { listDirectory(NativePath(path), mode, del); } void listDirectory(NativePath path, scope bool delegate(FileInfo info) @safe del) { listDirectory(path, DirectoryListMode.shallow, del); } /// ditto void listDirectory(string path, scope bool delegate(FileInfo info) @safe del) { listDirectory(path, DirectoryListMode.shallow, del); } /// ditto void listDirectory(NativePath path, DirectoryListMode mode, scope bool delegate(FileInfo info) @system del) @system { listDirectory(path, mode, (nfo) @trusted => del(nfo)); } /// ditto void listDirectory(string path, DirectoryListMode mode, scope bool delegate(FileInfo info) @system del) @system { listDirectory(path, mode, (nfo) @trusted => del(nfo)); } /// ditto void listDirectory(NativePath path, scope bool delegate(FileInfo info) @system del) @system { listDirectory(path, (nfo) @trusted => del(nfo)); } /// ditto void listDirectory(string path, scope bool delegate(FileInfo info) @system del) @system { listDirectory(path, (nfo) @trusted => del(nfo)); } /// ditto int delegate(scope int delegate(ref FileInfo)) iterateDirectory(NativePath path, DirectoryListMode mode = DirectoryListMode.shallow) { int iterator(scope int delegate(ref FileInfo) del){ int ret = 0; listDirectory(path, mode, (fi) { ret = del(fi); return ret == 0; }); return ret; } return &iterator; } /// ditto int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path, DirectoryListMode mode = DirectoryListMode.shallow) { return iterateDirectory(NativePath(path), mode); } /** Starts watching a directory for changes. */ DirectoryWatcher watchDirectory(NativePath path, bool recursive = true) { return DirectoryWatcher(path, recursive); } // ditto DirectoryWatcher watchDirectory(string path, bool recursive = true) { return watchDirectory(NativePath(path), recursive); } /** Returns the current working directory. */ NativePath getWorkingDirectory() { return NativePath(() @trusted { return std.file.getcwd(); } ()); } /** Contains general information about a file. */ struct FileInfo { /// Name of the file (not including the path) string name; /// The directory containing the file NativePath directory; /// Size of the file (zero for directories) ulong size; /// Time of the last modification SysTime timeModified; /// Time of creation (not available on all operating systems/file systems) SysTime timeCreated; /// True if this is a symlink to an actual file bool isSymlink; /// True if this is a directory or a symlink pointing to a directory bool isDirectory; /// True if this is a file. On POSIX if both isFile and isDirectory are false it is a special file. bool isFile; /** True if the file's hidden attribute is set. On systems that don't support a hidden attribute, any file starting with a single dot will be treated as hidden. */ bool hidden; } /** Specifies how a file is manipulated on disk. */ enum FileMode { /// The file is opened read-only. read = FileOpenMode.read, /// The file is opened for read-write random access. readWrite = FileOpenMode.readWrite, /// The file is truncated if it exists or created otherwise and then opened for read-write access. createTrunc = FileOpenMode.createTrunc, /// The file is opened for appending data to it and created if it does not exist. append = FileOpenMode.append } enum DirectoryListMode { /// Only iterate the directory itself shallow = 0, /// Only iterate over directories directly within the given directory shallowDirectories = 1<<1, /// Iterate recursively (depth-first, pre-order) recursive = 1<<0, /// Iterate only directories recursively (depth-first, pre-order) recursiveDirectories = recursive | shallowDirectories, } /** Accesses the contents of a file as a stream. */ struct FileStream { @safe: private struct CTX { NativePath path; ulong size; FileMode mode; ulong ptr; shared(NativeEventDriver) driver; } private { FileFD m_fd; CTX* m_ctx; } private this(FileFD fd, NativePath path, FileMode mode) { assert(fd != FileFD.invalid, "Constructing FileStream from invalid file descriptor."); m_fd = fd; m_ctx = new CTX; // TODO: use FD custom storage m_ctx.path = path; m_ctx.mode = mode; m_ctx.size = eventDriver.files.getSize(fd); m_ctx.driver = () @trusted { return cast(shared)eventDriver; } (); if (mode == FileMode.append) m_ctx.ptr = m_ctx.size; } this(this) { if (m_fd != FileFD.invalid) eventDriver.files.addRef(m_fd); } ~this() { if (m_fd != FileFD.invalid) releaseHandle!"files"(m_fd, m_ctx.driver); } @property int fd() { return cast(int)m_fd; } /// The path of the file. @property NativePath path() const { return ctx.path; } /// Determines if the file stream is still open @property bool isOpen() const { return m_fd != FileFD.invalid; } @property ulong size() const nothrow { return ctx.size; } @property bool readable() const nothrow { return ctx.mode != FileMode.append; } @property bool writable() const nothrow { return ctx.mode != FileMode.read; } bool opCast(T)() if (is (T == bool)) { return m_fd != FileFD.invalid; } void takeOwnershipOfFD() { assert(false, "TODO!"); } void seek(ulong offset) { enforce(ctx.mode != FileMode.append, "File opened for appending, not random access. Cannot seek."); ctx.ptr = offset; } ulong tell() nothrow { return ctx.ptr; } void truncate(ulong size) { enforce(ctx.mode != FileMode.append, "File opened for appending, not random access. Cannot truncate."); auto res = asyncAwaitUninterruptible!(FileIOCallback, cb => eventDriver.files.truncate(m_fd, size, cb) ); enforce(res[1] == IOStatus.ok, "Failed to resize file."); m_ctx.size = size; } /// Closes the file handle. void close() { if (m_fd == FileFD.invalid) return; if (!eventDriver.files.isValid(m_fd)) return; auto res = asyncAwaitUninterruptible!(FileCloseCallback, cb => eventDriver.files.close(m_fd, cb) ); releaseHandle!"files"(m_fd, m_ctx.driver); m_fd = FileFD.invalid; m_ctx = null; if (res[1] != CloseStatus.ok) throw new Exception("Failed to close file"); } @property bool empty() const { assert(this.readable); return ctx.ptr >= ctx.size; } @property ulong leastSize() const { assert(this.readable); return ctx.size - ctx.ptr; } @property bool dataAvailableForRead() { return true; } const(ubyte)[] peek() { return null; } size_t read(ubyte[] dst, IOMode mode) { // NOTE: cancelRead is currently not behaving as specified and cannot // be relied upon. For this reason, we MUST use the uninterruptible // version of asyncAwait here! auto res = asyncAwaitUninterruptible!(FileIOCallback, cb => eventDriver.files.read(m_fd, ctx.ptr, dst, mode, cb) ); ctx.ptr += res[2]; enforce(res[1] == IOStatus.ok, "Failed to read data from disk."); return res[2]; } void read(ubyte[] dst) { auto ret = read(dst, IOMode.all); assert(ret == dst.length, "File.read returned less data than requested for IOMode.all."); } size_t write(in ubyte[] bytes, IOMode mode) { // NOTE: cancelWrite is currently not behaving as specified and cannot // be relied upon. For this reason, we MUST use the uninterruptible // version of asyncAwait here! auto res = asyncAwaitUninterruptible!(FileIOCallback, cb => eventDriver.files.write(m_fd, ctx.ptr, bytes, mode, cb) ); ctx.ptr += res[2]; if (ctx.ptr > ctx.size) ctx.size = ctx.ptr; enforce(res[1] == IOStatus.ok, "Failed to write data to disk."); return res[2]; } void write(in ubyte[] bytes) { write(bytes, IOMode.all); } void write(in char[] bytes) { write(cast(const(ubyte)[])bytes); } void write(InputStream)(InputStream stream, ulong nbytes = ulong.max) if (isInputStream!InputStream) { pipe(stream, this, nbytes, PipeMode.concurrent); } void flush() { assert(this.writable); } void finalize() { flush(); } private inout(CTX)* ctx() inout nothrow { return m_ctx; } } mixin validateRandomAccessStream!FileStream; /** Interface for directory watcher implementations. Directory watchers monitor the contents of a directory (wither recursively or non-recursively) for changes, such as file additions, deletions or modifications. */ struct DirectoryWatcher { // TODO: avoid all those heap allocations! import std.array : Appender, appender; import vibe.core.sync : LocalManualEvent, createManualEvent; @safe: private static struct Context { NativePath path; bool recursive; Appender!(DirectoryChange[]) changes; LocalManualEvent changeEvent; shared(NativeEventDriver) driver; // Support for `-preview=in` static if (!is(typeof(mixin(q{(in ref int a) => a})))) { void onChange(WatcherID id, in FileChange change) nothrow { this.onChangeImpl(id, change); } } else { mixin(q{ void onChange(WatcherID id, in ref FileChange change) nothrow { this.onChangeImpl(id, change); }}); } void onChangeImpl(WatcherID, const scope ref FileChange change) nothrow { DirectoryChangeType ct; final switch (change.kind) { case FileChangeKind.added: ct = DirectoryChangeType.added; break; case FileChangeKind.removed: ct = DirectoryChangeType.removed; break; case FileChangeKind.modified: ct = DirectoryChangeType.modified; break; } static if (is(typeof(change.baseDirectory))) { // eventcore 0.8.23 and up this.changes ~= DirectoryChange(ct, NativePath.fromTrustedString(change.baseDirectory) ~ NativePath.fromTrustedString(change.directory) ~ NativePath.fromTrustedString(change.name.idup)); } else { this.changes ~= DirectoryChange(ct, NativePath.fromTrustedString(change.directory) ~ NativePath.fromTrustedString(change.name.idup)); } this.changeEvent.emit(); } } private { WatcherID m_watcher; Context* m_context; } private this(NativePath path, bool recursive) { m_context = new Context; // FIME: avoid GC allocation (use FD user data slot) m_context.changeEvent = createManualEvent(); m_watcher = eventDriver.watchers.watchDirectory(path.toNativeString, recursive, &m_context.onChange); enforce(m_watcher != WatcherID.invalid, "Failed to watch directory."); m_context.path = path; m_context.recursive = recursive; m_context.changes = appender!(DirectoryChange[]); m_context.driver = () @trusted { return cast(shared)eventDriver; } (); } this(this) nothrow { if (m_watcher != WatcherID.invalid) eventDriver.watchers.addRef(m_watcher); } ~this() nothrow { if (m_watcher != WatcherID.invalid) releaseHandle!"watchers"(m_watcher, m_context.driver); } /// The path of the watched directory @property NativePath path() const nothrow { return m_context.path; } /// Indicates if the directory is watched recursively @property bool recursive() const nothrow { return m_context.recursive; } /** Fills the destination array with all changes that occurred since the last call. The function will block until either directory changes have occurred or until the timeout has elapsed. Specifying a negative duration will cause the function to wait without a timeout. Params: dst = The destination array to which the changes will be appended timeout = Optional timeout for the read operation. A value of `Duration.max` will wait indefinitely. Returns: If the call completed successfully, true is returned. */ bool readChanges(ref DirectoryChange[] dst, Duration timeout = Duration.max) { if (timeout == Duration.max) { while (!m_context.changes.data.length) m_context.changeEvent.wait(Duration.max, m_context.changeEvent.emitCount); } else { MonoTime now = MonoTime.currTime(); MonoTime final_time = now + timeout; while (!m_context.changes.data.length) { m_context.changeEvent.wait(final_time - now, m_context.changeEvent.emitCount); now = MonoTime.currTime(); if (now >= final_time) break; } if (!m_context.changes.data.length) return false; } dst = m_context.changes.data; m_context.changes = appender!(DirectoryChange[]); return true; } } /** Specifies the kind of change in a watched directory. */ enum DirectoryChangeType { /// A file or directory was added added, /// A file or directory was deleted removed, /// A file or directory was modified modified } /** Describes a single change in a watched directory. */ struct DirectoryChange { /// The type of change DirectoryChangeType type; /// Path of the file/directory that was changed NativePath path; } private FileInfo makeFileInfo(DirEntry ent) @trusted nothrow { import std.algorithm.comparison : among; FileInfo ret; string fullname = ent.name; if (fullname.length) { if (ent.name[$-1].among('/', '\\')) fullname = ent.name[0 .. $-1]; ret.name = baseName(fullname); ret.directory = NativePath.fromTrustedString(dirName(fullname)); } try { ret.isFile = ent.isFile; ret.isDirectory = ent.isDir; ret.isSymlink = ent.isSymlink; ret.timeModified = ent.timeLastModified; version(Windows) ret.timeCreated = ent.timeCreated; else ret.timeCreated = ent.timeLastModified; ret.size = ent.size; } catch (Exception e) { logDebug("Failed to get information for file '%s': %s", fullname, e.msg); } version (Windows) { import core.sys.windows.windows : FILE_ATTRIBUTE_HIDDEN; ret.hidden = (ent.attributes & FILE_ATTRIBUTE_HIDDEN) != 0; } else ret.hidden = ret.name.length > 1 && ret.name[0] == '.' && ret.name != ".."; return ret; } version (Windows) {} else unittest { void test(string name_in, string name_out, bool hidden) { auto de = DirEntry(name_in); assert(makeFileInfo(de).hidden == hidden); assert(makeFileInfo(de).name == name_out); } void testCreate(string name_in, string name_out, bool hidden) { if (name_in.endsWith("/")) createDirectory(name_in); else writeFileUTF8(NativePath(name_in), name_in); scope (exit) removeFile(name_in); test(name_in, name_out, hidden); } test(".", ".", false); test("..", "..", false); testCreate(".test_foo", ".test_foo", true); test("./", ".", false); testCreate(".test_foo/", ".test_foo", true); test("/", "", false); } unittest { auto name = "toAppend.txt"; scope(exit) removeFile(name); { auto handle = openFile(name, FileMode.createTrunc); handle.write("create,"); assert(handle.tell() == "create,".length); handle.close(); } { auto handle = openFile(name, FileMode.append); handle.write(" then append"); assert(handle.tell() == "create, then append".length); handle.close(); } assert(readFile(name) == "create, then append"); } private auto performInWorker(C, ARGS...)(C callable, auto ref ARGS args) { version (none) { import vibe.core.concurrency : asyncWork; return asyncWork(callable, args).getResult(); } else { import vibe.core.core : runWorkerTask; import core.atomic : atomicFence; import std.concurrency : Tid, send, receiveOnly, thisTid; struct R {} alias RET = typeof(callable(args)); shared(RET) ret; runWorkerTask(ioTaskSettings, (shared(RET)* r, Tid caller, C c, ref ARGS a) nothrow { *() @trusted { return cast(RET*)r; } () = c(a); // Just as a precaution, because ManualEvent is not well defined in // terms of fence semantics atomicFence(); try caller.send(R.init); catch (Exception e) assert(false, e.msg); }, () @trusted { return &ret; } (), thisTid, callable, args); () @trusted { receiveOnly!R(); } (); atomicFence(); return ret; } } private void performListDirectory(ListDirectoryRequest req) @trusted nothrow { scope (exit) req.channel.close(); auto dirs_only = !!(req.spanMode & DirectoryListMode.shallowDirectories); auto rec = !!(req.spanMode & DirectoryListMode.recursive); bool scanRec(NativePath path) { import std.algorithm.comparison : among; import std.algorithm.searching : countUntil; version (Windows) { import core.sys.windows.windows : FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_DEVICE, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_REPARSE_POINT, FINDEX_INFO_LEVELS, FINDEX_SEARCH_OPS, INVALID_HANDLE_VALUE, WIN32_FIND_DATAW, FindFirstFileExW, FindNextFileW, FindClose; import std.conv : to; import std.utf : toUTF16z; import std.windows.syserror : wenforce; static immutable timebase = SysTime(DateTime(1601, 1, 1), UTC()); WIN32_FIND_DATAW fd; FINDEX_INFO_LEVELS lvl; static if (is(typeof(FINDEX_INFO_LEVELS.FindExInfoBasic))) lvl = FINDEX_INFO_LEVELS.FindExInfoBasic; else lvl = cast(FINDEX_INFO_LEVELS)1; auto fh = FindFirstFileExW((path.toString ~ "\\*").toUTF16z, lvl, &fd, dirs_only ? FINDEX_SEARCH_OPS.FindExSearchLimitToDirectories : FINDEX_SEARCH_OPS.FindExSearchNameMatch, null, 2/*FIND_FIRST_EX_LARGE_FETCH*/); wenforce(fh != INVALID_HANDLE_VALUE, path.toString); scope (exit) FindClose(fh); do { // skip non-directories if requested if (dirs_only && !(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue; FileInfo fi; auto zi = fd.cFileName[].representation.countUntil(0); if (zi < 0) zi = fd.cFileName.length; if (fd.cFileName[0 .. zi].among("."w, ".."w)) continue; fi.name = fd.cFileName[0 .. zi].to!string; fi.directory = path; fi.size = (ulong(fd.nFileSizeHigh) << 32) + fd.nFileSizeLow; fi.timeModified = timebase + hnsecs((ulong(fd.ftLastWriteTime.dwHighDateTime) << 32) + fd.ftLastWriteTime.dwLowDateTime); fi.timeCreated = timebase + hnsecs((ulong(fd.ftCreationTime.dwHighDateTime) << 32) + fd.ftCreationTime.dwLowDateTime); fi.isSymlink = !!(fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT); fi.isDirectory = !!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY); fi.isFile = !fi.isDirectory && !(fd.dwFileAttributes & FILE_ATTRIBUTE_DEVICE); fi.hidden = !!(fd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN); try req.channel.put(ListDirectoryData(fi, null)); catch (Exception e) return false; // channel got closed if (rec && fi.isDirectory) { if (fi.isSymlink && !req.followSymlinks) continue; try { if (!scanRec(path ~ NativePath.Segment2(fi.name))) return false; } catch (Exception e) {} } } while (FindNextFileW(fh, &fd)); } else { import core.sys.posix.dirent : DT_DIR, DT_LNK, DT_UNKNOWN, dirent, opendir, closedir, readdir; import std.string : toStringz; static immutable timebase = SysTime(DateTime(1970, 1, 1), UTC()); auto dir = opendir(path.toString.toStringz); errnoEnforce(dir !is null, path.toString); scope (exit) closedir(dir); auto dfd = dirfd(dir); dirent* de; while ((de = readdir(dir)) !is null) { // skip non-directories early, if possible if (dirs_only && !de.d_type.among(DT_DIR, DT_LNK, DT_UNKNOWN)) continue; FileInfo fi; auto zi = de.d_name[].representation.countUntil(0); if (zi < 0) zi = de.d_name.length; if (de.d_name[0 .. zi].among(".", "..")) continue; fi.name = de.d_name[0 .. zi].idup; fi.directory = path; fi.hidden = de.d_name[0] == '.'; static SysTime getTimeField(string f)(ref const stat_t st) { long secs, nsecs; static if (is(typeof(__traits(getMember, st, f)))) { secs = __traits(getMember, st, f).tv_sec; nsecs = __traits(getMember, st, f).tv_nsec; } else { secs = __traits(getMember, st, f ~ "e"); static if (is(typeof(__traits(getMember, st, f ~ "ensec")))) nsecs = __traits(getMember, st, f ~ "ensec"); else static if (is(typeof(__traits(getMember, st, "__" ~ f ~ "ensec")))) nsecs = __traits(getMember, st, "__" ~ f ~ "ensec"); else static if (is(typeof(__traits(getMember, st, "__" ~ f ~ "e_nsec")))) nsecs = __traits(getMember, st, "__" ~ f ~ "e_nsec"); else static assert(false, "Found no nanoseconds fields in struct stat"); } return timebase + secs.seconds + (nsecs / 100).hnsecs; } stat_t st; if (fstatat(dfd, fi.name.toStringz, &st, AT_SYMLINK_NOFOLLOW) == 0) { fi.isSymlink = S_ISLNK(st.st_mode); // apart from the symlink flag, get the rest of the information from the link target if (fi.isSymlink) fstatat(dfd, fi.name.toStringz, &st, 0); fi.size = st.st_size; fi.timeModified = getTimeField!"st_mtim"(st); fi.timeCreated = getTimeField!"st_ctim"(st); fi.isDirectory = S_ISDIR(st.st_mode); fi.isFile = S_ISREG(st.st_mode); } // skip non-directories if requested if (dirs_only && !fi.isDirectory) continue; try req.channel.put(ListDirectoryData(fi, null)); catch (Exception e) return false; // channel got closed if (rec && fi.isDirectory) { if (fi.isSymlink && !req.followSymlinks) continue; try { if (!scanRec(path ~ NativePath.Segment2(fi.name))) return false; } catch (Exception e) {} } } } return true; } try scanRec(req.path); catch (Exception e) { logException(e, "goo"); try req.channel.put(ListDirectoryData(FileInfo.init, e.msg.length ? e.msg : "Failed to iterate directory")); catch (Exception e2) {} // channel got closed } } version (Posix) { import core.sys.posix.dirent : DIR; import core.sys.posix.sys.stat : stat; extern(C) @safe nothrow @nogc { static if (!is(typeof(dirfd))) int dirfd(DIR*); static if (!is(typeof(fstatat))) { version (OSX) { pragma(mangle, "fstatat$INODE64") int fstatat(int dirfd, const(char)* pathname, stat_t *statbuf, int flags); } else int fstatat(int dirfd, const(char)* pathname, stat_t *statbuf, int flags); } } version (darwin) { static if (!is(typeof(AT_SYMLINK_NOFOLLOW))) enum AT_SYMLINK_NOFOLLOW = 0x0020; } version (CRuntime_Musl) { static if (!is(typeof(AT_SYMLINK_NOFOLLOW))) enum AT_SYMLINK_NOFOLLOW = 0x0100; } } private immutable TaskSettings ioTaskSettings = { priority: 20 * Task.basePriority }; private struct ListDirectoryData { FileInfo info; string error; } private struct ListDirectoryRequest { import vibe.core.channel : Channel; NativePath path; DirectoryListMode spanMode; Channel!ListDirectoryData channel; bool followSymlinks; }