1 /// ThePath - easy way to work with paths and files 2 module thepath; 3 4 static public import std.file: SpanMode; 5 static private import std.path; 6 static private import std.file; 7 static private import std.stdio; 8 private import std.path: expandTilde; 9 private import std.format: format; 10 private import std.exception: enforce; 11 12 13 /** Create temporary directory 14 * Note, that caller is responsible to remove created directory. 15 * The temp directory will be created inside specified path. 16 * 17 * Params: 18 * path = path to already existing directory to create 19 * temp directory inside. Default: std.file.tempDir 20 * prefix = prefix to be used in name of temp directory. Default: "tmp" 21 * Returns: string, representing path to created temporary directory 22 * Throws: PathException in case of error 23 **/ 24 string createTempDirectory(in string prefix="tmp") { 25 import std.file : tempDir; 26 return createTempDirectory(tempDir, prefix); 27 } 28 29 /// ditto 30 string createTempDirectory(in string path, in string prefix) { 31 version(Posix) { 32 import std.string : fromStringz; 33 import std.conv: to; 34 import core.sys.posix.stdlib : mkdtemp; 35 36 // Prepare template for mkdtemp function. 37 // It have to be mutable array of chars ended with zero to be compatibale 38 // with mkdtemp function. 39 scope char[] tempname_str = std.path.buildNormalizedPath( 40 std.path.expandTilde(path), 41 prefix ~ "-XXXXXX").dup ~ "\0"; 42 43 // mkdtemp will modify tempname_str directly. and res is pointer to 44 // tempname_str in case of success. 45 char* res = mkdtemp(tempname_str.ptr); 46 enforce!PathException( 47 res !is null, "Cannot create temporary directory"); 48 49 // Converting to string will duplicate result. 50 // But may be it have sense to do it in more obvious way 51 // for example: return tempname_str[0..$-1].idup; 52 return to!string(res.fromStringz); 53 } else { 54 import std.ascii: letters; 55 import std.random: uniform; 56 57 // Generate new random temp path to test using provided path and prefix 58 // as template. 59 string generate_temp_dir() { 60 string suffix = "-"; 61 for(ubyte i; i<6; i++) suffix ~= letters[uniform(0, $)]; 62 return std.path.buildNormalizedPath( 63 std.path.expandTilde(path), prefix ~ suffix); 64 } 65 66 string temp_dir = generate_temp_dir(); 67 while (std.file.exists(temp_dir)) { 68 temp_dir = generate_temp_dir(); 69 } 70 std.file.mkdir(temp_dir); 71 return temp_dir; 72 } 73 } 74 75 76 /** Create temporary directory 77 * Note, that caller is responsible to remove created directory. 78 * The temp directory will be created inside specified path. 79 * 80 * Params: 81 * path = path to already existing directory to create 82 * temp directory inside. Default: std.file.tempDir 83 * prefix = prefix to be used in name of temp directory. Default: "tmp" 84 * Returns: Path to created temp directory 85 * Throws: PathException in case of error 86 **/ 87 Path createTempPath(in string prefix="tmp") { 88 return Path(createTempDirectory(prefix)); 89 } 90 91 /// ditto 92 Path createTempPath(in string path, in string prefix) { 93 return Path(createTempDirectory(path, prefix)); 94 } 95 96 /// ditto 97 Path createTempPath(in Path path, in string prefix) { 98 return createTempPath(path.toString, prefix); 99 } 100 101 102 /// PathException - will be raise on failure on path (or file) operations 103 class PathException : Exception { 104 105 /// Main constructor 106 this(string msg, string file = __FILE__, size_t line = __LINE__) { 107 super(msg, file, line); 108 } 109 } 110 111 112 /** Main struct to work with paths. 113 **/ 114 struct Path { 115 // TODO: Deside if we need to make _path by default configured to current directory or to allow path to be null 116 private string _path="."; 117 118 /** Main constructor to build new Path from string 119 * Params: 120 * path = string representation of path to point to 121 **/ 122 this(in string path) { 123 _path = path; 124 } 125 126 /** Constructor that allows to build path from segments 127 * Params: 128 * segments = array of segments to build path from 129 **/ 130 this(in string[] segments...) { 131 _path = std.path.buildNormalizedPath(segments); 132 } 133 134 /// 135 unittest { 136 import dshould; 137 138 version(Posix) { 139 Path("foo", "moo", "boo").toString.should.equal("foo/moo/boo"); 140 Path("/foo/moo", "boo").toString.should.equal("/foo/moo/boo"); 141 } 142 } 143 144 /** Check if path is valid. 145 * Returns: true if this is valid path. 146 **/ 147 bool isValid() const { 148 return std.path.isValidPath(_path); 149 } 150 151 /// Check if path is absolute 152 bool isAbsolute() const { 153 return std.path.isAbsolute(_path); 154 } 155 156 /// Check if path starts at root directory (or drive letter) 157 bool isRooted() const { 158 return std.path.isRooted(_path); 159 } 160 161 /// Determine if path is file. 162 bool isFile() const { 163 return std.file.isFile(_path.expandTilde); 164 } 165 166 /// Determine if path is directory. 167 bool isDir() const { 168 return std.file.isDir(_path.expandTilde); 169 } 170 171 /// Determine if path is symlink 172 bool isSymlink() const { 173 return std.file.isSymlink(_path.expandTilde); 174 } 175 176 /// Check if path exists 177 bool exists() const { 178 return std.file.exists(_path.expandTilde); 179 } 180 181 /// 182 unittest { 183 import dshould; 184 185 version(Posix) { 186 import std.algorithm: startsWith; 187 // Prepare test dir in user's home directory 188 Path home_tmp = createTempPath("~", "tmp-d-test"); 189 scope(exit) home_tmp.remove(); 190 Path home_rel = Path("~").join(home_tmp.baseName); 191 home_rel.toString.startsWith("~/tmp-d-test").should.be(true); 192 193 home_rel.join("test-dir").exists.should.be(false); 194 home_rel.join("test-dir").mkdir; 195 home_rel.join("test-dir").exists.should.be(true); 196 197 home_rel.join("test-file").exists.should.be(false); 198 home_rel.join("test-file").writeFile("test"); 199 home_rel.join("test-file").exists.should.be(true); 200 } 201 } 202 203 /// Return path as string 204 string toString() const { 205 return _path; 206 } 207 208 209 /** Convert path to absolute path. 210 * Returns: new instance of Path that represents current path converted to 211 * absolute path. 212 * Also, this method will automatically do tilde expansion and 213 * normalization of path. 214 **/ 215 Path toAbsolute() const { 216 return Path( 217 std.path.buildNormalizedPath( 218 std.path.absolutePath(_path.expandTilde))); 219 } 220 221 /// 222 unittest { 223 import dshould; 224 225 version(Posix) { 226 auto cdir = std.file.getcwd; 227 scope(exit) std.file.chdir(cdir); 228 std.file.chdir("/tmp"); 229 230 Path("foo/moo").toAbsolute.toString.should.equal("/tmp/foo/moo"); 231 Path("../my-path").toAbsolute.toString.should.equal("/my-path"); 232 Path("/a/path").toAbsolute.toString.should.equal("/a/path"); 233 234 string home_path = "~".expandTilde; 235 home_path[0].should.equal('/'); 236 237 Path("~/my/path").toAbsolute.toString.should.equal("%s/my/path".format(home_path)); 238 } 239 } 240 241 /** Expand tilde (~) in current path. 242 * Returns: New path with tilde expaded 243 **/ 244 Path expandTilde() const { 245 return Path(std.path.expandTilde(_path)); 246 } 247 248 /** Normalize path. 249 * Returns: new normalized Path. 250 **/ 251 Path normalize() const { 252 import std.array : array; 253 import std.exception : assumeUnique; 254 auto result = std.path.asNormalizedPath(_path); 255 return Path(assumeUnique(result.array)); 256 } 257 258 /// 259 unittest { 260 import dshould; 261 262 version(Posix) { 263 Path("foo").normalize.toString.should.equal("foo"); 264 Path("../foo/../moo").normalize.toString.should.equal("../moo"); 265 Path("/foo/./moo/../bar").normalize.toString.should.equal("/foo/bar"); 266 } 267 } 268 269 /** Join multiple path segments and return single path. 270 * Params: 271 * segments = Array of strings (or Path) to build new path.. 272 * Returns: 273 * New path build from current path and provided segments 274 **/ 275 Path join(in string[] segments...) const { 276 string[] args=[cast(string)_path]; 277 foreach(s; segments) args ~= s; 278 return Path(std.path.buildPath(args)); 279 } 280 281 /// ditto 282 Path join(in Path[] segments...) const { 283 string[] args=[]; 284 foreach(p; segments) args ~= p.toString(); 285 return this.join(args); 286 } 287 288 /// 289 unittest { 290 import dshould; 291 string tmp_dir = createTempDirectory(); 292 scope(exit) std.file.rmdirRecurse(tmp_dir); 293 294 auto ps = std.path.dirSeparator; 295 296 Path("tmp").join("test1", "subdir", "2").toString.should.equal( 297 "tmp" ~ ps ~ "test1" ~ ps ~ "subdir" ~ ps ~ "2"); 298 299 Path root = Path(tmp_dir); 300 root._path.should.equal(tmp_dir); 301 auto test_c_file = root.join("test-create.txt"); 302 test_c_file._path.should.equal(tmp_dir ~ ps ~"test-create.txt"); 303 test_c_file.isAbsolute.should.be(true); 304 305 version(Posix) { 306 Path("/").join("test2", "test3").toString.should.equal("/test2/test3"); 307 } 308 309 } 310 311 312 /** determine parent path of this path 313 * Returns: 314 * Absolute Path to parent directory. 315 **/ 316 Path parent() const { 317 if (isAbsolute()) { 318 return Path(std.path.dirName(_path)); 319 } else { 320 return this.toAbsolute.parent; 321 } 322 } 323 324 /// 325 unittest { 326 import dshould; 327 version(Posix) { 328 Path("/tmp").parent.toString.should.equal("/"); 329 Path("/").parent.toString.should.equal("/"); 330 Path("/tmp/parent/child").parent.toString.should.equal("/tmp/parent"); 331 332 Path("parent/child").parent.toString.should.equal( 333 Path(std.file.getcwd).join("parent").toString); 334 335 auto cdir = std.file.getcwd; 336 scope(exit) std.file.chdir(cdir); 337 338 std.file.chdir("/tmp"); 339 340 Path("parent/child").parent.toString.should.equal("/tmp/parent"); 341 342 Path("~/test-dir").parent.toString.should.equal( 343 "~".expandTilde); 344 } 345 } 346 347 /** Return this path as relative to base 348 * Params: 349 * base = base path to make this path relative to. Must be absolute. 350 * Returns: 351 * new Path that is relative to base but represent same location 352 * as this path. 353 * Throws: 354 * PathException if base path is not valid or not absolute 355 **/ 356 Path relativeTo(in Path base) const { 357 enforce!PathException( 358 base.isValid && base.isAbsolute, 359 "Base path must be valid and absolute"); 360 return Path(std.path.relativePath(_path, base._path)); 361 } 362 363 /// ditto 364 Path relativeTo(in string base) const { 365 return relativeTo(Path(base)); 366 } 367 368 /// 369 unittest { 370 import dshould; 371 Path("foo").relativeTo(std.file.getcwd).toString().should.equal("foo"); 372 373 version(Posix) { 374 auto path1 = Path("/foo/root/child/subchild"); 375 auto root1 = Path("/foo/root"); 376 auto root2 = Path("/moo/root"); 377 auto rpath1 = path1.relativeTo(root1); 378 379 rpath1.toString.should.equal("child/subchild"); 380 root2.join(rpath1).toString.should.equal("/moo/root/child/subchild"); 381 path1.relativeTo(root2).toString.should.equal("../../foo/root/child/subchild"); 382 383 // Base path must be absolute, so this should throw error 384 Path("~/my/path/1").relativeTo("~/my").should.throwA!PathException; 385 } 386 } 387 388 /// Returns extension for current path 389 string extension() const { 390 return std.path.extension(_path); 391 } 392 393 /// Returns base name of current path 394 string baseName() const { 395 return std.path.baseName(_path); 396 } 397 398 /// 399 unittest { 400 import dshould; 401 Path("foo").baseName.should.equal("foo"); 402 Path("foo", "moo").baseName.should.equal("moo"); 403 Path("foo", "moo", "test.txt").baseName.should.equal("test.txt"); 404 } 405 406 /// Return size of file specified by path 407 ulong getSize() const { 408 return std.file.getSize(_path.expandTilde); 409 } 410 411 /// 412 unittest { 413 import dshould; 414 Path root = createTempPath(); 415 scope(exit) root.remove(); 416 417 ubyte[4] data = [1, 2, 3, 4]; 418 root.join("test-file.txt").writeFile(data); 419 root.join("test-file.txt").getSize.should.equal(4); 420 421 version(Posix) { 422 // Prepare test dir in user's home directory 423 Path home_tmp = createTempPath("~", "tmp-d-test"); 424 scope(exit) home_tmp.remove(); 425 string tmp_dir_name = home_tmp.baseName; 426 427 Path("~/%s/test-file.txt".format(tmp_dir_name)).writeFile(data); 428 Path("~/%s/test-file.txt".format(tmp_dir_name)).getSize.should.equal(4); 429 } 430 } 431 432 /** Resolve link and return real path. 433 * Available only for posix systems. 434 * If path is not symlink, then return it unchanged 435 **/ 436 version(Posix) Path readLink() const { 437 if (isSymlink()) { 438 return Path(std.file.readLink(_path.expandTilde)); 439 } else { 440 return this; 441 } 442 } 443 444 /** Iterate over all files and directories inside path; 445 * 446 * Params: 447 * mode = The way to traverse directories. See [docs](https://dlang.org/phobos/std_file.html#SpanMode) 448 * followSymlink = do we need to follow symlinks of not. By default set to True. 449 * 450 * Examples: 451 * --- 452 * // Iterate over paths in current directory 453 * foreach (Path p; Path(".").walk(SpanMode.breadth)) { 454 * if (p.isFile) writeln(p); 455 * --- 456 **/ 457 auto walk(SpanMode mode=SpanMode.shallow, bool followSymlink=true) const { 458 import std.algorithm.iteration: map; 459 return std.file.dirEntries( 460 _path, mode, followSymlink).map!(a => Path(a)); 461 462 } 463 464 /// Change current working directory to this. 465 void chdir() const { 466 std.file.chdir(_path.expandTilde); 467 } 468 469 /// 470 unittest { 471 import dshould; 472 auto cdir = std.file.getcwd; 473 Path root = createTempPath(); 474 scope(exit) { 475 std.file.chdir(cdir); 476 root.remove(); 477 } 478 479 std.file.getcwd.should.not.equal(root._path); 480 root.chdir; 481 std.file.getcwd.should.equal(root._path); 482 483 version(Posix) { 484 // Prepare test dir in user's home directory 485 Path home_tmp = createTempPath("~", "tmp-d-test"); 486 scope(exit) home_tmp.remove(); 487 string tmp_dir_name = home_tmp.baseName; 488 std.file.getcwd.should.not.equal(home_tmp._path); 489 490 // Change current working directory to tmp-dir-name 491 Path("~", tmp_dir_name).chdir; 492 std.file.getcwd.should.equal(home_tmp._path); 493 } 494 } 495 496 /** Copy single file to destination. 497 * If destination does not exists, 498 * then file will be copied exactly to that path. 499 * If destination already exists and it is directory, then method will 500 * try to copy file inside that directory with same name. 501 * If destination already exists and it is file, 502 * then depending on `rewrite` param file will be owerwritten or 503 * PathException will be thrown. 504 * Params: 505 * dest = destination path to copy file to. Could be new file path, 506 * or directory where to copy file. 507 * rewrite = do we need to rewrite file if it already exists? 508 * Throws: 509 * PathException if source file does not exists or 510 * if destination already exists and 511 * it is not a directory and rewrite is set to false. 512 **/ 513 void copyFileTo(in Path dest, in bool rewrite=false) const { 514 enforce!PathException( 515 this.exists, 516 "Cannot Copy! Source file %s does not exists!".format(_path)); 517 if (dest.exists) { 518 if (dest.isDir) { 519 this.copyFileTo(dest.join(this.baseName), rewrite); 520 } else if (!rewrite) { 521 throw new PathException( 522 "Cannot copy! Destination file %s already exists!".format(dest._path)); 523 } else { 524 std.file.copy(_path, dest._path); 525 } 526 } else { 527 std.file.copy(_path, dest._path); 528 } 529 } 530 531 /// 532 unittest { 533 import dshould; 534 535 // Prepare temporary path for test 536 auto cdir = std.file.getcwd; 537 Path root = createTempPath(); 538 scope(exit) { 539 std.file.chdir(cdir); 540 root.remove(); 541 } 542 543 // Create test directory structure 544 root.join("test-file.txt").writeFile("test"); 545 root.join("test-file-2.txt").writeFile("test-2"); 546 root.join("test-dst-dir").mkdir; 547 548 // Test copy file by path 549 root.join("test-dst-dir", "test1.txt").exists.should.be(false); 550 root.join("test-file.txt").copyFileTo(root.join("test-dst-dir", "test1.txt")); 551 root.join("test-dst-dir", "test1.txt").exists.should.be(true); 552 553 // Test copy file by path with rewrite 554 root.join("test-dst-dir", "test1.txt").readFile.should.equal("test"); 555 root.join("test-file-2.txt").copyFileTo(root.join("test-dst-dir", "test1.txt")).should.throwA!PathException; 556 root.join("test-file-2.txt").copyFileTo(root.join("test-dst-dir", "test1.txt"), true); 557 root.join("test-dst-dir", "test1.txt").readFile.should.equal("test-2"); 558 559 // Test copy file inside dir 560 root.join("test-dst-dir", "test-file.txt").exists.should.be(false); 561 root.join("test-file.txt").copyFileTo(root.join("test-dst-dir")); 562 root.join("test-dst-dir", "test-file.txt").exists.should.be(true); 563 564 // Test copy file inside dir with rewrite 565 root.join("test-file.txt").writeFile("test-42"); 566 root.join("test-dst-dir", "test-file.txt").readFile.should.equal("test"); 567 root.join("test-file.txt").copyFileTo(root.join("test-dst-dir")).should.throwA!PathException; 568 root.join("test-file.txt").copyFileTo(root.join("test-dst-dir"), true); 569 root.join("test-dst-dir", "test-file.txt").readFile.should.equal("test-42"); 570 } 571 572 /** Copy file or directory to destination 573 * If source is a file, then copyFileTo will be use to copy it. 574 * If source is a directory, then more complex logic will be applied: 575 * - if dest already exists and it is not dir, then exception will be raised. 576 * - if dest already exists and it is dir, then source dir will be copied inseide that dir with it's name 577 * - if dest does not exists, then current directory will be copied to dest path. 578 * 579 * Note, that work with symlinks have to be improved. Not tested yet. 580 * 581 * Params: 582 * dest = destination path to copy content of this. 583 * Throws: 584 * PathException when cannot copy 585 **/ 586 void copyTo(in Path dest) const { 587 import std.stdio; 588 if (isDir) { 589 Path dst_root = dest.toAbsolute; 590 if (dst_root.exists) { 591 enforce!PathException( 592 dst_root.isDir, 593 "Cannot copy! Destination %s already exists and it is not directory!".format(dst_root)); 594 dst_root = dst_root.join(this.baseName); 595 enforce!PathException( 596 !dst_root.exists, 597 "Cannot copy! Destination %s already exists!".format(dst_root)); 598 } 599 std.file.mkdirRecurse(dst_root._path); 600 auto src_root = this.toAbsolute(); 601 foreach (Path src; src_root.walk(SpanMode.breadth)) { 602 auto dst = dst_root.join(src.relativeTo(src_root)); 603 if (src.isFile) { 604 std.file.copy(src._path, dst._path); 605 } else if (src.isSymlink) { 606 // TODO: Posix only 607 if (src.readLink.exists) { 608 std.file.copy( 609 std.file.readLink(src._path), 610 dst._path, 611 ); 612 //} else { 613 // Log info about broken symlink 614 } 615 } else { 616 std.file.mkdirRecurse(dst._path); 617 } 618 } 619 } else { 620 copyFileTo(dest); 621 } 622 } 623 624 /// ditto 625 void copyTo(in string dest) const { 626 copyTo(Path(dest)); 627 } 628 629 /// 630 unittest { 631 import dshould; 632 auto cdir = std.file.getcwd; 633 Path root = createTempPath(); 634 scope(exit) { 635 std.file.chdir(cdir); 636 root.remove(); 637 } 638 639 auto test_c_file = root.join("test-create.txt"); 640 641 // Create test file to copy 642 test_c_file.exists.should.be(false); 643 test_c_file.writeFile("Hello World"); 644 test_c_file.exists.should.be(true); 645 646 // Test copy file when dest dir does not exists 647 test_c_file.copyTo( 648 root.join("test-copy-dst", "test.txt") 649 ).should.throwA!(std.file.FileException); 650 651 // Test copy file where dest dir exists and dest name specified 652 root.join("test-copy-dst").exists().should.be(false); 653 root.join("test-copy-dst").mkdir(); 654 root.join("test-copy-dst").exists().should.be(true); 655 root.join("test-copy-dst", "test.txt").exists.should.be(false); 656 test_c_file.copyTo(root.join("test-copy-dst", "test.txt")); 657 root.join("test-copy-dst", "test.txt").exists.should.be(true); 658 659 // Try to copy file when it is already exists in dest folder 660 test_c_file.copyTo( 661 root.join("test-copy-dst", "test.txt") 662 ).should.throwA!PathException; 663 664 // Try to copy file, when only dirname specified 665 root.join("test-copy-dst", "test-create.txt").exists.should.be(false); 666 test_c_file.copyTo(root.join("test-copy-dst")); 667 root.join("test-copy-dst", "test-create.txt").exists.should.be(true); 668 669 // Try to copy empty directory with its content 670 root.join("test-copy-dir-empty").mkdir; 671 root.join("test-copy-dir-empty").exists.should.be(true); 672 root.join("test-copy-dir-empty-cpy").exists.should.be(false); 673 root.join("test-copy-dir-empty").copyTo( 674 root.join("test-copy-dir-empty-cpy")); 675 root.join("test-copy-dir-empty").exists.should.be(true); 676 root.join("test-copy-dir-empty-cpy").exists.should.be(true); 677 678 // Create test dir with content to test copying non-empty directory 679 root.join("test-dir").mkdir(); 680 root.join("test-dir", "f1.txt").writeFile("f1"); 681 root.join("test-dir", "d2").mkdir(); 682 root.join("test-dir", "d2", "f2.txt").writeFile("f2"); 683 684 // Test that test-dir content created 685 root.join("test-dir").exists.should.be(true); 686 root.join("test-dir").isDir.should.be(true); 687 root.join("test-dir", "f1.txt").exists.should.be(true); 688 root.join("test-dir", "f1.txt").isFile.should.be(true); 689 root.join("test-dir", "d2").exists.should.be(true); 690 root.join("test-dir", "d2").isDir.should.be(true); 691 root.join("test-dir", "d2", "f2.txt").exists.should.be(true); 692 root.join("test-dir", "d2", "f2.txt").isFile.should.be(true); 693 694 // Copy non-empty dir to unexisting location 695 root.join("test-dir-cpy-1").exists.should.be(false); 696 root.join("test-dir").copyTo(root.join("test-dir-cpy-1")); 697 698 // Test that dir copied successfully 699 root.join("test-dir-cpy-1").exists.should.be(true); 700 root.join("test-dir-cpy-1").isDir.should.be(true); 701 root.join("test-dir-cpy-1", "f1.txt").exists.should.be(true); 702 root.join("test-dir-cpy-1", "f1.txt").isFile.should.be(true); 703 root.join("test-dir-cpy-1", "d2").exists.should.be(true); 704 root.join("test-dir-cpy-1", "d2").isDir.should.be(true); 705 root.join("test-dir-cpy-1", "d2", "f2.txt").exists.should.be(true); 706 root.join("test-dir-cpy-1", "d2", "f2.txt").isFile.should.be(true); 707 708 // Copy non-empty dir to existing location 709 root.join("test-dir-cpy-2").exists.should.be(false); 710 root.join("test-dir-cpy-2").mkdir; 711 root.join("test-dir-cpy-2").exists.should.be(true); 712 713 // Copy directory to already existing dir 714 root.join("test-dir").copyTo(root.join("test-dir-cpy-2")); 715 716 // Test that dir copied successfully 717 root.join("test-dir-cpy-2", "test-dir").exists.should.be(true); 718 root.join("test-dir-cpy-2", "test-dir").isDir.should.be(true); 719 root.join("test-dir-cpy-2", "test-dir", "f1.txt").exists.should.be(true); 720 root.join("test-dir-cpy-2", "test-dir", "f1.txt").isFile.should.be(true); 721 root.join("test-dir-cpy-2", "test-dir", "d2").exists.should.be(true); 722 root.join("test-dir-cpy-2", "test-dir", "d2").isDir.should.be(true); 723 root.join("test-dir-cpy-2", "test-dir", "d2", "f2.txt").exists.should.be(true); 724 root.join("test-dir-cpy-2", "test-dir", "d2", "f2.txt").isFile.should.be(true); 725 726 // Try again to copy non-empty dir to already existing dir 727 // where dir with same base name already exists 728 root.join("test-dir").copyTo(root.join("test-dir-cpy-2")).should.throwA!PathException; 729 730 731 // Change dir to our temp directory and test copying using 732 // relative paths 733 root.chdir; 734 735 // Copy content using relative paths 736 root.join("test-dir-cpy-3").exists.should.be(false); 737 Path("test-dir-cpy-3").exists.should.be(false); 738 Path("test-dir").copyTo("test-dir-cpy-3"); 739 740 // Test that content was copied in right way 741 root.join("test-dir-cpy-3").exists.should.be(true); 742 root.join("test-dir-cpy-3").isDir.should.be(true); 743 root.join("test-dir-cpy-3", "f1.txt").exists.should.be(true); 744 root.join("test-dir-cpy-3", "f1.txt").isFile.should.be(true); 745 root.join("test-dir-cpy-3", "d2").exists.should.be(true); 746 root.join("test-dir-cpy-3", "d2").isDir.should.be(true); 747 root.join("test-dir-cpy-3", "d2", "f2.txt").exists.should.be(true); 748 root.join("test-dir-cpy-3", "d2", "f2.txt").isFile.should.be(true); 749 750 // Try to copy to already existing file 751 root.join("test-dir-cpy-4").writeFile("Test"); 752 753 // Expect error 754 root.join("test-dir").copyTo("test-dir-cpy-4").should.throwA!PathException; 755 756 version(Posix) { 757 // Prepare test dir in user's home directory 758 Path home_tmp = createTempPath("~", "tmp-d-test"); 759 scope(exit) home_tmp.remove(); 760 761 // Test if home_tmp created in right way and ensure that 762 // dest for copy dir does not exists 763 home_tmp.parent.toString.should.equal(std.path.expandTilde("~")); 764 home_tmp.isAbsolute.should.be(true); 765 home_tmp.join("test-dir").exists.should.be(false); 766 767 // Copy test-dir to home_tmp 768 import std.algorithm: startsWith; 769 auto home_tmp_rel = home_tmp.baseName; 770 string home_tmp_tilde = "~/%s".format(home_tmp_rel); 771 home_tmp_tilde.startsWith("~/tmp-d-test").should.be(true); 772 root.join("test-dir").copyTo(home_tmp_tilde); 773 774 // Test that content was copied in right way 775 home_tmp.join("test-dir").exists.should.be(true); 776 home_tmp.join("test-dir").isDir.should.be(true); 777 home_tmp.join("test-dir", "f1.txt").exists.should.be(true); 778 home_tmp.join("test-dir", "f1.txt").isFile.should.be(true); 779 home_tmp.join("test-dir", "d2").exists.should.be(true); 780 home_tmp.join("test-dir", "d2").isDir.should.be(true); 781 home_tmp.join("test-dir", "d2", "f2.txt").exists.should.be(true); 782 home_tmp.join("test-dir", "d2", "f2.txt").isFile.should.be(true); 783 } 784 785 786 787 } 788 789 /** Remove file or directory referenced by this path. 790 * This operation is recursive, so if path references to a direcotry, 791 * then directory itself and all content inside referenced dir will be 792 * removed 793 **/ 794 void remove() const { 795 if (isFile) std.file.remove(_path.expandTilde); 796 else std.file.rmdirRecurse(_path.expandTilde); 797 } 798 799 /// 800 unittest { 801 import dshould; 802 Path root = createTempPath(); 803 scope(exit) root.remove(); 804 805 // Try to remove unexisting file 806 root.join("unexising-file.txt").remove.should.throwA!(std.file.FileException); 807 808 // Try to remove file 809 root.join("test-file.txt").exists.should.be(false); 810 root.join("test-file.txt").writeFile("test"); 811 root.join("test-file.txt").exists.should.be(true); 812 root.join("test-file.txt").remove(); 813 root.join("test-file.txt").exists.should.be(false); 814 815 // Create test dir with contents 816 root.join("test-dir").mkdir(); 817 root.join("test-dir", "f1.txt").writeFile("f1"); 818 root.join("test-dir", "d2").mkdir(); 819 root.join("test-dir", "d2", "f2.txt").writeFile("f2"); 820 821 // Ensure test dir with contents created 822 root.join("test-dir").exists.should.be(true); 823 root.join("test-dir").isDir.should.be(true); 824 root.join("test-dir", "f1.txt").exists.should.be(true); 825 root.join("test-dir", "f1.txt").isFile.should.be(true); 826 root.join("test-dir", "d2").exists.should.be(true); 827 root.join("test-dir", "d2").isDir.should.be(true); 828 root.join("test-dir", "d2", "f2.txt").exists.should.be(true); 829 root.join("test-dir", "d2", "f2.txt").isFile.should.be(true); 830 831 // Remove test directory 832 root.join("test-dir").remove(); 833 834 // Ensure directory was removed 835 root.join("test-dir").exists.should.be(false); 836 root.join("test-dir", "f1.txt").exists.should.be(false); 837 root.join("test-dir", "d2").exists.should.be(false); 838 root.join("test-dir", "d2", "f2.txt").exists.should.be(false); 839 840 841 version(Posix) { 842 // Prepare test dir in user's home directory 843 Path home_tmp = createTempPath("~", "tmp-d-test"); 844 scope(exit) home_tmp.remove(); 845 846 // Create test dir with contents 847 home_tmp.join("test-dir").mkdir(); 848 home_tmp.join("test-dir", "f1.txt").writeFile("f1"); 849 home_tmp.join("test-dir", "d2").mkdir(); 850 home_tmp.join("test-dir", "d2", "f2.txt").writeFile("f2"); 851 852 // Remove created directory 853 Path("~").join(home_tmp.baseName).toAbsolute.toString.should.equal(home_tmp.toString); 854 Path("~").join(home_tmp.baseName, "test-dir").remove(); 855 856 // Ensure directory was removed 857 home_tmp.join("test-dir").exists.should.be(false); 858 home_tmp.join("test-dir", "f1.txt").exists.should.be(false); 859 home_tmp.join("test-dir", "d2").exists.should.be(false); 860 home_tmp.join("test-dir", "d2", "f2.txt").exists.should.be(false); 861 } 862 } 863 864 /** Rename current path. 865 * 866 * Note: case of moving file/dir between filesystesm is not tested. 867 * 868 * Throws: 869 * PathException when destination already exists 870 **/ 871 void rename(in Path to) const { 872 // TODO: Add support to move files between filesystems 873 enforce!PathException( 874 !to.exists, 875 "Destination %s already exists!".format(to)); 876 return std.file.rename(_path.expandTilde, to._path.expandTilde); 877 } 878 879 /// ditto 880 void rename(in string to) const { 881 return rename(Path(to)); 882 } 883 884 /// 885 unittest { 886 import dshould; 887 Path root = createTempPath(); 888 scope(exit) root.remove(); 889 890 // Create file 891 root.join("test-file.txt").exists.should.be(false); 892 root.join("test-file-new.txt").exists.should.be(false); 893 root.join("test-file.txt").writeFile("test"); 894 root.join("test-file.txt").exists.should.be(true); 895 root.join("test-file-new.txt").exists.should.be(false); 896 897 // Rename file 898 root.join("test-file.txt").exists.should.be(true); 899 root.join("test-file-new.txt").exists.should.be(false); 900 root.join("test-file.txt").rename(root.join("test-file-new.txt")); 901 root.join("test-file.txt").exists.should.be(false); 902 root.join("test-file-new.txt").exists.should.be(true); 903 904 // Try to move file to existing directory 905 root.join("my-dir").mkdir; 906 root.join("test-file-new.txt").rename(root.join("my-dir")).should.throwA!PathException; 907 908 // Try to rename one olready existing dir to another 909 root.join("other-dir").mkdir; 910 root.join("my-dir").exists.should.be(true); 911 root.join("other-dir").exists.should.be(true); 912 root.join("my-dir").rename(root.join("other-dir")).should.throwA!PathException; 913 914 // Create test dir with contents 915 root.join("test-dir").mkdir(); 916 root.join("test-dir", "f1.txt").writeFile("f1"); 917 root.join("test-dir", "d2").mkdir(); 918 root.join("test-dir", "d2", "f2.txt").writeFile("f2"); 919 920 // Ensure test dir with contents created 921 root.join("test-dir").exists.should.be(true); 922 root.join("test-dir").isDir.should.be(true); 923 root.join("test-dir", "f1.txt").exists.should.be(true); 924 root.join("test-dir", "f1.txt").isFile.should.be(true); 925 root.join("test-dir", "d2").exists.should.be(true); 926 root.join("test-dir", "d2").isDir.should.be(true); 927 root.join("test-dir", "d2", "f2.txt").exists.should.be(true); 928 root.join("test-dir", "d2", "f2.txt").isFile.should.be(true); 929 930 // Try to rename directory 931 root.join("test-dir").rename(root.join("test-dir-new")); 932 933 // Ensure old dir does not exists anymore 934 root.join("test-dir").exists.should.be(false); 935 root.join("test-dir", "f1.txt").exists.should.be(false); 936 root.join("test-dir", "d2").exists.should.be(false); 937 root.join("test-dir", "d2", "f2.txt").exists.should.be(false); 938 939 // Ensure test dir was renamed successfully 940 root.join("test-dir-new").exists.should.be(true); 941 root.join("test-dir-new").isDir.should.be(true); 942 root.join("test-dir-new", "f1.txt").exists.should.be(true); 943 root.join("test-dir-new", "f1.txt").isFile.should.be(true); 944 root.join("test-dir-new", "d2").exists.should.be(true); 945 root.join("test-dir-new", "d2").isDir.should.be(true); 946 root.join("test-dir-new", "d2", "f2.txt").exists.should.be(true); 947 root.join("test-dir-new", "d2", "f2.txt").isFile.should.be(true); 948 949 950 version(Posix) { 951 // Prepare test dir in user's home directory 952 Path home_tmp = createTempPath("~", "tmp-d-test"); 953 scope(exit) home_tmp.remove(); 954 955 // Ensure that there is no test dir in our home/based temp dir; 956 home_tmp.join("test-dir").exists.should.be(false); 957 home_tmp.join("test-dir", "f1.txt").exists.should.be(false); 958 home_tmp.join("test-dir", "d2").exists.should.be(false); 959 home_tmp.join("test-dir", "d2", "f2.txt").exists.should.be(false); 960 961 root.join("test-dir-new").rename( 962 Path("~").join(home_tmp.baseName, "test-dir")); 963 964 // Ensure test dir was renamed successfully 965 home_tmp.join("test-dir").exists.should.be(true); 966 home_tmp.join("test-dir").isDir.should.be(true); 967 home_tmp.join("test-dir", "f1.txt").exists.should.be(true); 968 home_tmp.join("test-dir", "f1.txt").isFile.should.be(true); 969 home_tmp.join("test-dir", "d2").exists.should.be(true); 970 home_tmp.join("test-dir", "d2").isDir.should.be(true); 971 home_tmp.join("test-dir", "d2", "f2.txt").exists.should.be(true); 972 home_tmp.join("test-dir", "d2", "f2.txt").isFile.should.be(true); 973 } 974 } 975 976 /** Create directory by this path 977 * Params: 978 * recursive = if set to true, then 979 * parent directories will be created if not exist 980 * Throws: 981 * FileException if cannot create dir (it already exists) 982 **/ 983 void mkdir(in bool recursive=false) const { 984 if (recursive) std.file.mkdirRecurse(std.path.expandTilde(_path)); 985 else std.file.mkdir(std.path.expandTilde(_path)); 986 } 987 988 /// 989 unittest { 990 import dshould; 991 Path root = createTempPath(); 992 scope(exit) root.remove(); 993 994 root.join("test-dir").exists.should.be(false); 995 root.join("test-dir", "subdir").exists.should.be(false); 996 997 root.join("test-dir", "subdir").mkdir().should.throwA!(std.file.FileException); 998 999 root.join("test-dir").mkdir(); 1000 root.join("test-dir").exists.should.be(true); 1001 root.join("test-dir", "subdir").exists.should.be(false); 1002 1003 root.join("test-dir", "subdir").mkdir(); 1004 1005 root.join("test-dir").exists.should.be(true); 1006 root.join("test-dir", "subdir").exists.should.be(true); 1007 } 1008 1009 /// 1010 unittest { 1011 import dshould; 1012 Path root = createTempPath(); 1013 scope(exit) root.remove(); 1014 1015 root.join("test-dir").exists.should.be(false); 1016 root.join("test-dir", "subdir").exists.should.be(false); 1017 1018 root.join("test-dir", "subdir").mkdir(true); 1019 1020 root.join("test-dir").exists.should.be(true); 1021 root.join("test-dir", "subdir").exists.should.be(true); 1022 } 1023 1024 /** Open file and return `std.stdio.File` struct with opened file 1025 * Params: 1026 * openMode = string representing open mode with 1027 * same semantic as in C standard lib 1028 * $(HTTP cplusplus.com/reference/clibrary/cstdio/fopen.html, fopen) function. 1029 * Returns: 1030 * std.stdio.File struct 1031 **/ 1032 std.stdio.File openFile(in string openMode = "rb") const { 1033 static import std.stdio; 1034 1035 return std.stdio.File(_path.expandTilde, openMode); 1036 } 1037 1038 /// 1039 unittest { 1040 import dshould; 1041 Path root = createTempPath(); 1042 scope(exit) root.remove(); 1043 1044 auto test_file = root.join("test-create.txt").openFile("wt"); 1045 scope(exit) test_file.close(); 1046 test_file.write("Test1"); 1047 test_file.flush(); 1048 root.join("test-create.txt").readFile().should.equal("Test1"); 1049 test_file.write("12"); 1050 test_file.flush(); 1051 root.join("test-create.txt").readFile().should.equal("Test112"); 1052 } 1053 1054 /** Write data to file as is 1055 * Params: 1056 * buffer = untypes array to write to file. 1057 * Throws: 1058 * FileException in case of error 1059 **/ 1060 void writeFile(in void[] buffer) const { 1061 return std.file.write(_path.expandTilde, buffer); 1062 } 1063 1064 /// 1065 unittest { 1066 import dshould; 1067 Path root = createTempPath(); 1068 scope(exit) root.remove(); 1069 1070 root.join("test-write-1.txt").exists.should.be(false); 1071 root.join("test-write-1.txt").writeFile("Hello world"); 1072 root.join("test-write-1.txt").exists.should.be(true); 1073 root.join("test-write-1.txt").readFile.should.equal("Hello world"); 1074 1075 ubyte[] data = [1, 7, 13, 5, 9]; 1076 root.join("test-write-2.txt").exists.should.be(false); 1077 root.join("test-write-2.txt").writeFile(data); 1078 root.join("test-write-2.txt").exists.should.be(true); 1079 ubyte[] rdata = cast(ubyte[])root.join("test-write-2.txt").readFile; 1080 rdata.length.should.equal(5); 1081 rdata[0].should.equal(1); 1082 rdata[1].should.equal(7); 1083 rdata[2].should.equal(13); 1084 rdata[3].should.equal(5); 1085 rdata[4].should.equal(9); 1086 } 1087 1088 /** Append data to file as is 1089 * Params: 1090 * buffer = untypes array to write to file. 1091 * Throws: 1092 * FileException in case of error 1093 **/ 1094 void appendFile(in void[] buffer) const { 1095 return std.file.append(_path.expandTilde, buffer); 1096 } 1097 1098 /// 1099 unittest { 1100 import dshould; 1101 Path root = createTempPath(); 1102 scope(exit) root.remove(); 1103 1104 ubyte[] data = [1, 7, 13, 5, 9]; 1105 ubyte[] data2 = [8, 17]; 1106 root.join("test-write-2.txt").exists.should.be(false); 1107 root.join("test-write-2.txt").writeFile(data); 1108 root.join("test-write-2.txt").appendFile(data2); 1109 root.join("test-write-2.txt").exists.should.be(true); 1110 ubyte[] rdata = cast(ubyte[])root.join("test-write-2.txt").readFile; 1111 rdata.length.should.equal(7); 1112 rdata[0].should.equal(1); 1113 rdata[1].should.equal(7); 1114 rdata[2].should.equal(13); 1115 rdata[3].should.equal(5); 1116 rdata[4].should.equal(9); 1117 rdata[5].should.equal(8); 1118 rdata[6].should.equal(17); 1119 } 1120 1121 1122 /** Read entire contents of file `name` and returns it as an untyped 1123 * array. If the file size is larger than `upTo`, only `upTo` 1124 * bytes are _read. 1125 * Params: 1126 * upTo = if present, the maximum number of bytes to _read 1127 * Returns: 1128 * Untyped array of bytes _read 1129 * Throws: 1130 * FileException in case of error 1131 **/ 1132 auto readFile(size_t upTo=size_t.max) const { 1133 return std.file.read(_path.expandTilde, upTo); 1134 } 1135 1136 unittest { 1137 import dshould; 1138 Path root = createTempPath(); 1139 scope(exit) root.remove(); 1140 1141 root.join("test-create.txt").exists.should.be(false); 1142 1143 // Test file read/write/apppend 1144 root.join("test-create.txt").writeFile("Hello World"); 1145 root.join("test-create.txt").exists.should.be(true); 1146 root.join("test-create.txt").readFile.should.equal("Hello World"); 1147 root.join("test-create.txt").appendFile("!"); 1148 root.join("test-create.txt").readFile.should.equal("Hello World!"); 1149 1150 // Try to remove file 1151 root.join("test-create.txt").exists.should.be(true); 1152 root.join("test-create.txt").remove(); 1153 root.join("test-create.txt").exists.should.be(false); 1154 1155 // Try to read data as bytes 1156 ubyte[] data = [1, 7, 13, 5, 9]; 1157 root.join("test-write-2.txt").exists.should.be(false); 1158 root.join("test-write-2.txt").writeFile(data); 1159 root.join("test-write-2.txt").exists.should.be(true); 1160 ubyte[] rdata = cast(ubyte[])root.join("test-write-2.txt").readFile; 1161 rdata.length.should.equal(5); 1162 rdata[0].should.equal(1); 1163 rdata[1].should.equal(7); 1164 rdata[2].should.equal(13); 1165 rdata[3].should.equal(5); 1166 rdata[4].should.equal(9); 1167 } 1168 1169 // TODO: Add readFileText method 1170 // TODO: to add: 1171 // - match pattern 1172 // - Handle symlinks 1173 // - Add readFileText 1174 }