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     AbsolutePath[] rval = only(only(xdgDataHome ~ programName, Path(buildPath(thisExePath.dirName,
35             "data")), Path(buildPath(thisExePath.dirName.dirName, "data"))).map!(a => AbsolutePath(a))
36             .array, xdgDataDirs.map!(a => AbsolutePath(a ~ "data")).array).joiner.array;
37 
38     return rval;
39 }
40 
41 /// Search order is the users home directory, beside the binary followed by XDG config dir.
42 AbsolutePath[] configSearch(string programName) {
43     AbsolutePath[] rval = only(only(xdgDataHome ~ programName, Path(buildPath(thisExePath.dirName,
44             "config")), Path(buildPath(thisExePath.dirName.dirName, "config"))).map!(a => AbsolutePath(a))
45             .array, xdgDataDirs.map!(a => AbsolutePath(a ~ "config")).array).joiner.array;
46 
47     return rval;
48 }
49 
50 @("shall return the default locations to search for config resources")
51 unittest {
52     auto a = configSearch("caleb");
53     assert(a.length >= 4);
54     assert(a[0].baseName == "caleb");
55     assert(a[1].baseName == "config");
56     assert(a[2].baseName == "config");
57     assert(a[3].baseName == "config");
58 }
59 
60 @("shall return the default locations to search for data resources")
61 unittest {
62     auto a = dataSearch("caleb");
63     assert(a.length >= 4);
64     assert(a[0].baseName == "caleb");
65     assert(a[1].baseName == "data");
66     assert(a[2].baseName == "data");
67     assert(a[3].baseName == "data");
68 }
69 
70 /** Look for `lookFor` in `searchIn` by checking if the file exists at
71  * `buildPath(searchIn[i],lookFor)`.
72  *
73  * The result is cached thus further calls will use a thread local cache.
74  *
75  * Params:
76  *  searchIn = directories to search in starting from index 0.
77  *  lookFor = the file to search for.
78  */
79 Optional!ResourceFile resolve(const AbsolutePath[] searchIn, const Path lookFor) @trusted {
80     import std.file : dirEntries, SpanMode, exists;
81 
82     if (auto v = lookFor in resolveCache) {
83         return some(ResourceFile(*v));
84     }
85 
86     foreach (const sIn; searchIn) {
87         try {
88             AbsolutePath rval = sIn ~ lookFor;
89             if (exists(rval)) {
90                 resolveCache[lookFor] = rval;
91                 return some(ResourceFile(rval));
92             }
93 
94             foreach (a; dirEntries(sIn.value, SpanMode.shallow).filter!(a => a.isDir)) {
95                 rval = AbsolutePath(Path(a.name) ~ lookFor);
96                 if (exists(rval)) {
97                     resolveCache[lookFor] = rval;
98                     return some(ResourceFile(rval));
99                 }
100             }
101 
102         } catch (Exception e) {
103             logger.trace(e.msg);
104         }
105     }
106 
107     return none!ResourceFile();
108 }
109 
110 @("shall find the local file")
111 @system unittest {
112     import std.file : exists;
113     import std.stdio : File;
114     import my.test;
115 
116     auto testEnv = makeTestArea("find_local_file");
117 
118     File(testEnv.inSandbox("foo"), "w").write("bar");
119     auto res = resolve([testEnv.sandboxPath], Path("foo"));
120     assert(exists(res.orElse(ResourceFile.init).get));
121 
122     auto res2 = resolve([testEnv.sandboxPath], Path("foo"));
123     assert(res == res2);
124 }
125 
126 /// A convenient function to read a file as a text string from a resource.
127 string readResource(const ResourceFile r) {
128     import std.file : readText;
129 
130     return readText(r.get);
131 }