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 }