vibe-core/source/vibe/core/file.d
2016-12-10 16:01:40 +01:00

653 lines
16 KiB
D

/**
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;
import eventcore.core : eventDriver;
import eventcore.driver;
import vibe.core.log;
import vibe.core.path;
import vibe.core.stream;
import vibe.internal.async : asyncAwait;
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 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);
}
@safe:
/**
Opens a file stream with the specified mode.
*/
FileStream openFile(Path path, FileMode mode = FileMode.read)
{
return FileStream(eventDriver.files.open(path.toNativeString(), cast(FileOpenMode)mode), 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 = () @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, 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)
@trusted {
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)
{
() @trusted { 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)
@trusted {
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)
{
return DirectoryWatcher(path, recursive);
}
// ditto
DirectoryWatcher watchDirectory(string path, bool recursive = true)
{
return watchDirectory(Path(path), recursive);
}
/**
Returns the current working directory.
*/
Path getWorkingDirectory()
{
return Path(() @trusted { return 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 = 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
}
/**
Accesses the contents of a file as a stream.
*/
struct FileStream {
@safe:
private {
FileFD m_fd;
Path m_path;
ulong m_size;
FileMode m_mode;
ulong m_ptr;
}
this(FileFD fd, Path path, FileMode mode)
{
m_fd = fd;
m_path = path;
m_mode = mode;
m_size = eventDriver.files.getSize(fd);
}
this(this)
{
if (m_fd != FileFD.invalid)
eventDriver.files.addRef(m_fd);
}
~this()
{
if (m_fd != FileFD.invalid)
eventDriver.files.releaseRef(m_fd);
}
@property int fd() { return m_fd; }
/// 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_fd != FileFD.invalid; }
@property ulong size() const nothrow { return m_size; }
@property bool readable() const nothrow { return m_mode != FileMode.append; }
@property bool writable() const nothrow { return m_mode != FileMode.read; }
bool opCast(T)() if (is (T == bool)) { return m_fd != FileFD.invalid; }
void takeOwnershipOfFD()
{
assert(false, "TODO!");
}
void seek(ulong offset)
{
m_ptr = offset;
}
ulong tell() nothrow { return m_ptr; }
/// Closes the file handle.
void close()
{
if (m_fd != FileFD.init) {
eventDriver.files.close(m_fd);
eventDriver.files.releaseRef(m_fd);
m_fd = FileFD.init;
}
}
@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)
{
auto res = asyncAwait!(FileIOCallback,
cb => eventDriver.files.read(m_fd, m_ptr, dst, cb),
cb => eventDriver.files.cancelRead(m_fd)
);
enforce(res[1] == IOStatus.ok, "Failed to read data from disk.");
}
void write(in ubyte[] bytes)
{
auto res = asyncAwait!(FileIOCallback,
cb => eventDriver.files.write(m_fd, m_ptr, bytes, cb),
cb => eventDriver.files.cancelWrite(m_fd)
);
m_ptr += res[2];
logDebug("Written %s", res[2]);
if (m_ptr > m_size) m_size = m_ptr;
enforce(res[1] == IOStatus.ok, "Failed to read data from disk.");
}
void write(in char[] bytes)
{
write(cast(const(ubyte)[])bytes);
}
void write(InputStream)(InputStream stream, ulong nbytes = 0)
if (isInputStream!InputStream)
{
writeDefault(this, stream, nbytes);
}
void flush()
{
assert(this.writable);
}
void finalize()
{
flush();
}
}
mixin validateRandomAccessStream!FileStream;
private void writeDefault(OutputStream, InputStream)(ref OutputStream dst, InputStream stream, ulong nbytes = 0)
if (isOutputStream!OutputStream && isInputStream!InputStream)
{
import vibe.internal.allocator : theAllocator, make, dispose;
import std.algorithm.comparison : min;
static struct Buffer { ubyte[64*1024] bytes = void; }
auto bufferobj = () @trusted { return theAllocator.make!Buffer(); } ();
scope (exit) () @trusted { theAllocator.dispose(bufferobj); } ();
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.
*/
struct DirectoryWatcher { // TODO: avoid all those heap allocations!
import std.array : Appender, appender;
import vibe.core.sync : ManualEvent;
@safe:
private struct Context {
Path path;
bool recursive;
Appender!(DirectoryChange[]) changes;
ManualEvent changeEvent;
}
private {
WatcherID m_watcher;
Context m_context;
}
private this(Path path, bool recursive)
{
m_watcher = eventDriver.watchers.watchDirectory(path.toNativeString, recursive, &onChange);
m_context.path = path;
m_context.recursive = recursive;
m_context.changes = appender!(DirectoryChange[]);
}
this(this) nothrow { if (m_watcher != WatcherID.invalid) eventDriver.watchers.addRef(m_watcher); }
~this() nothrow { if (m_watcher != WatcherID.invalid) eventDriver.watchers.releaseRef(m_watcher); }
/// The path of the watched directory
@property Path 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)
{
SysTime now = Clock.currTime(UTC());
SysTime final_time = now + timeout;
while (!m_context.changes.data.length) {
m_context.changeEvent.wait(final_time - now, m_context.changeEvent.emitCount);
if (m_context.changes.data.length) break;
else now = Clock.currTime(UTC());
}
if (!m_context.changes.data.length) return false;
dst = m_context.changes.data;
m_context.changes = appender!(DirectoryChange[]);
return true;
}
private void onChange(WatcherID, in 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;
}
m_context.changes ~= DirectoryChange(ct, Path(change.directory) ~ change.name.idup);
m_context.changeEvent.emit();
}
}
/** 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)
@trusted {
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;
}