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