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 @system 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 * Saves settings. Uses user's settings dir. 68 * Params: 69 * data = The data that will be saved to the settings file. 70 * name = The subdirectory of the settings dir to save the config to. Created if nonexistent. 71 * filename = The filename the settings will be saved to. 72 * subdir = The subdirectory that the settings will be saved in. 73 */ 74 void saveSettings(T, alias settingsFormat = SettingsFormat)(T data, string name, string filename = settingsFilename, string subdir = "") { 75 import std.conv : text; 76 import std.exception : enforce; 77 auto paths = getSettingsPaths(name, subdir, filename, true); 78 enforce (!paths.empty, "No writable paths found"); 79 safeSave(paths.front.text, data.toString!settingsFormat()); 80 } 81 /// 82 unittest { 83 struct Settings { 84 bool blah; 85 string text; 86 string[] texts; 87 } 88 saveSettings(Settings(true, "some words", ["c", "b", "a"]), "testapp", "settings", "subdir"); 89 90 assert(loadSettings!Settings("testapp", "settings", "subdir") == Settings(true, "some words", ["c", "b", "a"])); 91 } 92 /** 93 * Deletes settings files for the specified app that are handled by this 94 * library. Also removes directory if empty. 95 * Params: 96 * name = App name. 97 * filename = The settings file that will be deleted. 98 * subdir = Settings subdirectory to delete. 99 */ 100 void deleteSettings(alias settingsFormat = SettingsFormat)(string name, string filename = settingsFilename, string subdir = "") { 101 import std.conv : text; 102 import std.file : exists, remove, dirEntries, SpanMode, rmdir; 103 import std.path : dirName; 104 foreach (path; getSettingsPaths(name, subdir, filename, true)) { 105 if (path.exists) { 106 remove(path); 107 if (path.dirName.text.dirEntries(SpanMode.shallow).empty) { 108 rmdir(path.dirName); 109 } 110 } 111 } 112 } 113 /// 114 unittest { 115 deleteSettings("testapp", "settings", "subdir"); 116 } 117 118 private template SettingsExtensions(T) { 119 import std.meta : AliasSeq; 120 static if (is(T == YAML)) { 121 alias SettingsExtensions = AliasSeq!(".yaml", ".yml"); 122 } else static if (is(T == JSON)) { 123 alias SettingsExtensions = AliasSeq!(".json"); 124 } 125 } 126 127 /** Safely save a text file. If something goes wrong while writing, the new 128 * file is simply discarded while the old one is untouched 129 */ 130 void safeSave(string path, string data) @safe { 131 import std.file : exists, remove, rename, write; 132 import std.path : setExtension; 133 const tmpFile = path.setExtension(".tmp"); 134 scope(exit) { 135 if (tmpFile.exists) { 136 remove(tmpFile); 137 } 138 } 139 write(tmpFile, data); 140 if (path.exists) { 141 remove(path); 142 } 143 rename(tmpFile, path); 144 } 145 146 @safe unittest { 147 import std.exception : assertThrown; 148 import std.file : exists, getAttributes, mkdir, remove, rmdir, setAttributes; 149 import std.path : buildPath; 150 enum testdir = "test"; 151 enum testFile = buildPath(testdir, "test.txt"); 152 mkdir(testdir); 153 scope(exit) { 154 rmdir(testdir); 155 } 156 safeSave(testFile, ""); 157 version(Windows) { 158 import core.sys.windows.winnt : FILE_ATTRIBUTE_READONLY; 159 enum readOnly = FILE_ATTRIBUTE_READONLY; 160 enum attributesTarget = testFile; 161 } else version(Posix) { 162 enum readOnly = 555; 163 enum attributesTarget = "test"; 164 } 165 const oldAttributes = getAttributes(attributesTarget); 166 setAttributes(attributesTarget, readOnly); 167 scope(exit) { 168 setAttributes(attributesTarget, oldAttributes); 169 remove(testFile); 170 } 171 assertThrown(safeSave(testFile, "")); 172 assert(!(testFile~".tmp").exists); 173 }