/** File handling functions and types. Copyright: © 2012-2016 RejectedSoftware e.K. 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; //public import vibe.core.stream; //public import vibe.inet.url; import vibe.core.path; import core.stdc.stdio; import core.sys.posix.unistd; import core.sys.posix.fcntl; import core.sys.posix.sys.stat; import std.conv : octal; import vibe.core.log; import std.datetime; import std.exception; import std.file; import std.path; import std.string; version(Posix){ private extern(C) int mkstemps(char* templ, int suffixlen); } /** Opens a file stream with the specified mode. */ FileStream openFile(Path path, FileMode mode = FileMode.read) { assert(false); //return eventDriver.openFile(path, mode); } /// ditto FileStream openFile(string path, FileMode mode = FileMode.read) { return openFile(Path(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(Path 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(Path(path), buffer, max_size); } /** Write a whole file at once. */ void writeFile(Path 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(Path(path), contents); } /** Convenience function to append to a file. */ void appendToFile(Path path, string data) { auto fil = openFile(path, FileMode.append); scope(exit) fil.close(); fil.write(data); } /// ditto void appendToFile(string path, string data) { appendToFile(Path(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(Path path) { import vibe.internal.string; return stripUTF8Bom(sanitizeUTF8(readFile(path))); } /// ditto string readFileUTF8(string path) { return readFileUTF8(Path(path)); } /** Write a string into a UTF-8 encoded file. The file will have a byte order mark (BOM) prepended. */ void writeFileUTF8(Path 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; char[L_tmpnam] tmp; tmpnam(tmp.ptr); auto tmpname = to!string(tmp.ptr); 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 = mkstemps(templ.ptr, cast(int)suffix.length); enforce(fd >= 0, "Failed to create temporary file."); assert(false); //return eventDriver.adoptFile(fd, Path(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(Path from, Path to, bool copy_fallback = false) { moveFile(from.toNativeString(), to.toNativeString(), copy_fallback); } /// ditto void moveFile(string from, string to, bool copy_fallback = false) { if (!copy_fallback) { std.file.rename(from, to); } else { try { std.file.rename(from, to); } catch (FileException e) { std.file.copy(from, to); std.file.remove(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(Path from, Path to, bool overwrite = false) { { 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.write(src); } // TODO: retain attributes and time stamps } /// ditto void copyFile(string from, string to) { copyFile(Path(from), Path(to)); } /** Removes a file */ void removeFile(Path path) { removeFile(path.toNativeString()); } /// ditto void removeFile(string path) { std.file.remove(path); } /** Checks if a file exists */ bool existsFile(Path 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"); return std.file.exists(path); } /** Stores information about the specified file/directory into 'info' Throws: A `FileException` is thrown if the file does not exist. */ FileInfo getFileInfo(Path path) { auto ent = DirEntry(path.toNativeString()); return makeFileInfo(ent); } /// ditto FileInfo getFileInfo(string path) { return getFileInfo(Path(path)); } /** Creates a new directory. */ void createDirectory(Path path) { mkdir(path.toNativeString()); } /// ditto void createDirectory(string path) { createDirectory(Path(path)); } /** Enumerates all files in the specified directory. */ void listDirectory(Path path, scope bool delegate(FileInfo info) del) { foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) ) if( !del(makeFileInfo(ent)) ) break; } /// ditto void listDirectory(string path, scope bool delegate(FileInfo info) del) { listDirectory(Path(path), del); } /// ditto int delegate(scope int delegate(ref FileInfo)) iterateDirectory(Path path) { int iterator(scope int delegate(ref FileInfo) del){ int ret = 0; listDirectory(path, (fi){ ret = del(fi); return ret == 0; }); return ret; } return &iterator; } /// ditto int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path) { return iterateDirectory(Path(path)); } /** Starts watching a directory for changes. */ DirectoryWatcher watchDirectory(Path path, bool recursive = true) { assert(false); //return eventDriver.watchDirectory(path, recursive); } // ditto DirectoryWatcher watchDirectory(string path, bool recursive = true) { return watchDirectory(Path(path), recursive); } /** Returns the current working directory. */ Path getWorkingDirectory() { return Path(std.file.getcwd()); } /** Contains general information about a file. */ struct FileInfo { /// Name of the file (not including the path) string name; /// 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; } /** Specifies how a file is manipulated on disk. */ enum FileMode { /// The file is opened read-only. read, /// The file is opened for read-write random access. readWrite, /// The file is truncated if it exists or created otherwise and then opened for read-write access. createTrunc, /// The file is opened for appending data to it and created if it does not exist. append } /** Accesses the contents of a file as a stream. */ struct FileStream { import std.algorithm.comparison : min; import vibe.core.core : yield; import core.stdc.errno; version (Windows) {} else { enum O_BINARY = 0; } private { int m_fileDescriptor; Path m_path; ulong m_size; ulong m_ptr = 0; FileMode m_mode; bool m_ownFD = true; } this(Path path, FileMode mode) { auto pathstr = path.toNativeString(); final switch(mode){ case FileMode.read: m_fileDescriptor = open(pathstr.toStringz(), O_RDONLY|O_BINARY); break; case FileMode.readWrite: m_fileDescriptor = open(pathstr.toStringz(), O_RDWR|O_BINARY); break; case FileMode.createTrunc: m_fileDescriptor = open(pathstr.toStringz(), O_RDWR|O_CREAT|O_TRUNC|O_BINARY, octal!644); break; case FileMode.append: m_fileDescriptor = open(pathstr.toStringz(), O_WRONLY|O_CREAT|O_APPEND|O_BINARY, octal!644); break; } if( m_fileDescriptor < 0 ) //throw new Exception(format("Failed to open '%s' with %s: %d", pathstr, cast(int)mode, errno)); throw new Exception("Failed to open file '"~pathstr~"'."); this(m_fileDescriptor, path, mode); } this(int fd, Path path, FileMode mode) { assert(fd >= 0); m_fileDescriptor = fd; m_path = path; m_mode = mode; version(linux){ // stat_t seems to be defined wrong on linux/64 m_size = lseek(m_fileDescriptor, 0, SEEK_END); } else { stat_t st; fstat(m_fileDescriptor, &st); m_size = st.st_size; // (at least) on windows, the created file is write protected version(Windows){ if( mode == FileMode.createTrunc ) chmod(path.toNativeString().toStringz(), S_IREAD|S_IWRITE); } } lseek(m_fileDescriptor, 0, SEEK_SET); logDebug("opened file %s with %d bytes as %d", path.toNativeString(), m_size, m_fileDescriptor); } ~this() { close(); } @property int fd() { return m_fileDescriptor; } /// The path of the file. @property Path path() const { return m_path; } /// Determines if the file stream is still open @property bool isOpen() const { return m_fileDescriptor >= 0; } @property ulong size() const { return m_size; } @property bool readable() const { return m_mode != FileMode.append; } @property bool writable() const { return m_mode != FileMode.read; } void takeOwnershipOfFD() { enforce(m_ownFD); m_ownFD = false; } void seek(ulong offset) { version (Win32) { enforce(offset <= off_t.max, "Cannot seek above 4GB on Windows x32."); auto pos = lseek(m_fileDescriptor, cast(off_t)offset, SEEK_SET); } else auto pos = lseek(m_fileDescriptor, offset, SEEK_SET); enforce(pos == offset, "Failed to seek in file."); m_ptr = offset; } ulong tell() { return m_ptr; } /// Closes the file handle. void close() { if( m_fileDescriptor != -1 && m_ownFD ){ .close(m_fileDescriptor); m_fileDescriptor = -1; } } @property bool empty() const { assert(this.readable); return m_ptr >= m_size; } @property ulong leastSize() const { assert(this.readable); return m_size - m_ptr; } @property bool dataAvailableForRead() { return true; } const(ubyte)[] peek() { return null; } void read(ubyte[] dst) { assert(this.readable); while (dst.length > 0) { enforce(dst.length <= leastSize); auto sz = min(dst.length, 4096); enforce(.read(m_fileDescriptor, dst.ptr, cast(int)sz) == sz, "Failed to read data from disk."); dst = dst[sz .. $]; m_ptr += sz; yield(); } } void write(in ubyte[] bytes_) { const(ubyte)[] bytes = bytes_; assert(this.writable); while (bytes.length > 0) { auto sz = min(bytes.length, 4096); auto ret = .write(m_fileDescriptor, bytes.ptr, cast(int)sz); import std.format : format; enforce(ret == sz, format("Failed to write data to disk. %s %s %s %s", sz, errno, ret, m_fileDescriptor)); bytes = bytes[sz .. $]; m_ptr += sz; yield(); } } void write(InputStream)(InputStream stream, ulong nbytes = 0) { writeDefault(stream, nbytes); } void flush() { assert(this.writable); } void finalize() { flush(); } } private void writeDefault(OutputStream, InputStream)(ref OutputStream dst, InputStream stream, ulong nbytes = 0) { assert(false); /* static struct Buffer { ubyte[64*1024] bytes = void; } auto bufferobj = FreeListRef!(Buffer, false)(); auto buffer = bufferobj.bytes[]; //logTrace("default write %d bytes, empty=%s", nbytes, stream.empty); if (nbytes == 0) { while (!stream.empty) { size_t chunk = min(stream.leastSize, buffer.length); assert(chunk > 0, "leastSize returned zero for non-empty stream."); //logTrace("read pipe chunk %d", chunk); stream.read(buffer[0 .. chunk]); dst.write(buffer[0 .. chunk]); } } else { while (nbytes > 0) { size_t chunk = min(nbytes, buffer.length); //logTrace("read pipe chunk %d", chunk); stream.read(buffer[0 .. chunk]); dst.write(buffer[0 .. chunk]); nbytes -= chunk; } } */ } /** 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. */ interface DirectoryWatcher { /// The path of the watched directory @property Path path() const; /// Indicates if the directory is watched recursively @property bool recursive() const; /** 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 Returns: If the call completed successfully, true is returned. */ bool readChanges(ref DirectoryChange[] dst, Duration timeout = dur!"seconds"(-1)); } /** 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 Path path; } private FileInfo makeFileInfo(DirEntry ent) { FileInfo ret; ret.name = baseName(ent.name); if( ret.name.length == 0 ) ret.name = ent.name; assert(ret.name.length > 0); ret.size = ent.size; ret.timeModified = ent.timeLastModified; version(Windows) ret.timeCreated = ent.timeCreated; else ret.timeCreated = ent.timeLastModified; ret.isSymlink = ent.isSymlink; ret.isDirectory = ent.isDir; return ret; }