1 module easysettings; 2 private import siryul; 3 private import standardpaths; 4 5 private enum settingsFilename = "settings"; 6 private alias SettingsFormat = YAML; 7 8 /** 9 * Get a list of paths where settings files were found. 10 * Params: 11 * settingsForma = The serialization format used to save and load settings (YAML, JSON, etc) 12 * name = Subdirectory of settings dir to save config to. Created if nonexistent. 13 * filename = The filename the settings will be loaded from. 14 * subdir = The subdirectory that the settings will be loaded from. 15 */ 16 auto getSettingsPaths(alias settingsFormat = SettingsFormat)(string name, string subdir, string filename, bool writable) { 17 import std.algorithm : cartesianProduct, filter, map; 18 import std.experimental.logger : tracef; 19 import std.file : exists; 20 import std.path : buildPath, chainPath, withExtension; 21 import std.range : chain, choose, only; 22 const subPath = buildPath(name, subdir); 23 const candidates = writable.choose(only(writablePath(StandardPath.config, subPath, FolderFlag.create)), standardPaths(StandardPath.config, subPath)); 24 auto searchPaths = candidates.chain(["."]).cartesianProduct(only(SettingsExtensions!settingsFormat)).map!(x => chainPath(x[0], filename ~ x[1])); 25 tracef("Search paths: %s", searchPaths); 26 return searchPaths.filter!(x => writable || x.exists); 27 } 28 29 /** 30 * Load settings. Will create settings file by default. Searches all system-wide 31 * settings dirs as well as the user's settings dir, and loads the first file 32 * found. 33 * Params: 34 * T = Type of settings struct to load 35 * name = Subdirectory of settings dir to save config to. Created if nonexistent. 36 * filename = The filename the settings will be loaded from. 37 * subdir = The subdirectory that the settings will be loaded from. 38 */ 39 auto loadSettings(T, alias settingsFormat = SettingsFormat)(string name, string filename = settingsFilename, string subdir = "") { 40 import std.conv : text; 41 import std.experimental.logger : tracef; 42 auto paths = getSettingsPaths!settingsFormat(name, subdir, filename, false); 43 if (!paths.empty) { 44 tracef("Loading settings from '%s'", paths.front); 45 return fromFile!(T, settingsFormat, DeSiryulize.optionalByDefault)(paths.front.text); 46 } else { 47 saveSettings(T.init, name, filename, subdir); 48 } 49 return T.init; 50 } 51 /// 52 @safe unittest { 53 struct Settings { 54 bool blah; 55 string text; 56 string[] texts; 57 } 58 auto settings = loadSettings!Settings("testapp", "settings", "subdir"); 59 settings.texts = ["a", "b", "c"]; 60 saveSettings(settings, "testapp", "settings", "subdir"); 61 62 auto reloadedSettings = loadSettings!Settings("testapp", "settings", "subdir"); 63 assert(reloadedSettings == settings); 64 assert(reloadedSettings.texts == ["a", "b", "c"]); 65 } 66 /** 67 * Loads all settings files from a subdirectory, with the assumption that each 68 * file has the same format. 69 * Params: 70 * name = The main settings directory for the application 71 * subdir = The subdirectory to load these settings files from 72 */ 73 auto loadSubdirSettings(T, alias settingsFormat = SettingsFormat)(string name, string subdir) { 74 import std.algorithm : cartesianProduct, filter, joiner, map; 75 import std.experimental.logger : tracef; 76 import std.file : dirEntries, exists, SpanMode; 77 import std.path : buildPath, chainPath, withExtension; 78 import std.range : chain, choose, only; 79 const subPath = buildPath(name, subdir); 80 return standardPaths(StandardPath.config, subPath) 81 .cartesianProduct(only(SettingsExtensions!settingsFormat)) 82 .filter!(x => x[0].exists) 83 .map!(x => dirEntries(x[0], "*"~x[1], SpanMode.depth)) 84 .joiner() 85 .map!(x => fromFile!(T, settingsFormat, DeSiryulize.optionalByDefault)(x)); 86 } 87 /// 88 @system unittest { 89 import std.array : array; 90 import std.algorithm.searching : canFind; 91 static struct Settings { 92 uint a; 93 } 94 saveSettings(Settings(1), "testapp", "1", "mysubdir"); 95 saveSettings(Settings(2), "testapp", "2", "mysubdir"); 96 auto loaded = loadSubdirSettings!Settings("testapp", "mysubdir").array; 97 assert(loaded.canFind(Settings(1))); 98 assert(loaded.canFind(Settings(2))); 99 } 100 101 /** 102 * Saves settings. Uses user's settings dir. 103 * Params: 104 * data = The data that will be saved to the settings file. 105 * name = The subdirectory of the settings dir to save the config to. Created if nonexistent. 106 * filename = The filename the settings will be saved to. 107 * subdir = The subdirectory that the settings will be saved in. 108 */ 109 void saveSettings(T, alias settingsFormat = SettingsFormat)(T data, string name, string filename = settingsFilename, string subdir = "") { 110 import std.conv : text; 111 import std.exception : enforce; 112 auto paths = getSettingsPaths(name, subdir, filename, true); 113 enforce (!paths.empty, "No writable paths found"); 114 safeSave(paths.front.text, toString!settingsFormat(data)); 115 } 116 /// 117 @safe unittest { 118 struct Settings { 119 bool blah; 120 string text; 121 string[] texts; 122 } 123 saveSettings(Settings(true, "some words", ["c", "b", "a"]), "testapp", "settings", "subdir"); 124 125 assert(loadSettings!Settings("testapp", "settings", "subdir") == Settings(true, "some words", ["c", "b", "a"])); 126 } 127 /** 128 * Deletes settings files for the specified app that are handled by this 129 * library. Also removes directory if empty. 130 * Params: 131 * name = App name. 132 * filename = The settings file that will be deleted. 133 * subdir = Settings subdirectory to delete. 134 */ 135 void deleteSettings(alias settingsFormat = SettingsFormat)(string name, string filename = settingsFilename, string subdir = "") { 136 import std.conv : text; 137 import std.file : exists, remove, dirEntries, SpanMode, rmdir; 138 import std.path : dirName; 139 foreach (path; getSettingsPaths(name, subdir, filename, true)) { 140 if (path.exists) { 141 remove(path); 142 if (path.dirName.text.dirEntries(SpanMode.shallow).empty) { 143 rmdir(path.dirName); 144 } 145 } 146 } 147 } 148 /// 149 @system unittest { 150 deleteSettings("testapp", "settings", "subdir"); 151 deleteSettings("testapp", "settings", "mysubdir"); 152 deleteSettings("testapp", "settings", ""); 153 } 154 155 private template SettingsExtensions(T) { 156 import std.meta : AliasSeq; 157 static if (is(T == YAML)) { 158 alias SettingsExtensions = AliasSeq!(".yaml", ".yml"); 159 } else static if (is(T == JSON)) { 160 alias SettingsExtensions = AliasSeq!(".json"); 161 } 162 } 163 164 /** Safely save a text file. If something goes wrong while writing, the new 165 * file is simply discarded while the old one is untouched 166 */ 167 void safeSave(string path, string data) @safe { 168 import std.file : exists, remove, rename, write; 169 import std.path : setExtension; 170 const tmpFile = path.setExtension(".tmp"); 171 scope(exit) { 172 if (tmpFile.exists) { 173 remove(tmpFile); 174 } 175 } 176 write(tmpFile, data); 177 if (path.exists) { 178 remove(path); 179 } 180 rename(tmpFile, path); 181 } 182 183 @safe unittest { 184 import std.exception : assertThrown; 185 import std.file : exists, getAttributes, mkdir, remove, rmdir, setAttributes; 186 import std.path : buildPath; 187 enum testdir = "test"; 188 enum testFile = buildPath(testdir, "test.txt"); 189 mkdir(testdir); 190 scope(exit) { 191 rmdir(testdir); 192 } 193 safeSave(testFile, ""); 194 version(Windows) { 195 import core.sys.windows.winnt : FILE_ATTRIBUTE_READONLY; 196 enum readOnly = FILE_ATTRIBUTE_READONLY; 197 enum attributesTarget = testFile; 198 } else version(Posix) { 199 enum readOnly = 555; 200 enum attributesTarget = "test"; 201 } 202 const oldAttributes = getAttributes(attributesTarget); 203 setAttributes(attributesTarget, readOnly); 204 scope(exit) { 205 setAttributes(attributesTarget, oldAttributes); 206 remove(testFile); 207 } 208 assertThrown(safeSave(testFile, "")); 209 assert(!(testFile~".tmp").exists); 210 }