PR B: atomic XML writes and per-file locking
Save<T> writes to a .tmp sibling and atomically renames, so a crash mid-write leaves the previous file intact. Load<T>, Save<T>, and EnsureFileExisits share a per-absolute-path lock so concurrent Save+Save and Save+Load can't observe a half-written file. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9f11f6490b
commit
152d4c3d57
1 changed files with 51 additions and 19 deletions
|
|
@ -1,4 +1,6 @@
|
||||||
using System.IO;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
|
|
@ -8,6 +10,11 @@ namespace Schaad.Accounting.Repositories
|
||||||
{
|
{
|
||||||
public abstract class BaseRepository
|
public abstract class BaseRepository
|
||||||
{
|
{
|
||||||
|
// One lock per absolute file path so concurrent Save+Save and Save+Load are serialized
|
||||||
|
// and can't observe a half-written file.
|
||||||
|
private static readonly ConcurrentDictionary<string, object> FileLocks =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
protected readonly ISettingsService settingsService;
|
protected readonly ISettingsService settingsService;
|
||||||
|
|
||||||
protected BaseRepository(ISettingsService settingsService)
|
protected BaseRepository(ISettingsService settingsService)
|
||||||
|
|
@ -18,6 +25,8 @@ namespace Schaad.Accounting.Repositories
|
||||||
protected void EnsureFileExisits(string fileName)
|
protected void EnsureFileExisits(string fileName)
|
||||||
{
|
{
|
||||||
string filePath = Path.Combine(settingsService.GetDbPath(), fileName);
|
string filePath = Path.Combine(settingsService.GetDbPath(), fileName);
|
||||||
|
lock (GetLock(filePath))
|
||||||
|
{
|
||||||
if (File.Exists(filePath) == false)
|
if (File.Exists(filePath) == false)
|
||||||
{
|
{
|
||||||
var lastYearFile = Path.Combine(settingsService.GetLastYearDbPath(), fileName);
|
var lastYearFile = Path.Combine(settingsService.GetLastYearDbPath(), fileName);
|
||||||
|
|
@ -27,26 +36,42 @@ namespace Schaad.Accounting.Repositories
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save an object to an xml file
|
/// Save an object to an xml file. Writes to a .tmp sibling and then atomically
|
||||||
|
/// renames it, so a crash mid-write leaves the previous file intact.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected void Save<T>(T obj, string fileName)
|
protected void Save<T>(T obj, string fileName)
|
||||||
{
|
{
|
||||||
var filePath = Path.Combine(settingsService.GetDbPath(), fileName);
|
var filePath = Path.Combine(settingsService.GetDbPath(), fileName);
|
||||||
using (var sww = new MemoryStream())
|
var tmpPath = filePath + ".tmp";
|
||||||
|
|
||||||
|
lock (GetLock(filePath))
|
||||||
{
|
{
|
||||||
var settings = new XmlWriterSettings
|
var settings = new XmlWriterSettings
|
||||||
{
|
{
|
||||||
Encoding = Encoding.UTF8,
|
Encoding = Encoding.UTF8,
|
||||||
Indent = true
|
Indent = true
|
||||||
};
|
};
|
||||||
using (var writer = XmlWriter.Create(sww, settings))
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var xsSubmit = new XmlSerializer(typeof(T));
|
using (var writer = XmlWriter.Create(tmpPath, settings))
|
||||||
xsSubmit.Serialize(writer, obj);
|
{
|
||||||
var xml = Encoding.UTF8.GetString(sww.ToArray());
|
var serializer = new XmlSerializer(typeof(T));
|
||||||
File.WriteAllText(filePath, xml);
|
serializer.Serialize(writer, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Move(tmpPath, filePath, overwrite: true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (File.Exists(tmpPath))
|
||||||
|
{
|
||||||
|
try { File.Delete(tmpPath); } catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +82,9 @@ namespace Schaad.Accounting.Repositories
|
||||||
protected T Load<T>(string fileName)
|
protected T Load<T>(string fileName)
|
||||||
{
|
{
|
||||||
var filePath = Path.Combine(settingsService.GetDbPath(), fileName);
|
var filePath = Path.Combine(settingsService.GetDbPath(), fileName);
|
||||||
|
|
||||||
|
lock (GetLock(filePath))
|
||||||
|
{
|
||||||
if (File.Exists(filePath) == false)
|
if (File.Exists(filePath) == false)
|
||||||
{
|
{
|
||||||
return default(T);
|
return default(T);
|
||||||
|
|
@ -69,4 +97,8 @@ namespace Schaad.Accounting.Repositories
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object GetLock(string filePath)
|
||||||
|
=> FileLocks.GetOrAdd(filePath, _ => new object());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue