1 /** 2 * File stuff 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.sys.file; 15 16 import core.stdc.wchar_; 17 import core.thread; 18 19 import std.array; 20 import std.conv; 21 import std.file; 22 import std.path; 23 import std.stdio : File; 24 import std.string; 25 import std.typecons; 26 import std.utf; 27 28 import ae.sys.cmd : getCurrentThreadID; 29 30 public import std.typecons : No, Yes; 31 32 alias wcscmp = core.stdc.wchar_.wcscmp; 33 alias wcslen = core.stdc.wchar_.wcslen; 34 35 version(Windows) import ae.sys.windows.imports; 36 37 // ************************************************************************ 38 39 version(Windows) 40 { 41 string[] fastListDir(bool recursive = false, bool symlinks=false)(string pathname, string pattern = null) 42 { 43 import core.sys.windows.windows; 44 45 static if (recursive) 46 enforce(!pattern, "TODO: recursive fastListDir with pattern"); 47 48 string[] result; 49 string c; 50 HANDLE h; 51 52 c = buildPath(pathname, pattern ? pattern : "*.*"); 53 WIN32_FIND_DATAW fileinfo; 54 55 h = FindFirstFileW(toUTF16z(c), &fileinfo); 56 if (h != INVALID_HANDLE_VALUE) 57 { 58 scope(exit) FindClose(h); 59 60 do 61 { 62 // Skip "." and ".." 63 if (wcscmp(fileinfo.cFileName.ptr, ".") == 0 || 64 wcscmp(fileinfo.cFileName.ptr, "..") == 0) 65 continue; 66 67 static if (!symlinks) 68 { 69 if (fileinfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) 70 continue; 71 } 72 73 size_t clength = wcslen(fileinfo.cFileName.ptr); 74 string name = std.utf.toUTF8(fileinfo.cFileName[0 .. clength]); 75 string path = buildPath(pathname, name); 76 77 static if (recursive) 78 { 79 if (fileinfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 80 { 81 result ~= fastListDir!recursive(path); 82 continue; 83 } 84 } 85 86 result ~= path; 87 } while (FindNextFileW(h,&fileinfo) != FALSE); 88 } 89 return result; 90 } 91 } 92 else 93 version (Posix) 94 { 95 private import core.stdc.errno; 96 private import core.sys.posix.dirent; 97 private import core.stdc.string; 98 99 string[] fastListDir(bool recursive=false, bool symlinks=false)(string pathname, string pattern = null) 100 { 101 string[] result; 102 DIR* h; 103 dirent* fdata; 104 105 h = opendir(toStringz(pathname)); 106 if (h) 107 { 108 try 109 { 110 while((fdata = readdir(h)) != null) 111 { 112 // Skip "." and ".." 113 if (!core.stdc..string.strcmp(fdata.d_name.ptr, ".") || 114 !core.stdc..string.strcmp(fdata.d_name.ptr, "..")) 115 continue; 116 117 static if (!symlinks) 118 { 119 if (fdata.d_type == DT_LNK) 120 continue; 121 } 122 123 size_t len = core.stdc..string.strlen(fdata.d_name.ptr); 124 string name = fdata.d_name[0 .. len].idup; 125 if (pattern && !globMatch(name, pattern)) 126 continue; 127 string path = buildPath(pathname, name); 128 129 static if (recursive) 130 { 131 if (fdata.d_type & DT_DIR) 132 { 133 result ~= fastListDir!(recursive, symlinks)(path); 134 continue; 135 } 136 } 137 138 result ~= path; 139 } 140 } 141 finally 142 { 143 closedir(h); 144 } 145 } 146 else 147 { 148 throw new std.file.FileException(pathname, errno); 149 } 150 return result; 151 } 152 } 153 else 154 static assert(0, "TODO"); 155 156 // ************************************************************************ 157 158 string buildPath2(string[] segments...) { return segments.length ? buildPath(segments) : null; } 159 160 /// Shell-like expansion of ?, * and ** in path components 161 DirEntry[] fileList(string pattern) 162 { 163 auto components = cast(string[])array(pathSplitter(pattern)); 164 foreach (i, component; components[0..$-1]) 165 if (component.contains("?") || component.contains("*")) // TODO: escape? 166 { 167 DirEntry[] expansions; // TODO: filter range instead? 168 auto dir = buildPath2(components[0..i]); 169 if (component == "**") 170 expansions = array(dirEntries(dir, SpanMode.depth)); 171 else 172 expansions = array(dirEntries(dir, component, SpanMode.shallow)); 173 174 DirEntry[] result; 175 foreach (expansion; expansions) 176 if (expansion.isDir()) 177 result ~= fileList(buildPath(expansion.name ~ components[i+1..$])); 178 return result; 179 } 180 181 auto dir = buildPath2(components[0..$-1]); 182 if (!dir || exists(dir)) 183 return array(dirEntries(dir, components[$-1], SpanMode.shallow)); 184 else 185 return null; 186 } 187 188 /// ditto 189 DirEntry[] fileList(string pattern0, string[] patterns...) 190 { 191 DirEntry[] result; 192 foreach (pattern; [pattern0] ~ patterns) 193 result ~= fileList(pattern); 194 return result; 195 } 196 197 /// ditto 198 string[] fastFileList(string pattern) 199 { 200 auto components = cast(string[])array(pathSplitter(pattern)); 201 foreach (i, component; components[0..$-1]) 202 if (component.contains("?") || component.contains("*")) // TODO: escape? 203 { 204 string[] expansions; // TODO: filter range instead? 205 auto dir = buildPath2(components[0..i]); 206 if (component == "**") 207 expansions = fastListDir!true(dir); 208 else 209 expansions = fastListDir(dir, component); 210 211 string[] result; 212 foreach (expansion; expansions) 213 if (expansion.isDir()) 214 result ~= fastFileList(buildPath(expansion ~ components[i+1..$])); 215 return result; 216 } 217 218 auto dir = buildPath2(components[0..$-1]); 219 if (!dir || exists(dir)) 220 return fastListDir(dir, components[$-1]); 221 else 222 return null; 223 } 224 225 /// ditto 226 string[] fastFileList(string pattern0, string[] patterns...) 227 { 228 string[] result; 229 foreach (pattern; [pattern0] ~ patterns) 230 result ~= fastFileList(pattern); 231 return result; 232 } 233 234 // ************************************************************************ 235 236 import std.datetime; 237 import std.exception; 238 239 deprecated SysTime getMTime(string name) 240 { 241 return timeLastModified(name); 242 } 243 244 /// If target exists, update its modification time; 245 /// otherwise create it as an empty file. 246 void touch(in char[] target) 247 { 248 if (exists(target)) 249 { 250 auto now = Clock.currTime(); 251 setTimes(target, now, now); 252 } 253 else 254 std.file.write(target, ""); 255 } 256 257 /// Returns true if the target file doesn't exist, 258 /// or source is newer than the target. 259 bool newerThan(string source, string target) 260 { 261 if (!target.exists) 262 return true; 263 return source.timeLastModified() > target.timeLastModified(); 264 } 265 266 /// Returns true if the target file doesn't exist, 267 /// or any of the sources are newer than the target. 268 bool anyNewerThan(string[] sources, string target) 269 { 270 if (!target.exists) 271 return true; 272 auto targetTime = target.timeLastModified(); 273 return sources.any!(source => source.timeLastModified() > targetTime)(); 274 } 275 276 version (Posix) 277 { 278 import core.sys.posix.sys.stat; 279 import core.sys.posix.unistd; 280 281 int getOwner(string fn) 282 { 283 stat_t s; 284 errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn); 285 return s.st_uid; 286 } 287 288 int getGroup(string fn) 289 { 290 stat_t s; 291 errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn); 292 return s.st_gid; 293 } 294 295 void setOwner(string fn, int uid, int gid) 296 { 297 errnoEnforce(chown(toStringz(fn), uid, gid) == 0, "chown: " ~ fn); 298 } 299 } 300 301 /// Try to rename; copy/delete if rename fails 302 void move(string src, string dst) 303 { 304 try 305 src.rename(dst); 306 catch (Exception e) 307 { 308 atomicCopy(src, dst); 309 src.remove(); 310 } 311 } 312 313 /// Make sure that the given directory exists 314 /// (and create parent directories as necessary). 315 void ensureDirExists(string path) 316 { 317 if (!path.exists) 318 path.mkdirRecurse(); 319 } 320 321 /// Make sure that the path to the given file name 322 /// exists (and create directories as necessary). 323 void ensurePathExists(string fn) 324 { 325 fn.dirName.ensureDirExists(); 326 } 327 328 import ae.utils.text; 329 330 /// Forcibly remove a file or directory. 331 /// If atomic is true, the entire directory is deleted "atomically" 332 /// (it is first moved/renamed to another location). 333 /// On Windows, this will move the file/directory out of the way, 334 /// if it is in use and cannot be deleted (but can be renamed). 335 void forceDelete(Flag!"atomic" atomic=Yes.atomic)(string fn, Flag!"recursive" recursive = No.recursive) 336 { 337 import std.process : environment; 338 version(Windows) 339 { 340 mixin(importWin32!q{winnt}); 341 mixin(importWin32!q{winbase}); 342 } 343 344 auto name = fn.baseName(); 345 fn = fn.absolutePath().longPath(); 346 347 version(Windows) 348 { 349 auto fnW = toUTF16z(fn); 350 auto attr = GetFileAttributesW(fnW); 351 wenforce(attr != INVALID_FILE_ATTRIBUTES, "GetFileAttributes"); 352 if (attr & FILE_ATTRIBUTE_READONLY) 353 SetFileAttributesW(fnW, attr & ~FILE_ATTRIBUTE_READONLY).wenforce("SetFileAttributes"); 354 } 355 356 static if (atomic) 357 { 358 // To avoid zombifying locked directories, try renaming it first. 359 // Attempting to delete a locked directory will make it inaccessible. 360 361 bool tryMoveTo(string target) 362 { 363 target = target.longPath(); 364 if (target.endsWith(`\`)) 365 target = target[0..$-1]; 366 if (target.length && !target.exists) 367 return false; 368 369 string newfn; 370 do 371 newfn = format("%s\\deleted-%s.%s.%s", target, name, thisProcessID, randomString()); 372 while (newfn.exists); 373 374 version(Windows) 375 { 376 auto newfnW = toUTF16z(newfn); 377 if (!MoveFileW(fnW, newfnW)) 378 return false; 379 } 380 else 381 { 382 try 383 rename(fn, newfn); 384 catch (FileException e) 385 return false; 386 } 387 388 fn = newfn; 389 version(Windows) fnW = newfnW; 390 return true; 391 } 392 393 void tryMove() 394 { 395 auto tmp = environment.get("TEMP"); 396 if (tmp) 397 if (tryMoveTo(tmp)) 398 return; 399 400 version(Windows) 401 string tempDir = fn[0..7]~"Temp"; 402 else 403 enum tempDir = "/tmp"; 404 405 if (tryMoveTo(tempDir)) 406 return; 407 408 if (tryMoveTo(fn.dirName())) 409 return; 410 411 throw new Exception("Unable to delete " ~ fn ~ " atomically (all rename attempts failed)"); 412 } 413 414 tryMove(); 415 } 416 417 version(Windows) 418 { 419 if (attr & FILE_ATTRIBUTE_DIRECTORY) 420 { 421 if (recursive && (attr & FILE_ATTRIBUTE_REPARSE_POINT) == 0) 422 { 423 foreach (de; fn.dirEntries(SpanMode.shallow)) 424 forceDelete!(No.atomic)(de.name, Yes.recursive); 425 } 426 // Will fail if !recursive and directory is not empty 427 RemoveDirectoryW(fnW).wenforce("RemoveDirectory"); 428 } 429 else 430 DeleteFileW(fnW).wenforce("DeleteFile"); 431 } 432 else 433 { 434 if (recursive) 435 fn.removeRecurse(); 436 else 437 if (fn.isDir) 438 fn.rmdir(); 439 else 440 fn.remove(); 441 } 442 } 443 444 445 deprecated void forceDelete(bool atomic)(string fn, bool recursive = false) { forceDelete!(cast(Flag!"atomic")atomic)(fn, cast(Flag!"recursive")recursive); } 446 //deprecated void forceDelete()(string fn, bool recursive) { forceDelete!(Yes.atomic)(fn, cast(Flag!"recursive")recursive); } 447 448 deprecated unittest 449 { 450 mkdir("testdir"); touch("testdir/b"); forceDelete!(false )("testdir", true); 451 mkdir("testdir"); touch("testdir/b"); forceDelete!(true )("testdir", true); 452 } 453 454 unittest 455 { 456 mkdir("testdir"); touch("testdir/b"); forceDelete ("testdir", Yes.recursive); 457 mkdir("testdir"); touch("testdir/b"); forceDelete!(No .atomic)("testdir", Yes.recursive); 458 mkdir("testdir"); touch("testdir/b"); forceDelete!(Yes.atomic)("testdir", Yes.recursive); 459 } 460 461 /// If fn is a directory, delete it recursively. 462 /// Otherwise, delete the file or symlink fn. 463 void removeRecurse(string fn) 464 { 465 auto attr = fn.getAttributes(); 466 if (attr.attrIsSymlink) 467 { 468 version (Windows) 469 if (attr.attrIsDir) 470 fn.rmdir(); 471 else 472 fn.remove(); 473 else 474 fn.remove(); 475 } 476 else 477 if (attr.attrIsDir) 478 fn.rmdirRecurse(); 479 else 480 fn.remove(); 481 } 482 483 /// Create an empty directory, deleting 484 /// all its contents if it already exists. 485 void recreateEmptyDirectory()(string dir) 486 { 487 if (dir.exists) 488 dir.forceDelete(Yes.recursive); 489 mkdir(dir); 490 } 491 492 bool isHidden()(string fn) 493 { 494 if (baseName(fn).startsWith(".")) 495 return true; 496 version (Windows) 497 { 498 mixin(importWin32!q{winnt}); 499 if (getAttributes(fn) & FILE_ATTRIBUTE_HIDDEN) 500 return true; 501 } 502 return false; 503 } 504 505 /// Return a file's unique ID. 506 ulong getFileID()(string fn) 507 { 508 version (Windows) 509 { 510 mixin(importWin32!q{winnt}); 511 mixin(importWin32!q{winbase}); 512 513 auto fnW = toUTF16z(fn); 514 auto h = CreateFileW(fnW, FILE_READ_ATTRIBUTES, 0, null, OPEN_EXISTING, 0, HANDLE.init); 515 wenforce(h!=INVALID_HANDLE_VALUE, fn); 516 scope(exit) CloseHandle(h); 517 BY_HANDLE_FILE_INFORMATION fi; 518 GetFileInformationByHandle(h, &fi).wenforce("GetFileInformationByHandle"); 519 520 ULARGE_INTEGER li; 521 li.LowPart = fi.nFileIndexLow; 522 li.HighPart = fi.nFileIndexHigh; 523 auto result = li.QuadPart; 524 enforce(result, "Null file ID"); 525 return result; 526 } 527 else 528 { 529 return DirEntry(fn).statBuf.st_ino; 530 } 531 } 532 533 unittest 534 { 535 touch("a"); 536 scope(exit) remove("a"); 537 hardLink("a", "b"); 538 scope(exit) remove("b"); 539 touch("c"); 540 scope(exit) remove("c"); 541 assert(getFileID("a") == getFileID("b")); 542 assert(getFileID("a") != getFileID("c")); 543 } 544 545 deprecated alias std.file.getSize getSize2; 546 547 /// Using UNC paths bypasses path length limitation when using Windows wide APIs. 548 string longPath(string s) 549 { 550 version (Windows) 551 { 552 if (!s.startsWith(`\\`)) 553 return `\\?\` ~ s.absolutePath().buildNormalizedPath().replace(`/`, `\`); 554 } 555 return s; 556 } 557 558 version (Windows) 559 { 560 static if (__traits(compiles, { mixin importWin32!q{winnt}; })) 561 static mixin(importWin32!q{winnt}); 562 563 void createReparsePoint(string reparseBufferName, string extraInitialization, string reparseTagName)(in char[] target, in char[] print, in char[] link) 564 { 565 mixin(importWin32!q{winbase}); 566 mixin(importWin32!q{windef}); 567 mixin(importWin32!q{winioctl}); 568 569 enum SYMLINK_FLAG_RELATIVE = 1; 570 571 HANDLE hLink = CreateFileW(link.toUTF16z(), GENERIC_READ | GENERIC_WRITE, 0, null, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, null); 572 wenforce(hLink && hLink != INVALID_HANDLE_VALUE, "CreateFileW"); 573 scope(exit) CloseHandle(hLink); 574 575 enum pathOffset = 576 mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName) .offsetof + 577 mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName)._PathBuffer.offsetof; 578 579 auto targetW = target.toUTF16(); 580 auto printW = print .toUTF16(); 581 582 // Despite MSDN, two NUL-terminating characters are needed, one for each string. 583 584 auto pathBufferSize = targetW.length + 1 + printW.length + 1; // in chars 585 auto buf = new ubyte[pathOffset + pathBufferSize * WCHAR.sizeof]; 586 auto r = cast(REPARSE_DATA_BUFFER*)buf.ptr; 587 588 r.ReparseTag = mixin(reparseTagName); 589 r.ReparseDataLength = to!WORD(buf.length - mixin(q{r..} ~ reparseBufferName).offsetof); 590 591 auto pathBuffer = mixin(q{r..} ~ reparseBufferName).PathBuffer; 592 auto p = pathBuffer; 593 594 mixin(q{r..} ~ reparseBufferName).SubstituteNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof); 595 mixin(q{r..} ~ reparseBufferName).SubstituteNameLength = to!WORD(targetW.length * WCHAR.sizeof); 596 p[0..targetW.length] = targetW; 597 p += targetW.length; 598 *p++ = 0; 599 600 mixin(q{r..} ~ reparseBufferName).PrintNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof); 601 mixin(q{r..} ~ reparseBufferName).PrintNameLength = to!WORD(printW .length * WCHAR.sizeof); 602 p[0..printW.length] = printW; 603 p += printW.length; 604 *p++ = 0; 605 606 assert(p-pathBuffer == pathBufferSize); 607 608 mixin(extraInitialization); 609 610 DWORD dwRet; // Needed despite MSDN 611 DeviceIoControl(hLink, FSCTL_SET_REPARSE_POINT, buf.ptr, buf.length.to!DWORD(), null, 0, &dwRet, null).wenforce("DeviceIoControl"); 612 } 613 614 void acquirePrivilege(S)(S name) 615 { 616 mixin(importWin32!q{winbase}); 617 mixin(importWin32!q{windef}); 618 619 import ae.sys.windows; 620 621 HANDLE hToken = null; 622 wenforce(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)); 623 scope(exit) CloseHandle(hToken); 624 625 TOKEN_PRIVILEGES tp; 626 wenforce(LookupPrivilegeValue(null, name.toUTF16z(), &tp.Privileges[0].Luid), "LookupPrivilegeValue"); 627 628 tp.PrivilegeCount = 1; 629 tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 630 wenforce(AdjustTokenPrivileges(hToken, FALSE, &tp, cast(DWORD)TOKEN_PRIVILEGES.sizeof, null, null), "AdjustTokenPrivileges"); 631 } 632 633 /// Link a directory. 634 /// Uses symlinks on POSIX, and directory junctions on Windows. 635 void dirLink()(in char[] original, in char[] link) 636 { 637 mkdir(link); 638 scope(failure) rmdir(link); 639 640 auto target = `\??\` ~ original.idup.absolutePath(); 641 if (target[$-1] != '\\') 642 target ~= '\\'; 643 644 createReparsePoint!(q{MountPointReparseBuffer}, q{}, q{IO_REPARSE_TAG_MOUNT_POINT})(target, null, link); 645 } 646 647 void symlink()(in char[] original, in char[] link) 648 { 649 mixin(importWin32!q{winnt}); 650 651 acquirePrivilege(SE_CREATE_SYMBOLIC_LINK_NAME); 652 653 touch(link); 654 scope(failure) remove(link); 655 656 createReparsePoint!(q{SymbolicLinkReparseBuffer}, q{r.SymbolicLinkReparseBuffer.Flags = link.isAbsolute() ? 0 : SYMLINK_FLAG_RELATIVE;}, q{IO_REPARSE_TAG_SYMLINK})(original, original, link); 657 } 658 } 659 else 660 alias std.file.symlink dirLink; 661 662 version(Windows) version(unittest) static mixin(importWin32!q{winnt}); 663 664 unittest 665 { 666 mkdir("a"); scope(exit) rmdir("a"[]); 667 touch("a/f"); scope(exit) remove("a/f"); 668 dirLink("a", "b"); scope(exit) version(Windows) rmdir("b"); else remove("b"); 669 //symlink("a/f", "c"); scope(exit) remove("c"); 670 assert("b".isSymlink()); 671 //assert("c".isSymlink()); 672 assert("b/f".exists()); 673 } 674 675 version (Windows) 676 { 677 void hardLink()(string src, string dst) 678 { 679 mixin(importWin32!q{w32api}); 680 681 static assert(_WIN32_WINNT >= 0x501, "CreateHardLinkW not available for target Windows platform. Specify -version=WindowsXP"); 682 683 mixin(importWin32!q{winnt}); 684 mixin(importWin32!q{winbase}); 685 686 wenforce(CreateHardLinkW(toUTF16z(dst), toUTF16z(src), null), "CreateHardLink failed: " ~ src ~ " -> " ~ dst); 687 } 688 } 689 version (Posix) 690 { 691 void hardLink()(string src, string dst) 692 { 693 import core.sys.posix.unistd; 694 enforce(link(toUTFz!(const char*)(src), toUTFz!(const char*)(dst)) == 0, "link() failed: " ~ dst); 695 } 696 } 697 698 version (Posix) 699 { 700 string realPath(string path) 701 { 702 // TODO: Windows version 703 import core.sys.posix.stdlib; 704 auto p = realpath(toUTFz!(const char*)(path), null); 705 errnoEnforce(p, "realpath"); 706 string result = fromStringz(p).idup; 707 free(p); 708 return result; 709 } 710 } 711 712 // /proc/self/mounts parsing 713 version (linux) 714 { 715 struct MountInfo 716 { 717 string spec; /// device path 718 string file; /// mount path 719 string vfstype; /// file system 720 string mntops; /// options 721 int freq; /// dump flag 722 int passno; /// fsck order 723 } 724 725 string unescapeMountString(in char[] s) 726 { 727 string result; 728 729 size_t p = 0; 730 for (size_t i=0; i+3<s.length;) 731 { 732 auto c = s[i]; 733 if (c == '\\') 734 { 735 result ~= s[p..i]; 736 result ~= to!int(s[i+1..i+4], 8); 737 i += 4; 738 p = i; 739 } 740 else 741 i++; 742 } 743 result ~= s[p..$]; 744 return result; 745 } 746 747 unittest 748 { 749 assert(unescapeMountString(`a\040b\040c`) == "a b c"); 750 assert(unescapeMountString(`\040`) == " "); 751 } 752 753 MountInfo parseMountInfo(in char[] line) 754 { 755 const(char)[][6] parts; 756 copy(line.splitter(" "), parts[]); 757 return MountInfo( 758 unescapeMountString(parts[0]), 759 unescapeMountString(parts[1]), 760 unescapeMountString(parts[2]), 761 unescapeMountString(parts[3]), 762 parts[4].to!int, 763 parts[5].to!int, 764 ); 765 } 766 767 /// Returns an iterator of MountInfo structs. 768 auto getMounts() 769 { 770 return File("/proc/self/mounts", "rb").byLine().map!parseMountInfo(); 771 } 772 773 /// Get the name of the filesystem that the given path is mounted under. 774 string getPathFilesystem(string path) 775 { 776 path = realPath(path); 777 size_t bestLength; string bestFS; 778 foreach (ref info; getMounts()) 779 { 780 if (path.startsWith(info.file) && (path.length == info.file.length || path[info.file.length] == '/')) 781 { 782 if (bestLength < info.file.length) 783 { 784 bestLength = info.file.length; 785 bestFS = info.vfstype; 786 } 787 } 788 } 789 return bestFS; 790 } 791 } 792 793 // **************************************************************************** 794 795 version (linux) 796 { 797 import core.sys.linux.sys.xattr; 798 import core.stdc.errno; 799 alias ENOATTR = ENODATA; 800 801 /// AA-like object for accessing a file's extended attributes. 802 struct XAttrs(Obj, string funPrefix) 803 { 804 Obj obj; 805 806 mixin("alias getFun = " ~ funPrefix ~ "getxattr;"); 807 mixin("alias setFun = " ~ funPrefix ~ "setxattr;"); 808 mixin("alias removeFun = " ~ funPrefix ~ "removexattr;"); 809 mixin("alias listFun = " ~ funPrefix ~ "listxattr;"); 810 811 void[] opIndex(string key) 812 { 813 auto cKey = key.toStringz(); 814 auto size = getFun(obj, cKey, null, 0); 815 errnoEnforce(size >= 0); 816 auto result = new void[size]; 817 // TODO: race condition, retry 818 size = getFun(obj, cKey, result.ptr, result.length); 819 errnoEnforce(size == result.length); 820 return result; 821 } 822 823 bool opIn_r(string key) 824 { 825 auto cKey = key.toStringz(); 826 auto size = getFun(obj, cKey, null, 0); 827 if (size >= 0) 828 return true; 829 else 830 if (errno == ENOATTR) 831 return false; 832 else 833 errnoEnforce(false); 834 assert(false); 835 } 836 837 void opIndexAssign(in void[] value, string key) 838 { 839 auto ret = setFun(obj, key.toStringz(), value.ptr, value.length, 0); 840 errnoEnforce(ret == 0); 841 } 842 843 void remove(string key) 844 { 845 auto ret = removeFun(obj, key.toStringz()); 846 errnoEnforce(ret == 0); 847 } 848 849 string[] keys() 850 { 851 auto size = listFun(obj, null, 0); 852 errnoEnforce(size >= 0); 853 auto buf = new char[size]; 854 // TODO: race condition, retry 855 size = listFun(obj, buf.ptr, buf.length); 856 errnoEnforce(size == buf.length); 857 858 char[][] result; 859 size_t start; 860 foreach (p, c; buf) 861 if (!c) 862 { 863 result ~= buf[start..p]; 864 start = p+1; 865 } 866 867 return cast(string[])result; 868 } 869 } 870 871 auto xAttrs(string path) 872 { 873 return XAttrs!(const(char)*, "")(path.toStringz()); 874 } 875 876 auto linkXAttrs(string path) 877 { 878 return XAttrs!(const(char)*, "l")(path.toStringz()); 879 } 880 881 auto xAttrs(in ref File f) 882 { 883 return XAttrs!(int, "f")(f.fileno); 884 } 885 886 unittest 887 { 888 enum fn = "test.txt"; 889 std.file.write(fn, "test"); 890 scope(exit) remove(fn); 891 892 auto attrs = xAttrs(fn); 893 enum key = "user.foo"; 894 assert(key !in attrs); 895 assert(attrs.keys == []); 896 897 attrs[key] = "bar"; 898 assert(key in attrs); 899 assert(attrs[key] == "bar"); 900 assert(attrs.keys == [key]); 901 902 attrs.remove(key); 903 assert(key !in attrs); 904 assert(attrs.keys == []); 905 } 906 } 907 908 // **************************************************************************** 909 910 version (Windows) 911 { 912 /// Enumerate all hard links to the specified file. 913 // TODO: Return a range 914 string[] enumerateHardLinks()(string fn) 915 { 916 mixin(importWin32!q{winnt}); 917 mixin(importWin32!q{winbase}); 918 919 alias extern(System) HANDLE function(LPCWSTR lpFileName, DWORD dwFlags, LPDWORD StringLength, PWCHAR LinkName) TFindFirstFileNameW; 920 alias extern(System) BOOL function(HANDLE hFindStream, LPDWORD StringLength, PWCHAR LinkName) TFindNextFileNameW; 921 922 auto kernel32 = GetModuleHandle("kernel32.dll"); 923 auto FindFirstFileNameW = cast(TFindFirstFileNameW)GetProcAddress(kernel32, "FindFirstFileNameW").wenforce("GetProcAddress(FindFirstFileNameW)"); 924 auto FindNextFileNameW = cast(TFindNextFileNameW)GetProcAddress(kernel32, "FindNextFileNameW").wenforce("GetProcAddress(FindNextFileNameW)"); 925 926 static WCHAR[0x8000] buf; 927 DWORD len = buf.length; 928 auto h = FindFirstFileNameW(toUTF16z(fn), 0, &len, buf.ptr); 929 wenforce(h != INVALID_HANDLE_VALUE, "FindFirstFileNameW"); 930 scope(exit) FindClose(h); 931 932 string[] result; 933 do 934 { 935 enforce(len > 0 && len < buf.length && buf[len-1] == 0, "Bad FindFirst/NextFileNameW result"); 936 result ~= buf[0..len-1].toUTF8(); 937 len = buf.length; 938 auto ok = FindNextFileNameW(h, &len, buf.ptr); 939 if (!ok && GetLastError() == ERROR_HANDLE_EOF) 940 break; 941 wenforce(ok, "FindNextFileNameW"); 942 } while(true); 943 return result; 944 } 945 } 946 947 uint hardLinkCount(string fn) 948 { 949 version (Windows) 950 { 951 // TODO: Optimize (don't transform strings) 952 return cast(uint)fn.enumerateHardLinks.length; 953 } 954 else 955 { 956 import core.sys.posix.sys.stat; 957 958 stat_t s; 959 errnoEnforce(stat(fn.toStringz(), &s) == 0, "stat"); 960 return s.st_nlink.to!uint; 961 } 962 } 963 964 unittest 965 { 966 touch("a.test"); 967 scope(exit) remove("a.test"); 968 assert("a.test".hardLinkCount() == 1); 969 970 hardLink("a.test", "b.test"); 971 scope(exit) remove("b.test"); 972 assert("a.test".hardLinkCount() == 2); 973 assert("b.test".hardLinkCount() == 2); 974 975 version(Windows) 976 { 977 auto paths = enumerateHardLinks("a.test"); 978 assert(paths.length == 2); 979 paths.sort(); 980 assert(paths[0].endsWith(`\a.test`), paths[0]); 981 assert(paths[1].endsWith(`\b.test`)); 982 } 983 } 984 985 void toFile(in void[] data, in char[] name) 986 { 987 std.file.write(name, data); 988 } 989 990 /// Uses UNC paths to open a file. 991 /// Requires https://github.com/D-Programming-Language/phobos/pull/1888 992 File openFile()(string fn, string mode = "rb") 993 { 994 File f; 995 static if (is(typeof(&f.windowsHandleOpen))) 996 { 997 import core.sys.windows.windows; 998 import ae.sys.windows.exception; 999 1000 string winMode; 1001 foreach (c; mode) 1002 switch (c) 1003 { 1004 case 'r': 1005 case 'w': 1006 case 'a': 1007 case '+': 1008 winMode ~= c; 1009 break; 1010 case 'b': 1011 case 't': 1012 break; 1013 default: 1014 assert(false, "Unknown character in mode"); 1015 } 1016 DWORD access, creation; 1017 bool append; 1018 switch (winMode) 1019 { 1020 case "r" : access = GENERIC_READ ; creation = OPEN_EXISTING; break; 1021 case "r+": access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_EXISTING; break; 1022 case "w" : access = GENERIC_WRITE; creation = OPEN_ALWAYS ; break; 1023 case "w+": access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_ALWAYS ; break; 1024 case "a" : access = GENERIC_WRITE; creation = OPEN_ALWAYS ; append = true; break; 1025 case "a+": assert(false, "Not implemented"); // requires two file pointers 1026 default: assert(false, "Bad file mode: " ~ mode); 1027 } 1028 1029 auto pathW = toUTF16z(longPath(fn)); 1030 auto h = CreateFileW(pathW, access, FILE_SHARE_READ, null, creation, 0, HANDLE.init); 1031 wenforce(h != INVALID_HANDLE_VALUE); 1032 1033 if (append) 1034 h.SetFilePointer(0, null, FILE_END); 1035 1036 f.windowsHandleOpen(h, mode); 1037 } 1038 else 1039 f.open(fn, mode); 1040 return f; 1041 } 1042 1043 auto fileDigest(Digest)(string fn) 1044 { 1045 import std.range.primitives; 1046 Digest context; 1047 context.start(); 1048 put(context, openFile(fn, "rb").byChunk(64 * 1024)); 1049 auto digest = context.finish(); 1050 return digest; 1051 } 1052 1053 template mdFile() 1054 { 1055 import std.digest.md; 1056 alias mdFile = fileDigest!MD5; 1057 } 1058 1059 version (HAVE_WIN32) 1060 unittest 1061 { 1062 import std.digest.digest : toHexString; 1063 write("test.txt", "Hello, world!"); 1064 scope(exit) remove("test.txt"); 1065 assert(mdFile("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 1066 } 1067 1068 auto fileDigestCached(Digest)(string fn) 1069 { 1070 static typeof(Digest.init.finish())[ulong] cache; 1071 auto id = getFileID(fn); 1072 auto phash = id in cache; 1073 if (phash) 1074 return *phash; 1075 return cache[id] = fileDigest!Digest(fn); 1076 } 1077 1078 template mdFileCached() 1079 { 1080 import std.digest.md; 1081 alias mdFileCached = fileDigestCached!MD5; 1082 } 1083 1084 version (HAVE_WIN32) 1085 unittest 1086 { 1087 import std.digest.digest : toHexString; 1088 write("test.txt", "Hello, world!"); 1089 scope(exit) remove("test.txt"); 1090 assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 1091 write("test.txt", "Something else"); 1092 assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839"); 1093 } 1094 1095 /// Read a File (which might be a stream) into an array 1096 void[] readFile(File f) 1097 { 1098 import std.range.primitives; 1099 auto result = appender!(ubyte[]); 1100 put(result, f.byChunk(64*1024)); 1101 return result.data; 1102 } 1103 1104 unittest 1105 { 1106 auto s = "0123456789".replicate(10_000); 1107 write("test.txt", s); 1108 scope(exit) remove("test.txt"); 1109 assert(readFile(File("test.txt")) == s); 1110 } 1111 1112 /// Like std.file.readText for non-UTF8 1113 ascii readAscii()(string fileName) 1114 { 1115 return cast(ascii)readFile(openFile(fileName, "rb")); 1116 } 1117 1118 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 1119 version(Posix) static import ae.sys.signals; 1120 1121 /// Start a thread which writes data to f asynchronously. 1122 Thread writeFileAsync(File f, in void[] data) 1123 { 1124 static class Writer : Thread 1125 { 1126 File target; 1127 const void[] data; 1128 1129 this(ref File f, in void[] data) 1130 { 1131 this.target = f; 1132 this.data = data; 1133 super(&run); 1134 } 1135 1136 void run() 1137 { 1138 version (Posix) 1139 { 1140 import ae.sys.signals; 1141 collectSignal(SIGPIPE, &write); 1142 } 1143 else 1144 write(); 1145 } 1146 1147 void write() 1148 { 1149 target.rawWrite(data); 1150 target.close(); 1151 } 1152 } 1153 1154 auto t = new Writer(f, data); 1155 t.start(); 1156 return t; 1157 } 1158 1159 /// Write data to a file, and ensure it gets written to disk 1160 /// before this function returns. 1161 /// Consider using as atomic!syncWrite. 1162 /// See also: syncUpdate 1163 void syncWrite()(string target, in void[] data) 1164 { 1165 auto f = File(target, "wb"); 1166 f.rawWrite(data); 1167 version (Windows) 1168 { 1169 mixin(importWin32!q{windows}); 1170 FlushFileBuffers(f.windowsHandle); 1171 } 1172 else 1173 { 1174 import core.sys.posix.unistd; 1175 fsync(f.fileno); 1176 } 1177 f.close(); 1178 } 1179 1180 /// Atomically save data to a file (if the file doesn't exist, 1181 /// or its contents differs). The update operation as a whole 1182 /// is not atomic, only the write is. 1183 void syncUpdate()(string fn, in void[] data) 1184 { 1185 if (!fn.exists || fn.read() != data) 1186 atomic!(syncWrite!())(fn, data); 1187 } 1188 1189 version(Windows) import ae.sys.windows.exception; 1190 1191 struct NamedPipeImpl 1192 { 1193 immutable string fileName; 1194 1195 /// Create a named pipe, and reserve a filename. 1196 this()(string name) 1197 { 1198 version(Windows) 1199 { 1200 mixin(importWin32!q{winbase}); 1201 1202 fileName = `\\.\pipe\` ~ name; 1203 auto h = CreateNamedPipeW(fileName.toUTF16z, PIPE_ACCESS_OUTBOUND, PIPE_TYPE_BYTE, 10, 4096, 4096, 0, null).wenforce("CreateNamedPipeW"); 1204 f.windowsHandleOpen(h, "wb"); 1205 } 1206 else 1207 { 1208 import core.sys.posix.sys.stat; 1209 1210 fileName = `/tmp/` ~ name ~ `.fifo`; 1211 mkfifo(fileName.toStringz, S_IWUSR | S_IRUSR); 1212 } 1213 } 1214 1215 /// Wait for a peer to open the other end of the pipe. 1216 File connect()() 1217 { 1218 version(Windows) 1219 { 1220 mixin(importWin32!q{winbase}); 1221 mixin(importWin32!q{windef}); 1222 1223 BOOL bSuccess = ConnectNamedPipe(f.windowsHandle, null); 1224 1225 // "If a client connects before the function is called, the function returns zero 1226 // and GetLastError returns ERROR_PIPE_CONNECTED. This can happen if a client 1227 // connects in the interval between the call to CreateNamedPipe and the call to 1228 // ConnectNamedPipe. In this situation, there is a good connection between client 1229 // and server, even though the function returns zero." 1230 if (!bSuccess) 1231 wenforce(GetLastError() == ERROR_PIPE_CONNECTED, "ConnectNamedPipe"); 1232 1233 return f; 1234 } 1235 else 1236 { 1237 return File(fileName, "w"); 1238 } 1239 } 1240 1241 ~this() 1242 { 1243 version(Windows) 1244 { 1245 // File.~this will take care of cleanup 1246 } 1247 else 1248 fileName.remove(); 1249 } 1250 1251 private: 1252 File f; 1253 } 1254 alias NamedPipe = RefCounted!NamedPipeImpl; 1255 1256 import ae.utils.textout : StringBuilder; 1257 1258 /// Avoid std.stdio.File.readln's memory corruption bug 1259 /// https://issues.dlang.org/show_bug.cgi?id=13856 1260 string safeReadln(File f) 1261 { 1262 StringBuilder buf; 1263 char[1] arr; 1264 while (true) 1265 { 1266 auto result = f.rawRead(arr[]); 1267 if (!result.length) 1268 break; 1269 buf.put(result); 1270 if (result[0] == '\x0A') 1271 break; 1272 } 1273 return buf.get(); 1274 } 1275 1276 // **************************************************************************** 1277 1278 /// Change the current directory to the given directory. Does nothing if dir is null. 1279 /// Return a scope guard which, upon destruction, restores the previous directory. 1280 /// Asserts that only one thread has changed the process's current directory at any time. 1281 auto pushd(string dir) 1282 { 1283 import core.atomic; 1284 1285 static int threadCount = 0; 1286 static shared int processCount = 0; 1287 1288 static struct Popd 1289 { 1290 string oldPath; 1291 this(string cwd) { oldPath = cwd; } 1292 ~this() { if (oldPath) pop(); } 1293 @disable this(); 1294 @disable this(this); 1295 1296 void pop() 1297 { 1298 assert(oldPath); 1299 scope(exit) oldPath = null; 1300 chdir(oldPath); 1301 1302 auto newThreadCount = --threadCount; 1303 auto newProcessCount = atomicOp!"-="(processCount, 1); 1304 assert(newThreadCount == newProcessCount); // Shouldn't happen 1305 } 1306 } 1307 1308 string cwd; 1309 if (dir) 1310 { 1311 auto newThreadCount = ++threadCount; 1312 auto newProcessCount = atomicOp!"+="(processCount, 1); 1313 assert(newThreadCount == newProcessCount, "Another thread already has an active pushd"); 1314 1315 cwd = getcwd(); 1316 chdir(dir); 1317 } 1318 return Popd(cwd); 1319 } 1320 1321 // **************************************************************************** 1322 1323 import std.algorithm; 1324 import std.process : thisProcessID; 1325 import std.traits; 1326 import std.typetuple; 1327 import ae.utils.meta; 1328 1329 enum targetParameterNames = "target/to/name/dst"; 1330 1331 /// Wrap an operation which creates a file or directory, 1332 /// so that it is created safely and, for files, atomically 1333 /// (by performing the underlying operation to a temporary 1334 /// location, then renaming the completed file/directory to 1335 /// the actual target location). targetName specifies the name 1336 /// of the parameter containing the target file/directory. 1337 auto atomic(alias impl, string targetName = targetParameterNames)(staticMap!(Unqual, ParameterTypeTuple!impl) args) 1338 { 1339 enum targetIndex = findParameter([ParameterIdentifierTuple!impl], targetName, __traits(identifier, impl)); 1340 return atomic!(impl, targetIndex)(args); 1341 } 1342 1343 /// ditto 1344 auto atomic(alias impl, size_t targetIndex)(staticMap!(Unqual, ParameterTypeTuple!impl) args) 1345 { 1346 // idup for https://d.puremagic.com/issues/show_bug.cgi?id=12503 1347 auto target = args[targetIndex].idup; 1348 auto temp = "%s.%s.%s.temp".format(target, thisProcessID, getCurrentThreadID); 1349 if (temp.exists) temp.removeRecurse(); 1350 scope(success) rename(temp, target); 1351 scope(failure) if (temp.exists) temp.removeRecurse(); 1352 args[targetIndex] = temp; 1353 return impl(args); 1354 } 1355 1356 /// ditto 1357 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12230 1358 // Can't be an overload because of https://issues.dlang.org/show_bug.cgi?id=13374 1359 //R atomicDg(string targetName = "target", R, Args...)(R delegate(Args) impl, staticMap!(Unqual, Args) args) 1360 auto atomicDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args) 1361 { 1362 enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA; 1363 return atomic!(impl, targetIndex)(args); 1364 } 1365 1366 deprecated alias safeUpdate = atomic; 1367 1368 unittest 1369 { 1370 enum fn = "atomic.tmp"; 1371 scope(exit) if (fn.exists) fn.remove(); 1372 1373 atomic!touch(fn); 1374 assert(fn.exists); 1375 fn.remove(); 1376 1377 atomicDg(&touch, fn); 1378 assert(fn.exists); 1379 } 1380 1381 /// Wrap an operation so that it is skipped entirely 1382 /// if the target already exists. Implies atomic. 1383 auto cached(alias impl, string targetName = targetParameterNames)(ParameterTypeTuple!impl args) 1384 { 1385 enum targetIndex = findParameter([ParameterIdentifierTuple!impl], targetName, __traits(identifier, impl)); 1386 auto target = args[targetIndex]; 1387 if (!target.exists) 1388 atomic!(impl, targetIndex)(args); 1389 return target; 1390 } 1391 1392 /// ditto 1393 // Exists due to the same reasons as atomicDg 1394 auto cachedDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args) 1395 { 1396 enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA; 1397 auto target = args[targetIndex]; 1398 if (!target.exists) 1399 atomic!(impl, targetIndex)(args); 1400 return target; 1401 } 1402 1403 deprecated alias obtainUsing = cached; 1404 1405 /// Create a file, or replace an existing file's contents 1406 /// atomically. 1407 /// Note: Consider using atomic!syncWrite or 1408 /// atomic!syncUpdate instead. 1409 alias atomic!writeProxy atomicWrite; 1410 deprecated alias safeWrite = atomicWrite; 1411 void writeProxy(string target, in void[] data) 1412 { 1413 std.file.write(target, data); 1414 } 1415 1416 // Work around for https://github.com/D-Programming-Language/phobos/pull/2784#issuecomment-68117241 1417 private void copy2(string source, string target) { std.file.copy(source, target); } 1418 1419 /// Copy a file, or replace an existing file's contents 1420 /// with another file's, atomically. 1421 alias atomic!copy2 atomicCopy; 1422 1423 unittest 1424 { 1425 enum fn = "cached.tmp"; 1426 scope(exit) if (fn.exists) fn.remove(); 1427 1428 cached!touch(fn); 1429 assert(fn.exists); 1430 1431 std.file.write(fn, "test"); 1432 1433 cachedDg!0(&writeProxy, fn, "test2"); 1434 assert(fn.readText() == "test"); 1435 } 1436 1437 // **************************************************************************** 1438 1439 template withTarget(alias targetGen, alias fun) 1440 { 1441 auto withTarget(Args...)(auto ref Args args) 1442 { 1443 auto target = targetGen(args); 1444 fun(args, target); 1445 return target; 1446 } 1447 } 1448 1449 /// Two-argument buildPath with reversed arguments. 1450 /// Useful for UFCS chaining. 1451 string prependPath(string target, string path) 1452 { 1453 return buildPath(path, target); 1454 }