1 /**
2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 Convenient functions for accessing files via a priority list such that there
7 are defaults installed in e.g. /etc while a user can override them in their
8 home directory.
9 */
10 module my.resource;
11 
12 import logger = std.experimental.logger;
13 import std.algorithm : filter, map, joiner;
14 import std.array : array;
15 import std.file : thisExePath;
16 import std.path : dirName, buildPath, baseName;
17 import std.process : environment;
18 import std.range : only;
19 
20 import my.named_type;
21 import my.optional;
22 import my.path;
23 import my.xdg : xdgDataHome, xdgConfigHome, xdgDataDirs, xdgConfigDirs;
24 
25 alias ResourceFile = NamedType!(AbsolutePath, Tag!"ResourceFile",
26         AbsolutePath.init, TagStringable);
27 
28 @safe:
29 
30 private AbsolutePath[Path] resolveCache;
31 
32 /// Search order is the users home directory, beside the binary followed by XDG data dir.
33 AbsolutePath[] dataSearch(string programName) {
34     // dfmt off
35     AbsolutePath[] rval = only(only(xdgDataHome ~ programName,
36                                     Path(buildPath(thisExePath.dirName, "data")),
37                                     Path(buildPath(thisExePath.dirName.dirName, "data"))
38                                     ).map!(a => AbsolutePath(a)).array,
39                                xdgDataDirs.map!(a => AbsolutePath(buildPath(a, programName, "data"))).array
40                                ).joiner.array;
41     // dfmt on
42 
43     return rval;
44 }
45 
46 /// Search order is the users home directory, beside the binary followed by XDG config dir.
47 AbsolutePath[] configSearch(string programName) {
48     // dfmt off
49     AbsolutePath[] rval = only(only(xdgDataHome ~ programName,
50                                     Path(buildPath(thisExePath.dirName, "config")),
51                                     Path(buildPath(thisExePath.dirName.dirName, "config"))
52                                     ).map!(a => AbsolutePath(a)).array,
53                                xdgDataDirs.map!(a => AbsolutePath(buildPath(a, programName, "config"))).array
54                                ).joiner.array;
55     // dfmt on
56 
57     return rval;
58 }
59 
60 @("shall return the default locations to search for config resources")
61 unittest {
62     auto a = configSearch("caleb");
63     assert(a.length >= 3);
64     assert(a[0].baseName == "caleb");
65     assert(a[1].baseName == "config");
66     assert(a[2].baseName == "config");
67 }
68 
69 @("shall return the default locations to search for data resources")
70 unittest {
71     auto a = dataSearch("caleb");
72     assert(a.length >= 3);
73     assert(a[0].baseName == "caleb");
74     assert(a[1].baseName == "data");
75     assert(a[2].baseName == "data");
76 }
77 
78 /** Look for `lookFor` in `searchIn` by checking if the file exists at
79  * `buildPath(searchIn[i],lookFor)`.
80  *
81  * The result is cached thus further calls will use a thread local cache.
82  *
83  * Params:
84  *  searchIn = directories to search in starting from index 0.
85  *  lookFor = the file to search for.
86  */
87 Optional!ResourceFile resolve(const AbsolutePath[] searchIn, const Path lookFor) @trusted {
88     import std.file : dirEntries, SpanMode, exists;
89 
90     if (auto v = lookFor in resolveCache) {
91         return some(ResourceFile(*v));
92     }
93 
94     foreach (const sIn; searchIn) {
95         try {
96             AbsolutePath rval = sIn ~ lookFor;
97             if (exists(rval)) {
98                 resolveCache[lookFor] = rval;
99                 return some(ResourceFile(rval));
100             }
101 
102             foreach (a; dirEntries(sIn.value, SpanMode.shallow).filter!(a => a.isDir)) {
103                 rval = AbsolutePath(Path(a.name) ~ lookFor);
104                 if (exists(rval)) {
105                     resolveCache[lookFor] = rval;
106                     return some(ResourceFile(rval));
107                 }
108             }
109 
110         } catch (Exception e) {
111             logger.trace(e.msg);
112         }
113     }
114 
115     return none!ResourceFile();
116 }
117 
118 @("shall find the local file")
119 @system unittest {
120     import std.file : exists;
121     import std.stdio : File;
122     import my.test;
123 
124     auto testEnv = makeTestArea("find_local_file");
125 
126     File(testEnv.inSandbox("foo"), "w").write("bar");
127     auto res = resolve([testEnv.sandboxPath], Path("foo"));
128     assert(exists(res.orElse(ResourceFile.init).get));
129 
130     auto res2 = resolve([testEnv.sandboxPath], Path("foo"));
131     assert(res == res2);
132 }
133 
134 /// A convenient function to read a file as a text string from a resource.
135 string readResource(const ResourceFile r) {
136     import std.file : readText;
137 
138     return readText(r.get);
139 }