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 }