mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-03 18:11:36 -05:00
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>
85 lines
2.7 KiB
C#
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;
|
|
}
|
|
}
|