mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-11 05:21:08 -05:00
feat(data): read and write the desktop user settings outside WinForms
UserSettingsFile in OpenNest.Data reads and writes %APPDATA%\OpenNest\user-settings.json (~/.config/OpenNest on Linux) so the Avalonia app can share the settings the WinForms app keeps there. AppDataSettingsProvider now delegates its file reads and writes to it, so there is one implementation of the format. A missing, unreadable or corrupt file reads as empty without touching it, and Update keeps entries the caller does not change. One difference for WinForms: an entry whose value is not text is now skipped instead of the whole file reading as empty.
This commit is contained in:
1 parent
3beaf53942
commit
b92d079e77
3 files changed
+207
-33
No files matched your search
@@ -0,0 +1,79 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The desktop apps' per-user settings file, %APPDATA%\OpenNest\user-settings.json: one JSON
|
||||
/// object of setting names to their text values. The WinForms app writes it through
|
||||
/// <c>AppDataSettingsProvider</c> (values in System.Configuration's invariant text form, e.g.
|
||||
/// "1936, 1048" for a size); the Avalonia app reads and writes the same names, so both apps
|
||||
/// share one file on Windows.
|
||||
/// </summary>
|
||||
public static class UserSettingsFile
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
/// <summary>%APPDATA%\OpenNest\user-settings.json (~/.config/OpenNest on Linux).</summary>
|
||||
public static string DefaultPath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"OpenNest", "user-settings.json");
|
||||
|
||||
/// <summary>
|
||||
/// The stored values. A missing, unreadable or corrupt file reads as empty, and an entry
|
||||
/// whose value is not text is skipped; reading never changes the file.
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> Read(string path)
|
||||
{
|
||||
var values = new Dictionary<string, string>();
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
return values;
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return values;
|
||||
|
||||
foreach (var property in document.RootElement.EnumerateObject())
|
||||
{
|
||||
if (property.Value.ValueKind == JsonValueKind.String)
|
||||
values[property.Name] = property.Value.GetString()!;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (SettingsFile.IsUnreadableFile(ex))
|
||||
{
|
||||
values.Clear();
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the file with <paramref name="values"/> through a temporary file. Settings are a
|
||||
/// convenience: a failed write returns false and leaves the previous file in place.
|
||||
/// </summary>
|
||||
public static bool Write(string path, IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsFile.Write(path, JsonSerializer.Serialize(values, JsonOptions));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-reads the file, applies <paramref name="change"/> and writes it back, so entries this
|
||||
/// build does not know (from a newer copy, or the other desktop app) are kept.
|
||||
/// </summary>
|
||||
public static bool Update(string path, Action<Dictionary<string, string>> change)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(change);
|
||||
var values = Read(path);
|
||||
change(values);
|
||||
return Write(path, values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using OpenNest.Data;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
public class UserSettingsFileTests : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(
|
||||
Path.GetTempPath(), "OpenNestTests", Guid.NewGuid().ToString());
|
||||
private readonly string _path;
|
||||
|
||||
public UserSettingsFileTests()
|
||||
{
|
||||
Directory.CreateDirectory(_directory);
|
||||
_path = Path.Combine(_directory, "user-settings.json");
|
||||
}
|
||||
|
||||
public void Dispose() => Directory.Delete(_directory, recursive: true);
|
||||
|
||||
[Fact]
|
||||
public void DefaultPath_IsTheWinFormsSettingsFile()
|
||||
{
|
||||
Assert.Equal(
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"OpenNest", "user-settings.json"),
|
||||
UserSettingsFile.DefaultPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_MissingFile_IsEmptyAndCreatesNothing()
|
||||
{
|
||||
Assert.Empty(UserSettingsFile.Read(_path));
|
||||
Assert.False(File.Exists(_path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_ValuesTheWinFormsAppWrote()
|
||||
{
|
||||
File.WriteAllText(_path, """
|
||||
{
|
||||
"MainWindowSize": "1936, 1048",
|
||||
"MainWindowState": "Maximized",
|
||||
"LastFeedrate": "100"
|
||||
}
|
||||
""");
|
||||
|
||||
var values = UserSettingsFile.Read(_path);
|
||||
|
||||
Assert.Equal("1936, 1048", values["MainWindowSize"]);
|
||||
Assert.Equal("Maximized", values["MainWindowState"]);
|
||||
Assert.Equal("100", values["LastFeedrate"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{ not json")]
|
||||
[InlineData("[\"a\"]")]
|
||||
[InlineData("")]
|
||||
public void Read_BrokenFile_IsEmptyAndLeavesTheFileAlone(string content)
|
||||
{
|
||||
File.WriteAllText(_path, content);
|
||||
|
||||
Assert.Empty(UserSettingsFile.Read(_path));
|
||||
Assert.Equal(content, File.ReadAllText(_path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_SkipsEntriesThatAreNotText()
|
||||
{
|
||||
File.WriteAllText(_path, """{ "Kept": "x", "Number": 5, "Nested": { "a": "b" } }""");
|
||||
|
||||
var values = UserSettingsFile.Read(_path);
|
||||
|
||||
Assert.Equal(new Dictionary<string, string> { ["Kept"] = "x" }, values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KeepsEntriesItDoesNotChange()
|
||||
{
|
||||
File.WriteAllText(_path, """{ "FromNewerBuild": "x", "MainWindowState": "Normal" }""");
|
||||
|
||||
var updated = UserSettingsFile.Update(
|
||||
_path, values => values["MainWindowState"] = "Maximized");
|
||||
|
||||
Assert.True(updated);
|
||||
|
||||
var values = UserSettingsFile.Read(_path);
|
||||
Assert.Equal("x", values["FromNewerBuild"]);
|
||||
Assert.Equal("Maximized", values["MainWindowState"]);
|
||||
Assert.False(File.Exists(_path + ".tmp"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BrokenFile_WritesAReadableFile()
|
||||
{
|
||||
File.WriteAllText(_path, "{ not json");
|
||||
|
||||
Assert.True(UserSettingsFile.Update(_path, values => values["MainWindowState"] = "Normal"));
|
||||
|
||||
Assert.Equal("Normal", UserSettingsFile.Read(_path)["MainWindowState"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_CreatesTheFolder()
|
||||
{
|
||||
var path = Path.Combine(_directory, "new", "user-settings.json");
|
||||
|
||||
Assert.True(UserSettingsFile.Update(path, values => values["A"] = "1"));
|
||||
|
||||
Assert.Equal("1", UserSettingsFile.Read(path)["A"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_FailureReturnsFalseWithoutThrowing()
|
||||
{
|
||||
// A file where the settings folder should be makes the write fail.
|
||||
var blocker = Path.Combine(_directory, "blocker");
|
||||
File.WriteAllText(blocker, "");
|
||||
|
||||
Assert.False(UserSettingsFile.Write(
|
||||
Path.Combine(blocker, "user-settings.json"),
|
||||
new Dictionary<string, string> { ["A"] = "1" }));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using OpenNest.Data;
|
||||
|
||||
namespace OpenNest.Properties
|
||||
{
|
||||
@@ -20,8 +20,6 @@ namespace OpenNest.Properties
|
||||
/// </remarks>
|
||||
public sealed class AppDataSettingsProvider : SettingsProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
private readonly string _path;
|
||||
private readonly Func<SettingsProvider> _legacyProvider;
|
||||
|
||||
@@ -40,9 +38,7 @@ namespace OpenNest.Properties
|
||||
internal static string PathOverride { get; set; }
|
||||
|
||||
/// <summary>%APPDATA%\OpenNest\user-settings.json.</summary>
|
||||
public static string DefaultPath => PathOverride ?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"OpenNest", "user-settings.json");
|
||||
public static string DefaultPath => PathOverride ?? UserSettingsFile.DefaultPath;
|
||||
|
||||
public override string ApplicationName { get; set; } = "OpenNest";
|
||||
|
||||
@@ -91,19 +87,7 @@ namespace OpenNest.Properties
|
||||
Write(stored);
|
||||
}
|
||||
|
||||
private Dictionary<string, string> Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(_path)
|
||||
? JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(_path)) ?? new()
|
||||
: new();
|
||||
}
|
||||
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
private Dictionary<string, string> Load() => UserSettingsFile.Read(_path);
|
||||
|
||||
private Dictionary<string, string> Migrate(SettingsContext context, SettingsPropertyCollection collection)
|
||||
{
|
||||
@@ -135,19 +119,8 @@ namespace OpenNest.Properties
|
||||
return stored;
|
||||
}
|
||||
|
||||
private void Write(Dictionary<string, string> stored)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(_path)));
|
||||
var temp = _path + ".tmp";
|
||||
File.WriteAllText(temp, JsonSerializer.Serialize(stored, JsonOptions));
|
||||
File.Move(temp, _path, overwrite: true);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Settings are a convenience: keep running with the in-memory values.
|
||||
}
|
||||
}
|
||||
// Settings are a convenience: a failed write keeps running with the in-memory values.
|
||||
private void Write(Dictionary<string, string> stored) =>
|
||||
UserSettingsFile.Write(_path, stored);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user