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 }