From 3c0e30bf35034fb0ce64d1401e506d61766e6728 Mon Sep 17 00:00:00 2001 From: Josef Marek Date: Mon, 7 Sep 2026 10:39:25 +0200 Subject: [PATCH] =?UTF-8?q?U=C5=BEivatelsk=C3=A1=20data=20do=20%APPDATA%\R?= =?UTF-8?q?ozpisky=20m=C3=ADsto=20vedle=20.exe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Rozpisky/App.xaml.cs | 17 +++++---- Rozpisky/Cad/CadEnginesStore.cs | 12 +++---- Rozpisky/Cad/CadTiskService.cs | 9 ++--- Rozpisky/Cad/LayoutConfigStore.cs | 10 +++--- Rozpisky/Data/JsonConfigFile.cs | 10 +++--- Rozpisky/Data/Migrace.cs | 60 +++++++++++++++++++++++++++++++ Rozpisky/Data/UzivatelskaData.cs | 53 +++++++++++++++++++++++++++ 7 files changed, 145 insertions(+), 26 deletions(-) create mode 100644 Rozpisky/Data/Migrace.cs create mode 100644 Rozpisky/Data/UzivatelskaData.cs diff --git a/Rozpisky/App.xaml.cs b/Rozpisky/App.xaml.cs index 38960aa..4e7e9b9 100644 --- a/Rozpisky/App.xaml.cs +++ b/Rozpisky/App.xaml.cs @@ -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 { /// /// Globální záchytná síť pro neošetřené výjimky – aplikace nesmí spadnout bez stopy. - /// Každá výjimka se zapíše do error.log vedle .exe (stejný portable princip jako - /// cad.json/layout.json) a uživateli se ukáže MessageBox. Výjimky na UI + /// Každá výjimka se zapíše do error.log v uživatelské složce + /// (, stejně jako cad.json/layout.json) + /// 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. /// @@ -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); } } diff --git a/Rozpisky/Cad/CadEnginesStore.cs b/Rozpisky/Cad/CadEnginesStore.cs index bac688f..b42f2c5 100644 --- a/Rozpisky/Cad/CadEnginesStore.cs +++ b/Rozpisky/Cad/CadEnginesStore.cs @@ -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; /// /// Načítání/ukládání seznamu podporovaných CAD systémů + naposledy vybraného enginu do JSON souboru -/// vedle .exe (portable). Stejný princip jako / -/// : CAD je vlastnost stroje (ne zakázky), takže výběr žije mimo -/// projekt a je globální pro celou instalaci. +/// v uživatelské složce (). 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. /// public static class CadEnginesStore { @@ -21,8 +21,8 @@ public static class CadEnginesStore Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; - /// Výchozí umístění: soubor vedle .exe (portable). - public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev); + /// Výchozí umístění: uživatelská složka (viz ). + public static string DefaultPath => UzivatelskaData.Cesta(SouborNazev); /// Uložený výběr CADu: buď předdefinovaný engine (), nebo /// ruční režim „Manuálně" ( + ). Režim a ruční diff --git a/Rozpisky/Cad/CadTiskService.cs b/Rozpisky/Cad/CadTiskService.cs index 20c40b1..001a1ce 100644 --- a/Rozpisky/Cad/CadTiskService.cs +++ b/Rozpisky/Cad/CadTiskService.cs @@ -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 */ } } - /// Diagnostický „chlebníček“ – zapíše krok tisku do tisk_diag.log vedle .exe. + /// Diagnostický „chlebníček“ – zapíše krok tisku do tisk_diag.log + /// v uživatelské složce. /// Slouží k lokalizaci tvrdého pádu CADu (nativní chyba v PlotToFile), 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. @@ -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 */ } diff --git a/Rozpisky/Cad/LayoutConfigStore.cs b/Rozpisky/Cad/LayoutConfigStore.cs index 106bd1f..693b88f 100644 --- a/Rozpisky/Cad/LayoutConfigStore.cs +++ b/Rozpisky/Cad/LayoutConfigStore.cs @@ -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; /// -/// Načítání/ukládání do JSON souboru vedle .exe (portable). Stejný -/// princip jako – konfigurace layoutů/tisku je vlastnost stroje/instalace +/// Načítání/ukládání do JSON souboru v uživatelské složce +/// (). Stejný princip jako – konfigurace layoutů/tisku je vlastnost stroje/instalace /// (bloky a hladiny šablony, dostupné tiskárny), needituje se přes UI, jen ručně v souboru. /// public static class LayoutConfigStore @@ -20,8 +20,8 @@ public static class LayoutConfigStore Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; - /// Výchozí umístění: soubor vedle .exe (portable). - public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev); + /// Výchozí umístění: uživatelská složka (viz ). + public static string DefaultPath => UzivatelskaData.Cesta(SouborNazev); /// 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). diff --git a/Rozpisky/Data/JsonConfigFile.cs b/Rozpisky/Data/JsonConfigFile.cs index a7b977b..c649c16 100644 --- a/Rozpisky/Data/JsonConfigFile.cs +++ b/Rozpisky/Data/JsonConfigFile.cs @@ -1,12 +1,12 @@ -using System.IO; +using System.IO; using System.Text; using System.Windows; namespace Rozpisky.Data; /// -/// Společné zacházení s JSON konfiguračními soubory vedle .exe (ciselniky.json, -/// cad.json, layout.json, mapovani.json). Řeší dvě slabiny přímého +/// Společné zacházení s JSON konfiguračními soubory (ciselniky.user.json, cad.json, +/// layout.json v uživatelské složce, mapovani.json vedle .exe). Řeší dvě slabiny přímého /// File.WriteAllText: 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); } diff --git a/Rozpisky/Data/Migrace.cs b/Rozpisky/Data/Migrace.cs new file mode 100644 index 0000000..470fb1c --- /dev/null +++ b/Rozpisky/Data/Migrace.cs @@ -0,0 +1,60 @@ +using System.IO; + +namespace Rozpisky.Data; + +/// +/// Jednorázový přesun dat starších verzí, které zapisovaly vedle .exe, do uživatelské složky +/// (). 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. +/// +/// 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 *.migrovano (jediná kopie uživatelových dat). +/// Selhání nesmí shodit start – horší než nepřenesená data je aplikace, která nenaběhne. +/// +/// +public static class Migrace +{ + /// Přípona, kterou dostane původní soubor po přenesení. + public const string PriponaHotovo = ".migrovano"; + + /// Přenese cad.json a layout.json ze složky aplikace do uživatelské. + /// Číselníky mají vlastní cestu (rozpad na vrstvy) – viz . + public static void PrenesStaraData() + { + Prenes(CadEnginesStoreNazev); + Prenes(LayoutConfigStoreNazev); + } + + private const string CadEnginesStoreNazev = "cad.json"; + private const string LayoutConfigStoreNazev = "layout.json"; + + /// 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. + 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í. + } + } + + /// Přejmenuje původní soubor na *.migrovano, 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čí. + internal static void OznacJakoPrenesene(string cesta) + { + try { File.Move(cesta, cesta + PriponaHotovo, overwrite: true); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { } + } +} diff --git a/Rozpisky/Data/UzivatelskaData.cs b/Rozpisky/Data/UzivatelskaData.cs new file mode 100644 index 0000000..39ce29d --- /dev/null +++ b/Rozpisky/Data/UzivatelskaData.cs @@ -0,0 +1,53 @@ +using System.IO; + +namespace Rozpisky.Data; + +/// +/// Jediné místo pro data, která píše uživatel (číselníky, výběr CADu, konfigurace layoutů, +/// logy). Leží v profilu uživatele (%APPDATA%\Rozpisky), ne vedle .exe – přeinstalace novou +/// verzí ani .exe v Program Files (jen pro čtení) je tak nemůžou zničit. +/// +/// Podklady dodávané s programem (šablona, mustr, loga, výchozí číselníky, mapování) zůstávají +/// vedle .exe a čtou se přes – 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: leží-li vedle .exe soubor portable.txt, 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. +/// +/// +public static class UzivatelskaData +{ + /// Značka portable režimu vedle .exe (obsah souboru se neřeší, stačí že existuje). + public const string PortableZnacka = "portable.txt"; + + private const string NazevSlozky = "Rozpisky"; + + private static string? _slozka; + + /// 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í. + public static string Slozka => _slozka ??= UrciSlozku(); + + /// Plná cesta k souboru se zapisovanými daty. + 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; + } + } +}