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 }