Files
OpenNest/OpenNest.Data/SettingsFile.cs
T
ajandClaude Opus 5.5 d29f919aa2 fix(io): save nests safely and reopen them with their cut-off settings
Reopening a nest regenerated its cut-offs with built-in defaults (no
overtravel, a different minimum segment) rather than the machine's
settings it was made with, so a nest saved, reopened and posted could
cut scrap that never separates. Plates now record the settings their
cut-offs were last generated with, and files carry them; older files
still load with the defaults.

Saving over a nest file truncated it before writing, so any failure
part way lost the only good copy, and the error reached the user as an
unhandled exception after the save path had already changed. Nest and
settings files are now written beside the target and swapped in, and a
failed save is reported with the old file left intact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:51:11 -05:00

85 lines
2.7 KiB
C#

using System.Text.Json;
namespace OpenNest.Data;
/// <summary>File handling shared by the JSON settings files under %APPDATA%\OpenNest.</summary>
internal static class SettingsFile
{
/// <summary>
/// Writes the file through a temporary file swapped into place, so a failed write keeps
/// the previous settings, creating the parent directory and retrying briefly on IO
/// collisions. The last failure propagates; the host handles a persistent write failure.
/// </summary>
public static void Write(string path, string json, int maxRetries = 3)
{
var directory = Path.GetDirectoryName(Path.GetFullPath(path));
for (var attempt = 0; attempt < maxRetries; attempt++)
{
try
{
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
var temp = path + ".tmp";
File.WriteAllText(temp, json);
File.Move(temp, path, overwrite: true);
return;
}
catch (IOException) when (attempt < maxRetries - 1)
{
Thread.Sleep(100);
}
}
}
/// <summary>
/// Deserializes the file, or returns null when it is missing, unreadable or corrupt.
/// </summary>
public static T? Read<T>(string path, JsonSerializerOptions options)
where T : class
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return null;
try
{
return JsonSerializer.Deserialize<T>(File.ReadAllText(path), options);
}
catch (Exception ex) when (IsUnreadableFile(ex))
{
return null;
}
}
public static bool IsUnreadableFile(Exception ex) =>
ex
is JsonException
or IOException
or UnauthorizedAccessException
or NotSupportedException
or System.Security.SecurityException;
/// <summary>
/// Accepts only a defined unit name (case-insensitive). Enum.TryParse would also accept
/// numeric strings ("7") and comma-joined names, producing undefined or unintended values.
/// </summary>
public static bool TryParseUnits(JsonElement? element, out Units units)
{
units = default;
if (element is not { ValueKind: JsonValueKind.String } value)
return false;
var text = value.GetString();
foreach (var candidate in Enum.GetValues<Units>())
{
if (string.Equals(candidate.ToString(), text, StringComparison.OrdinalIgnoreCase))
{
units = candidate;
return true;
}
}
return false;
}
}