1 /// Utility functions to work with paths
2 module thepath.utils;
3 
4 private import std.exception: enforce, errnoEnforce;
5 private static import std.path;
6 
7 private import thepath.exception: PathException;
8 private import thepath.path: Path;
9 
10 
11 // Max attempts to create temp directory
12 private immutable ushort MAX_TMP_ATTEMPTS = 1000;
13 
14 
15 /** Create temporary directory
16   * Note, that caller is responsible to remove created directory.
17   * The temp directory will be created inside specified path.
18   * 
19   * Params:
20   *     path = path to already existing directory to create
21   *         temp directory inside. Default: std.file.tempDir
22   *     prefix = prefix to be used in name of temp directory. Default: "tmp"
23   * Returns: string, representing path to created temporary directory
24   * Throws:
25   *     ErrnoException (Posix) incase if mkdtemp was not able to create tempdir
26   *     PathException (Windows) in case of failure of creation of temp dir
27   **/
28 @safe string createTempDirectory(in string prefix="tmp") {
29     import std.file : tempDir;
30     return createTempDirectory(tempDir, prefix);
31 }
32 
33 /// ditto
34 @safe string createTempDirectory(in string path, in string prefix) {
35     version(Posix) {
36         import std.string : fromStringz;
37         import std.conv: to;
38         import core.sys.posix.stdlib : mkdtemp;
39 
40         // Make trusted version of mkdtemp
41         char* t_mkdtemp(scope char* tmpl) @trusted nothrow => mkdtemp(tmpl);
42 
43         // Prepare template for mkdtemp function.
44         // It have to be mutable array of chars ended with zero to be compatibale
45         // with mkdtemp function.
46         scope char[] tempname_str = std.path.buildNormalizedPath(
47             std.path.expandTilde(path),
48             prefix ~ "-XXXXXX").dup ~ '\0';
49 
50         // mkdtemp will modify tempname_str directly.
51         // and res will be pointer to tempname_str in case of success.
52         // in case of failure, res will be set to null.
53         char* res = t_mkdtemp(&tempname_str[0]);
54         errnoEnforce(res !is null, "Cannot create temporary directory");
55 
56         // Convert tempname to string.
57         // Just remove trailing \0 symbol, and duplicate.
58         return tempname_str[0 .. $-1].idup;
59     } else version (Windows) {
60         import std.ascii: letters;
61         import std.random: uniform;
62         import std.file;
63         import core.sys.windows.winerror;
64         import std.windows.syserror;
65         import std.format: format;
66 
67         // Generate new random temp path to test using provided path and prefix
68         // as template.
69         string generate_temp_dir() {
70             string suffix = "-";
71             for(ubyte i; i<6; i++) suffix ~= letters[uniform(0, $)];
72             return std.path.buildNormalizedPath(
73                 std.path.expandTilde(path), prefix ~ suffix);
74         }
75 
76         // Make trusted funcs to get windows error code and msg
77         auto get_err_code(WindowsException e) @trusted {
78             return e.code;
79         }
80         string get_err_str(WindowsException e) @trusted {
81             return sysErrorString(e.code);
82         }
83 
84         // Try to create new temp directory
85         for(ushort i=0; i<MAX_TMP_ATTEMPTS; i++) {
86             string temp_dir = generate_temp_dir();
87             try {
88                 std.file.mkdir(temp_dir);
89             } catch (WindowsException e) {
90                 if (get_err_code(e) == ERROR_ALREADY_EXISTS)
91                     continue;
92                 throw new PathException(
93                     "Cannot create temporary directory: %s".format(
94                         get_err_str(e)));
95             }
96             return temp_dir;
97         }
98         throw new PathException(
99             "Cannot create temporary directory: No usable name found!");
100     } else assert(0, "Not supported platform!");
101 }
102 
103 version(Posix) @system unittest {
104     import dshould;
105     import std.exception;
106     createTempDirectory("/some/unexisting/path").should.throwA!ErrnoException;
107 }
108 
109 version(Windows) @system unittest {
110     import dshould;
111     import std.exception;
112     createTempDirectory("/some/unexisting/path").should.throwA!PathException;
113 }
114 
115 
116 /** Create temporary directory
117   * Note, that caller is responsible to remove created directory.
118   * The temp directory will be created inside specified path.
119   *
120   * Params:
121   *     path = path to already existing directory to create
122   *         temp directory inside. Default: std.file.tempDir
123   *     prefix = prefix to be used in name of temp directory. Default: "tmp"
124   * Returns: Path to created temp directory
125   * Throws: PathException in case of error
126   **/
127 @safe Path createTempPath(in string prefix="tmp") {
128     return Path(createTempDirectory(prefix));
129 }
130 
131 /// ditto
132 @safe Path createTempPath(in string path, in string prefix) {
133     return Path(createTempDirectory(path, prefix));
134 }
135 
136 /// ditto
137 @safe Path createTempPath(in Path path, in string prefix) {
138     return createTempPath(path.toString, prefix);
139 }