První commit — WPF aplikace pro generování výkresových rohových razítek (.NET 10, SkiaSharp, ACadSharp, ClosedXML) včetně xUnit testů, DXF šablony, log a dokumentace. Build výstupy (bin/, obj/, .vs/) jsou ignorovány. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.6 KiB
C#
116 lines
4.6 KiB
C#
using System.IO;
|
||
using System.Text;
|
||
using System.Text.Encodings.Web;
|
||
using System.Text.Json;
|
||
using Rozpisky.Models;
|
||
|
||
namespace Rozpisky.Data;
|
||
|
||
/// <summary>
|
||
/// Společné konstanty a JSON nastavení pro `.rzp` soubory. Vlastní čtení/zápis se zámkem
|
||
/// řeší <see cref="ProjektFileHandle"/>. Žádné NuGet závislosti (System.Text.Json je součást .NET).
|
||
/// </summary>
|
||
public static class ProjektStore
|
||
{
|
||
public const string Pripona = ".rzp";
|
||
public const string DialogFilter = "Projekt Rozpisky (*.rzp)|*.rzp|Všechny soubory (*.*)|*.*";
|
||
|
||
internal static readonly JsonSerializerOptions Opts = new()
|
||
{
|
||
WriteIndented = true,
|
||
// Aby se diakritika v souboru psala čitelně (ě š č…), ne jako \uXXXX.
|
||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Otevřený `.rzp` soubor s exkluzivním zámkem drženým po celou dobu, co je projekt
|
||
/// otevřený. Díky <see cref="FileShare.Read"/> ho ostatní procesy smí jen číst/kopírovat,
|
||
/// ale nejde smazat, přejmenovat ani přepsat – Windows ho hlásí jako „používaný". Ukládání
|
||
/// jde přes držený handle (zámek se neuvolní). Zavře se přes <see cref="Dispose"/> (zavření
|
||
/// projektu/appky); při pádu uvolní zámek operační systém sám.
|
||
/// </summary>
|
||
public sealed class ProjektFileHandle : IDisposable
|
||
{
|
||
private readonly FileStream _stream;
|
||
|
||
/// <summary>Cesta k zamčenému souboru.</summary>
|
||
public string Cesta { get; }
|
||
|
||
private ProjektFileHandle(string cesta, FileStream stream)
|
||
{
|
||
Cesta = cesta;
|
||
_stream = stream;
|
||
}
|
||
|
||
/// <summary>Otevře existující soubor s exkluzivním zámkem a vrátí načtený dokument.
|
||
/// Vyhodí <see cref="IOException"/>, když je soubor otevřený jinde (sdílecí porušení).</summary>
|
||
public static ProjektFileHandle Otevri(string cesta, out ProjektDocument doc)
|
||
{
|
||
var fs = new FileStream(cesta, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
|
||
try
|
||
{
|
||
string text = Cti(fs);
|
||
doc = JsonSerializer.Deserialize<ProjektDocument>(text, ProjektStore.Opts)
|
||
?? throw new InvalidDataException("Soubor projektu je prázdný nebo poškozený.");
|
||
return new ProjektFileHandle(cesta, fs);
|
||
}
|
||
catch
|
||
{
|
||
fs.Dispose(); // při chybě zámek nedržíme
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>Vytvoří/přepíše soubor na nové cestě, zapíše dokument a podrží zámek.
|
||
/// Vyhodí <see cref="IOException"/>, když je cílový soubor otevřený jinde.</summary>
|
||
public static ProjektFileHandle Vytvor(string cesta, ProjektDocument doc)
|
||
{
|
||
var fs = new FileStream(cesta, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||
try
|
||
{
|
||
Zapis(fs, doc);
|
||
return new ProjektFileHandle(cesta, fs);
|
||
}
|
||
catch
|
||
{
|
||
fs.Dispose();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>Přečte soubor jen pro čtení (bez zámku) – i když ho jiný proces drží otevřený.
|
||
/// Žádá pouze <see cref="FileAccess.Read"/> a povoluje <see cref="FileShare.ReadWrite"/>, takže
|
||
/// nekoliduje s cizím zámkem. Handle se nedrží – vrací jen načtený dokument.</summary>
|
||
public static ProjektDocument CtiJenProCteni(string cesta)
|
||
{
|
||
using var fs = new FileStream(cesta, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||
return JsonSerializer.Deserialize<ProjektDocument>(Cti(fs), ProjektStore.Opts)
|
||
?? throw new InvalidDataException("Soubor projektu je prázdný nebo poškozený.");
|
||
}
|
||
|
||
/// <summary>Uloží dokument přes držený zámek (soubor zůstává zamčený).</summary>
|
||
public void Uloz(ProjektDocument doc) => Zapis(_stream, doc);
|
||
|
||
private static void Zapis(FileStream fs, ProjektDocument doc)
|
||
{
|
||
// Serializace musí proběhnout PŘED useknutím souboru – kdyby vyhodila výjimku
|
||
// až po SetLength(0), zůstal by soubor prázdný a projekt by byl nenávratně pryč.
|
||
string json = JsonSerializer.Serialize(doc, ProjektStore.Opts);
|
||
fs.Seek(0, SeekOrigin.Begin);
|
||
fs.SetLength(0);
|
||
using (var w = new StreamWriter(fs, new UTF8Encoding(false), 1024, leaveOpen: true))
|
||
w.Write(json);
|
||
fs.Flush(flushToDisk: true);
|
||
}
|
||
|
||
private static string Cti(FileStream fs)
|
||
{
|
||
fs.Seek(0, SeekOrigin.Begin);
|
||
using var r = new StreamReader(fs, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 1024, leaveOpen: true);
|
||
return r.ReadToEnd();
|
||
}
|
||
|
||
public void Dispose() => _stream.Dispose();
|
||
}
|