Uživatelská data do %APPDATA%\Rozpisky místo vedle .exe

Vše, co píše uživatel, míří přes novou třídu UzivatelskaData do profilu
uživatele: cad.json, layout.json, error.log, tisk_diag.log. Dosud se
zapisovalo vedle .exe, takže .exe v Program Files (složka jen pro čtení)
nebo přeinstalace novou verzí uživatele o data připravily.

Dodávané podklady (šablona, mustr, loga, mapovani.json) se dál čtou vedle
.exe – ty se naopak při každé instalaci obnovují. Hranice mezi tím, co se
instaluje, a tím, co patří uživateli, vede přesně tudy.

Portable režim zůstává: soubor portable.txt vedle .exe vrátí staré chování
(běh z USB, vývoj s jednou složkou).

Migrace: Migrace.PrenesStaraData() při startu přenese cad.json a layout.json
ze staré polohy, pokud v cíli ještě nejsou; originál označí příponou
.migrovano. Nic se nemaže a nic novějšího se nepřepíše.

Číselníky přijdou na řadu samostatně (rozpad na dodávanou a uživatelskou
vrstvu).

Build 0 chyb / 0 varování, 36 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 10:39:25 +02:00
co-authored by Claude Opus 5
parent d4eb3f2102
commit 3c0e30bf35
7 changed files with 145 additions and 26 deletions
+11 -6
View File
@@ -1,4 +1,4 @@
using System.IO;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Threading;
@@ -7,8 +7,9 @@ namespace Rozpisky
{
/// <summary>
/// Globální záchytná síť pro neošetřené výjimky aplikace nesmí spadnout bez stopy.
/// Každá výjimka se zapíše do <c>error.log</c> vedle .exe (stejný portable princip jako
/// <c>cad.json</c>/<c>layout.json</c>) a uživateli se ukáže MessageBox. Výjimky na UI
/// Každá výjimka se zapíše do <c>error.log</c> v uživatelské složce
/// (<see cref="Data.UzivatelskaData"/>, stejně jako <c>cad.json</c>/<c>layout.json</c>)
/// a uživateli se ukáže MessageBox. Výjimky na UI
/// (Dispatcher) vlákně se označí jako zpracované, takže aplikace běží dál rozdělaný
/// projekt zůstane otevřený a jde uložit.
/// </summary>
@@ -18,6 +19,10 @@ namespace Rozpisky
{
base.OnStartup(e);
// Data starších verzí ležela vedle .exe přenést do uživatelské složky dřív,
// než si na ně sáhne kterýkoli store (ten už čte jen nové umístění).
Data.Migrace.PrenesStaraData();
// Úklid diagnostického logu tisku, ať po měsících nenaroste do enormních rozměrů.
Cad.CadTiskService.UklidLog();
@@ -46,14 +51,14 @@ namespace Rozpisky
{
string radek = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {zdroj}: {ex?.ToString() ?? "(neznámá výjimka)"}"
+ Environment.NewLine + Environment.NewLine;
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "error.log"), radek, Encoding.UTF8);
File.AppendAllText(Data.UzivatelskaData.Cesta("error.log"), radek, Encoding.UTF8);
}
catch { /* read-only složka apod. MessageBox se ukáže i tak */ }
catch { /* nezapisovatelná složka apod. MessageBox se ukáže i tak */ }
}
private static void UkazChybu(Exception ex) => MessageBox.Show(
"Nastala neočekávaná chyba, aplikace ale běží dál doporučujeme uložit projekt.\n\n" +
$"{ex.Message}\n\nPodrobnosti jsou v souboru error.log vedle aplikace.",
$"{ex.Message}\n\nPodrobnosti jsou v souboru error.log ve složce\n{Data.UzivatelskaData.Slozka}",
"Neočekávaná chyba", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
+6 -6
View File
@@ -1,4 +1,4 @@
using System.IO;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Data;
@@ -7,9 +7,9 @@ namespace Rozpisky.Cad;
/// <summary>
/// Načítání/ukládání seznamu podporovaných CAD systémů + naposledy vybraného enginu do JSON souboru
/// <b>vedle .exe</b> (portable). Stejný princip jako <see cref="Data.CiselnikyStore"/> /
/// <see cref="Dxf.MapovaniStore"/>: CAD je vlastnost stroje (ne zakázky), takže výběr žije mimo
/// projekt a je globální pro celou instalaci.
/// v <b>uživatelské složce</b> (<see cref="Data.UzivatelskaData"/>). CAD je vlastnost stroje
/// (ne zakázky), takže výběr žije mimo projekt a je společný všem projektům a mimo instalační
/// složku, aby o něj uživatel při přeinstalaci nepřišel.
/// </summary>
public static class CadEnginesStore
{
@@ -21,8 +21,8 @@ public static class CadEnginesStore
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
/// <summary>Výchozí umístění: soubor vedle .exe (portable).</summary>
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
/// <summary>Výchozí umístění: uživatelská složka (viz <see cref="UzivatelskaData"/>).</summary>
public static string DefaultPath => UzivatelskaData.Cesta(SouborNazev);
/// <summary>Uložený výběr CADu: buď předdefinovaný engine (<see cref="VybranyProgId"/>), nebo
/// ruční režim „Manuálně" (<see cref="RucniRezim"/> + <see cref="RucniProgId"/>). Režim a ruční
+5 -4
View File
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -196,14 +196,15 @@ public static class CadTiskService
{
try
{
var cesta = Path.Combine(AppContext.BaseDirectory, "tisk_diag.log");
var cesta = Data.UzivatelskaData.Cesta("tisk_diag.log");
if (File.Exists(cesta) && new FileInfo(cesta).Length > MaxLogBajtu)
File.Delete(cesta);
}
catch { /* read-only složka apod. úklid nesmí přerušit start */ }
}
/// <summary>Diagnostický „chlebníček“ zapíše krok tisku do <c>tisk_diag.log</c> vedle .exe.
/// <summary>Diagnostický „chlebníček“ zapíše krok tisku do <c>tisk_diag.log</c>
/// v uživatelské složce.
/// Slouží k lokalizaci tvrdého pádu CADu (nativní chyba v <c>PlotToFile</c>), který nezanechá
/// stopu v error.log ani v log okně (obojí zmizí s procesem). Zapisuje se synchronně (flush ihned),
/// takže poslední řádek přežije i pád procesu. Diagnostika sama nesmí nic shodit.</summary>
@@ -211,7 +212,7 @@ public static class CadTiskService
{
try
{
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "tisk_diag.log"),
File.AppendAllText(Data.UzivatelskaData.Cesta("tisk_diag.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {krok}{Environment.NewLine}");
}
catch { /* read-only složka apod. nesmí přerušit tisk */ }
+5 -5
View File
@@ -1,4 +1,4 @@
using System.IO;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Data;
@@ -6,8 +6,8 @@ using Rozpisky.Data;
namespace Rozpisky.Cad;
/// <summary>
/// Načítání/ukládání <see cref="LayoutConfig"/> do JSON souboru <b>vedle .exe</b> (portable). Stejný
/// princip jako <see cref="CadEnginesStore"/> konfigurace layoutů/tisku je vlastnost stroje/instalace
/// Načítání/ukládání <see cref="LayoutConfig"/> do JSON souboru v <b>uživatelské složce</b>
/// (<see cref="Data.UzivatelskaData"/>). Stejný princip jako <see cref="CadEnginesStore"/> konfigurace layoutů/tisku je vlastnost stroje/instalace
/// (bloky a hladiny šablony, dostupné tiskárny), needituje se přes UI, jen ručně v souboru.
/// </summary>
public static class LayoutConfigStore
@@ -20,8 +20,8 @@ public static class LayoutConfigStore
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
/// <summary>Výchozí umístění: soubor vedle .exe (portable).</summary>
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
/// <summary>Výchozí umístění: uživatelská složka (viz <see cref="UzivatelskaData"/>).</summary>
public static string DefaultPath => UzivatelskaData.Cesta(SouborNazev);
/// <summary>Načte konfiguraci. Když soubor chybí/je poškozený, vrátí vestavěné výchozí hodnoty
/// (a rovnou je uloží, aby šel soubor dál ručně upravovat).</summary>
+5 -5
View File
@@ -1,12 +1,12 @@
using System.IO;
using System.IO;
using System.Text;
using System.Windows;
namespace Rozpisky.Data;
/// <summary>
/// Společné zacházení s JSON konfiguračními soubory vedle .exe (<c>ciselniky.json</c>,
/// <c>cad.json</c>, <c>layout.json</c>, <c>mapovani.json</c>). Řeší dvě slabiny přímého
/// Společné zacházení s JSON konfiguračními soubory (<c>ciselniky.user.json</c>, <c>cad.json</c>,
/// <c>layout.json</c> v uživatelské složce, <c>mapovani.json</c> vedle .exe). Řeší dvě slabiny přímého
/// <c>File.WriteAllText</c>: pád uprostřed zápisu nesmí poškodit původní soubor (atomický zápis
/// přes .tmp) a poškozený soubor se nesmí tiše zahodit ručně editovaný config s překlepem by se
/// jinak bez varování přepsal výchozími hodnotami a uživatel by přišel o vlastní data.
@@ -41,7 +41,7 @@ public static class JsonConfigFile
(zaloha is null
? "Zálohu původního souboru se nepodařilo vytvořit."
: $"Původní obsah je zálohovaný v:\n{zaloha}") +
"\n\nPodrobnosti jsou v souboru error.log vedle aplikace.";
$"\n\nPodrobnosti jsou v souboru error.log ve složce\n{UzivatelskaData.Slozka}";
// BeginInvoke: hláška se ukáže až po rozběhnutí UI smyčky (config se čte před oknem).
Application.Current?.Dispatcher.BeginInvoke(() =>
MessageBox.Show(zprava, "Poškozený konfigurační soubor",
@@ -54,7 +54,7 @@ public static class JsonConfigFile
{
try
{
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "error.log"),
File.AppendAllText(UzivatelskaData.Cesta("error.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Config: {text}{Environment.NewLine}{Environment.NewLine}",
Encoding.UTF8);
}
+60
View File
@@ -0,0 +1,60 @@
using System.IO;
namespace Rozpisky.Data;
/// <summary>
/// Jednorázový přesun dat starších verzí, které zapisovaly vedle .exe, do uživatelské složky
/// (<see cref="UzivatelskaData"/>). Bez toho by uživatel po aktualizaci našel prázdné číselníky
/// a přenastavený CAD. Volá se jednou při startu, ještě než si na data sáhne kterýkoli store.
/// <para>
/// Zásada: nikdy nic nemazat a nikdy nepřepsat novější data. Soubor se přenese jen tehdy, když
/// v cíli ještě není; originál zůstane ležet jako <c>*.migrovano</c> (jediná kopie uživatelových dat).
/// Selhání nesmí shodit start horší než nepřenesená data je aplikace, která nenaběhne.
/// </para>
/// </summary>
public static class Migrace
{
/// <summary>Přípona, kterou dostane původní soubor po přenesení.</summary>
public const string PriponaHotovo = ".migrovano";
/// <summary>Přenese <c>cad.json</c> a <c>layout.json</c> ze složky aplikace do uživatelské.
/// Číselníky mají vlastní cestu (rozpad na vrstvy) viz <see cref="CiselnikyStore"/>.</summary>
public static void PrenesStaraData()
{
Prenes(CadEnginesStoreNazev);
Prenes(LayoutConfigStoreNazev);
}
private const string CadEnginesStoreNazev = "cad.json";
private const string LayoutConfigStoreNazev = "layout.json";
/// <summary>Přesune soubor ze složky aplikace do uživatelské, pokud tam ještě není.
/// V portable režimu jsou obě složky totožné a neudělá se nic.</summary>
private static void Prenes(string nazevSouboru)
{
try
{
var zdroj = Path.Combine(AppContext.BaseDirectory, nazevSouboru);
var cil = UzivatelskaData.Cesta(nazevSouboru);
if (string.Equals(zdroj, cil, StringComparison.OrdinalIgnoreCase)) return;
if (!File.Exists(zdroj) || File.Exists(cil)) return;
File.Copy(zdroj, cil);
OznacJakoPrenesene(zdroj);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Nezapisovatelná instalační složka apod. data zůstanou tam, kde jsou, a zkusí se
// přenést zase příště. Start aplikace to zastavit nesmí.
}
}
/// <summary>Přejmenuje původní soubor na <c>*.migrovano</c>, aby se přenos neopakoval a aby
/// starší verze programu (kdyby ji uživatel vrátil) nezmátla už neaktuální data. Nedaří-li se
/// to (soubor jen pro čtení), nevadí kopie v cíli už existuje a další běh přenos přeskočí.</summary>
internal static void OznacJakoPrenesene(string cesta)
{
try { File.Move(cesta, cesta + PriponaHotovo, overwrite: true); }
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { }
}
}
+53
View File
@@ -0,0 +1,53 @@
using System.IO;
namespace Rozpisky.Data;
/// <summary>
/// Jediné místo pro data, která píše <b>uživatel</b> (číselníky, výběr CADu, konfigurace layoutů,
/// logy). Leží v profilu uživatele (<c>%APPDATA%\Rozpisky</c>), ne vedle .exe přeinstalace novou
/// verzí ani .exe v <c>Program Files</c> (jen pro čtení) je tak nemůžou zničit.
/// <para>
/// Podklady <b>dodávané</b> s programem (šablona, mustr, loga, výchozí číselníky, mapování) zůstávají
/// vedle .exe a čtou se přes <see cref="AppContext.BaseDirectory"/> ty se naopak při každé instalaci
/// obnovují. Hranice mezi tím, co se instaluje, a tím, co patří uživateli, vede přesně tudy.
/// </para>
/// <para>
/// <b>Portable režim:</b> leží-li vedle .exe soubor <c>portable.txt</c>, vrátí se složka aplikace.
/// Slouží k běhu z USB a k vývoji, kdy je pohodlnější mít všechno v jedné složce s buildem.
/// </para>
/// </summary>
public static class UzivatelskaData
{
/// <summary>Značka portable režimu vedle .exe (obsah souboru se neřeší, stačí že existuje).</summary>
public const string PortableZnacka = "portable.txt";
private const string NazevSlozky = "Rozpisky";
private static string? _slozka;
/// <summary>Složka pro zapisovaná data. Počítá se jednou za běh a při prvním použití se vytvoří.
/// Když se vytvořit nepodaří (výjimečné profil uživatele je zapisovatelný), spadne se zpátky
/// na složku aplikace, ať se program aspoň spustí.</summary>
public static string Slozka => _slozka ??= UrciSlozku();
/// <summary>Plná cesta k souboru se zapisovanými daty.</summary>
public static string Cesta(string nazevSouboru) => Path.Combine(Slozka, nazevSouboru);
private static string UrciSlozku()
{
var vedleExe = AppContext.BaseDirectory;
if (File.Exists(Path.Combine(vedleExe, PortableZnacka))) return vedleExe;
try
{
var slozka = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), NazevSlozky);
Directory.CreateDirectory(slozka);
return slozka;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return vedleExe;
}
}
}