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 debug(verbosesettings) 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 debug(verbosesettings) 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.file : dirEntries, exists, SpanMode; 76 import std.path : buildPath, chainPath, withExtension; 77 import std.range : chain, choose, only; 78 const subPath = buildPath(name, subdir); 79 return standardPaths(StandardPath.config, subPath) 80 .cartesianProduct(only(SettingsExtensions!settingsFormat)) 81 .filter!(x => x[0].exists) 82 .map!(x => dirEntries(x[0], "*"~x[1], SpanMode.depth)) 83 .joiner() 84 .map!(x => fromFile!(T, settingsFormat, DeSiryulize.optionalByDefault)(x)); 85 } 86 /// 87 @system unittest { 88 import std.array : array; 89 import std.algorithm.searching : canFind; 90 static struct Settings { 91 uint a; 92 } 93 saveSettings(Settings(1), "testapp", "1", "mysubdir"); 94 saveSettings(Settings(2), "testapp", "2", "mysubdir"); 95 auto loaded = loadSubdirSettings!Settings("testapp", "mysubdir").array; 96 assert(loaded.canFind(Settings(1))); 97 assert(loaded.canFind(Settings(2))); 98 } 99 100 /** 101 * Saves settings. Uses user's settings dir. 102 * Params: 103 * data = The data that will be saved to the settings file. 104 * name = The subdirectory of the settings dir to save the config to. Created if nonexistent. 105 * filename = The filename the settings will be saved to. 106 * subdir = The subdirectory that the settings will be saved in. 107 */ 108 void saveSettings(T, alias settingsFormat = SettingsFormat)(T data, string name, string filename = settingsFilename, string subdir = "") { 109 import std.conv : text; 110 import std.exception : enforce; 111 auto paths = getSettingsPaths(name, subdir, filename, true); 112 enforce (!paths.empty, "No writable paths found"); 113 safeSave(paths.front.text, toString!settingsFormat(data)); 114 } 115 /// 116 @safe unittest { 117 struct Settings { 118 bool blah; 119 string text; 120 string[] texts; 121 } 122 saveSettings(Settings(true, "some words", ["c", "b", "a"]), "testapp", "settings", "subdir"); 123 124 assert(loadSettings!Settings("testapp", "settings", "subdir") == Settings(true, "some words", ["c", "b", "a"])); 125 } 126 /** 127 * Deletes settings files for the specified app that are handled by this 128 * library. Also removes directory if empty. 129 * Params: 130 * name = App name. 131 * filename = The settings file that will be deleted. 132 * subdir = Settings subdirectory to delete. 133 */ 134 void deleteSettings(alias settingsFormat = SettingsFormat)(string name, string filename = settingsFilename, string subdir = "") { 135 import std.conv : text; 136 import std.file : exists, remove, dirEntries, SpanMode, rmdir; 137 import std.path : dirName; 138 foreach (path; getSettingsPaths(name, subdir, filename, true)) { 139 if (path.exists) { 140 remove(path); 141 if (path.dirName.text.dirEntries(SpanMode.shallow).empty) { 142 rmdir(path.dirName); 143 } 144 } 145 } 146 } 147 /// 148 @system unittest { 149 deleteSettings("testapp", "settings", "subdir"); 150 deleteSettings("testapp", "settings", "mysubdir"); 151 deleteSettings("testapp", "settings", ""); 152 } 153 154 private template SettingsExtensions(T) { 155 import std.meta : AliasSeq; 156 static if (is(T == YAML)) { 157 alias SettingsExtensions = AliasSeq!(".yaml", ".yml"); 158 } else static if (is(T == JSON)) { 159 alias SettingsExtensions = AliasSeq!(".json"); 160 } 161 } 162 163 /** Safely save a text file. If something goes wrong while writing, the new 164 * file is simply discarded while the old one is untouched 165 */ 166 void safeSave(string path, string data) @safe { 167 import std.file : exists, remove, rename, write; 168 import std.path : setExtension; 169 const tmpFile = path.setExtension(".tmp"); 170 scope(exit) { 171 if (tmpFile.exists) { 172 remove(tmpFile); 173 } 174 } 175 write(tmpFile, data); 176 if (path.exists) { 177 remove(path); 178 } 179 rename(tmpFile, path); 180 } 181 182 @safe unittest { 183 import std.exception : assertThrown; 184 import std.file : exists, getAttributes, mkdir, remove, rmdir, setAttributes; 185 import std.path : buildPath; 186 enum testdir = "test"; 187 enum testFile = buildPath(testdir, "test.txt"); 188 mkdir(testdir); 189 scope(exit) { 190 rmdir(testdir); 191 } 192 safeSave(testFile, ""); 193 version(Windows) { 194 import core.sys.windows.winnt : FILE_ATTRIBUTE_READONLY; 195 enum readOnly = FILE_ATTRIBUTE_READONLY; 196 enum attributesTarget = testFile; 197 } else version(Posix) { 198 enum readOnly = 555; 199 enum attributesTarget = "test"; 200 } 201 const oldAttributes = getAttributes(attributesTarget); 202 setAttributes(attributesTarget, readOnly); 203 scope(exit) { 204 setAttributes(attributesTarget, oldAttributes); 205 remove(testFile); 206 } 207 assertThrown(safeSave(testFile, "")); 208 assert(!(testFile~".tmp").exists); 209 }