using System; using System.Collections.Concurrent; using System.IO; using System.Text; using System.Xml; using System.Xml.Serialization; using Schaad.Accounting.Interfaces; namespace Schaad.Accounting.Repositories { 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 FileLocks = new(StringComparer.OrdinalIgnoreCase); protected readonly ISettingsService settingsService; protected BaseRepository(ISettingsService settingsService) { this.settingsService = settingsService; } protected void EnsureFileExisits(string fileName) { string filePath = Path.Combine(settingsService.GetDbPath(), fileName); lock (GetLock(filePath)) { if (File.Exists(filePath) == false) { var lastYearFile = Path.Combine(settingsService.GetLastYearDbPath(), fileName); if (File.Exists(lastYearFile)) { File.Copy(lastYearFile, filePath); } } } } /// /// 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. /// protected void Save(T obj, string fileName) { var filePath = Path.Combine(settingsService.GetDbPath(), fileName); var tmpPath = filePath + ".tmp"; lock (GetLock(filePath)) { var settings = new XmlWriterSettings { Encoding = Encoding.UTF8, Indent = true }; try { using (var writer = XmlWriter.Create(tmpPath, settings)) { var serializer = new XmlSerializer(typeof(T)); serializer.Serialize(writer, obj); } File.Move(tmpPath, filePath, overwrite: true); } catch { if (File.Exists(tmpPath)) { try { File.Delete(tmpPath); } catch { /* best effort */ } } throw; } } } /// /// Load an object from an xml file /// protected T Load(string fileName) { var filePath = Path.Combine(settingsService.GetDbPath(), fileName); lock (GetLock(filePath)) { if (File.Exists(filePath) == false) { return default(T); } using (XmlReader reader = XmlReader.Create(filePath)) { var serializer = new XmlSerializer(typeof(T)); return (T)serializer.Deserialize(reader); } } } private static object GetLock(string filePath) => FileLocks.GetOrAdd(filePath, _ => new object()); } }