diff --git a/FileManager/UNTESTED/Configuration.cs b/FileManager/UNTESTED/Configuration.cs index 6060ba56..9a51ed8e 100644 --- a/FileManager/UNTESTED/Configuration.cs +++ b/FileManager/UNTESTED/Configuration.cs @@ -47,15 +47,15 @@ namespace FileManager [Description("Your user-specific key used to decrypt your audible files (*.aax) into audio files you can use anywhere (*.m4b). Leave alone in most cases")] public string DecryptKey { - get => persistentDictionary[nameof(DecryptKey)]; - set => persistentDictionary[nameof(DecryptKey)] = value; + get => persistentDictionary.GetString(nameof(DecryptKey)); + set => persistentDictionary.Set(nameof(DecryptKey), value); } [Description("Location for book storage. Includes destination of newly liberated books")] public string Books { - get => persistentDictionary[nameof(Books)]; - set => persistentDictionary[nameof(Books)] = value; + get => persistentDictionary.GetString(nameof(Books)); + set => persistentDictionary.Set(nameof(Books), value); } private const string APP_DIR = "AppDir"; @@ -79,22 +79,22 @@ namespace FileManager [Description("Temporary location of files while they're in process of being downloaded.\r\nWhen download is complete, the final file will be in [LibationFiles]\\DownloadsFinal")] public string DownloadsInProgressEnum { - get => persistentDictionary[nameof(DownloadsInProgressEnum)]; - set => persistentDictionary[nameof(DownloadsInProgressEnum)] = value; + get => persistentDictionary.GetString(nameof(DownloadsInProgressEnum)); + set => persistentDictionary.Set(nameof(DownloadsInProgressEnum), value); } // temp/working dir(s) should be outside of dropbox [Description("Temporary location of files while they're in process of being decrypted.\r\nWhen decryption is complete, the final file will be in Books location")] public string DecryptInProgressEnum { - get => persistentDictionary[nameof(DecryptInProgressEnum)]; - set => persistentDictionary[nameof(DecryptInProgressEnum)] = value; + get => persistentDictionary.GetString(nameof(DecryptInProgressEnum)); + set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value); } public string LocaleCountryCode { - get => persistentDictionary[nameof(LocaleCountryCode)]; - set => persistentDictionary[nameof(LocaleCountryCode)] = value; + get => persistentDictionary.GetString(nameof(LocaleCountryCode)); + set => persistentDictionary.Set(nameof(LocaleCountryCode), value); } // note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app @@ -123,7 +123,6 @@ namespace FileManager // load json values into memory. create if not exists persistentDictionary = new PersistentDictionary(SettingsJsonPath); - persistentDictionary.EnsureEntries(); return value; } diff --git a/FileManager/UNTESTED/PersistentDictionary.cs b/FileManager/UNTESTED/PersistentDictionary.cs index 46356e94..27fe98fa 100644 --- a/FileManager/UNTESTED/PersistentDictionary.cs +++ b/FileManager/UNTESTED/PersistentDictionary.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace FileManager { @@ -10,78 +11,87 @@ namespace FileManager { public string Filepath { get; } - // forgiving -- doesn't drop settings. old entries will continue to be persisted even if not publicly visible - private Dictionary settingsDic { get; } - - public string this[string key] - { - get => settingsDic[key]; - set - { - if (settingsDic.ContainsKey(key) && settingsDic[key] == value) - return; - - settingsDic[key] = value; - - // auto-save to file - save(); - } - } + // optimize for strings. expectation is most settings will be strings and a rare exception will be something else + private Dictionary stringCache { get; } = new Dictionary(); + private Dictionary objectCache { get; } = new Dictionary(); public PersistentDictionary(string filepath) { Filepath = filepath; - // not found. create blank file - if (!File.Exists(Filepath)) - { - // will create any missing directories, incl subdirectories. if all already exist: no action - Directory.CreateDirectory(Path.GetDirectoryName(filepath)); - - File.WriteAllText(Filepath, "{}"); - - // give system time to create file before first use - System.Threading.Thread.Sleep(100); - } - - settingsDic = JsonConvert.DeserializeObject>(File.ReadAllText(Filepath)); - } - - public void EnsureEntries() - { - var stringProperties = - GetPropertiesToPersist(typeof(T)) - .Select(p => p.Name) - .ToList(); - var keys = settingsDic.Keys.Cast().ToList(); - var missingKeys = stringProperties.Except(keys).ToList(); - - if (!missingKeys.Any()) + if (File.Exists(Filepath)) return; - foreach (var key in missingKeys) - settingsDic.Add(key, null); - save(); + // will create any missing directories, incl subdirectories. if all already exist: no action + Directory.CreateDirectory(Path.GetDirectoryName(filepath)); + + File.WriteAllText(Filepath, "{}"); + System.Threading.Thread.Sleep(100); + } + + public string GetString(string propertyName) + { + if (!stringCache.ContainsKey(propertyName)) + { + var jObject = readFile(); + stringCache[propertyName] = jObject.ContainsKey(propertyName) ? jObject[propertyName].Value() : null; + } + + return stringCache[propertyName]; + } + + public T Get(string propertyName) where T : class + => GetObject(propertyName) is T obj ? obj : default; + + public object GetObject(string propertyName) + { + if (!objectCache.ContainsKey(propertyName)) + { + var jObject = readFile(); + objectCache[propertyName] = jObject.ContainsKey(propertyName) ? jObject[propertyName].Value() : null; + } + + return objectCache[propertyName]; } private object locker { get; } = new object(); - private void save() + public void Set(string propertyName, string newValue) { + // only do this check in string cache, NOT object cache + if (stringCache[propertyName] == newValue) + return; + + // set cache + stringCache[propertyName] = newValue; + + // set in file lock (locker) - File.WriteAllText(Filepath, JsonConvert.SerializeObject(settingsDic, Formatting.Indented)); + { + var jObject = readFile(); + jObject[propertyName] = newValue; + File.WriteAllText(Filepath, JsonConvert.SerializeObject(jObject, Formatting.Indented)); + } } - public static IEnumerable GetPropertiesToPersist(Type type) - => type - .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) - .Where(p => - // string properties only - p.PropertyType == typeof(string) - // exclude indexer - && p.GetIndexParameters().Length == 0 - // exclude read-only, write-only - && p.GetGetMethod(false) != null - && p.GetSetMethod(false) != null - ).ToList(); + public void Set(string propertyName, object newValue) + { + // set cache + objectCache[propertyName] = newValue; + + // set in file + lock (locker) + { + var jObject = readFile(); + jObject[propertyName] = JToken.Parse(JsonConvert.SerializeObject(newValue)); + File.WriteAllText(Filepath, JsonConvert.SerializeObject(jObject, Formatting.Indented)); + } + } + + private JObject readFile() + { + var settingsJsonContents = File.ReadAllText(Filepath); + var jObject = JsonConvert.DeserializeObject(settingsJsonContents); + return jObject; + } } } diff --git a/LibationWinForms/UNTESTED/Dialogs/SettingsDialog.cs b/LibationWinForms/UNTESTED/Dialogs/SettingsDialog.cs index bf349327..ddc1edc7 100644 --- a/LibationWinForms/UNTESTED/Dialogs/SettingsDialog.cs +++ b/LibationWinForms/UNTESTED/Dialogs/SettingsDialog.cs @@ -20,21 +20,28 @@ namespace LibationWinForms.Dialogs { this.decryptKeyTb.Text = config.DecryptKey; this.decryptKeyDescLbl.Text = desc(nameof(config.DecryptKey)); + this.booksLocationDescLbl.Text = desc(nameof(config.Books)); + this.downloadsInProgressDescLbl.Text = desc(nameof(config.DownloadsInProgressEnum)); + this.decryptInProgressDescLbl.Text = desc(nameof(config.DecryptInProgressEnum)); + + var winTempText = "In your Windows temporary folder\r\n"; + this.downloadsInProgressWinTempRb.Text = $"{winTempText}{Path.Combine(Configuration.WinTemp, "DownloadsInProgress")}"; + this.decryptInProgressWinTempRb.Text = $"{winTempText}{Path.Combine(Configuration.WinTemp, "DecryptInProgress")}"; + + var libFileText = "In your Libation Files (ie: program-created files)\r\n"; + this.downloadsInProgressLibationFilesRb.Text = $"{libFileText}{Path.Combine(config.LibationFiles, "DownloadsInProgress")}"; + this.decryptInProgressLibationFilesRb.Text = $"{libFileText}{Path.Combine(config.LibationFiles, "DecryptInProgress")}"; this.booksLocationTb.Text = !string.IsNullOrWhiteSpace(config.Books) ? config.Books : Path.GetDirectoryName(Exe.FileLocationOnDisk); - this.booksLocationDescLbl.Text = desc(nameof(config.Books)); this.audibleLocaleCb.Text = !string.IsNullOrWhiteSpace(config.LocaleCountryCode) ? config.LocaleCountryCode : "us"; - this.downloadsInProgressDescLbl.Text = desc(nameof(config.DownloadsInProgressEnum)); - var winTempDownloadsInProgress = Path.Combine(Configuration.WinTemp, "DownloadsInProgress"); - this.downloadsInProgressWinTempRb.Text = "In your Windows temporary folder\r\n" + winTempDownloadsInProgress; switch (config.DownloadsInProgressEnum) { case "LibationFiles": @@ -46,9 +53,6 @@ namespace LibationWinForms.Dialogs break; } - this.decryptInProgressDescLbl.Text = desc(nameof(config.DecryptInProgressEnum)); - var winTempDecryptInProgress = Path.Combine(Configuration.WinTemp, "DecryptInProgress"); - this.decryptInProgressWinTempRb.Text = "In your Windows temporary folder\r\n" + winTempDecryptInProgress; switch (config.DecryptInProgressEnum) { case "LibationFiles":