From bbf466c83b92e1a4d7453094145eb28c2a0c2523 Mon Sep 17 00:00:00 2001 From: phantomix Date: Wed, 27 Nov 2019 19:03:19 +0100 Subject: [PATCH 1/5] 191127PX Added Process log window with HelloWorld background process --- Program.cs | 8 +- dezentrale-members.csproj | 1 + view/frmMain.cs | 2 + view/frmProcessWithLog.cs | 199 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 view/frmProcessWithLog.cs diff --git a/Program.cs b/Program.cs index ea37c34..c06cfc9 100644 --- a/Program.cs +++ b/Program.cs @@ -16,8 +16,14 @@ TODO - FormMail: Add mail for automatic member type setting to "Foerdermitglied" - FormMail: Use manual type changing - FormMail: Cronjob found unassigned MoneyTransfers +- frmProcessWithLog: Implementation for non-immediate GUI actions (process csv, cronjob, import/export) with background thread +- frmEditEntry: Optional (checkbox) request for a comment on data changes, when hitting OK +- frmMain: Indicator that the data was modified after import + messagebox to remind user to export on quitting. +- frmMain: Member List: Column "monthly fee", Column "Last payment", disabled by default - Configuration window: MoneyTransferRegEx - Bug: Generating testdata doesn't remove old xml files, thus the memberlist will be mixed after next restart +- Bug: Database import fails on some PCs (seems that writing xml files during decompression is problematic) +- Bug: Member list not reloaded after ProcessCSV (balance display is wrong) - CronjobConfig - CustomListView: implement generic filtering */ @@ -25,7 +31,7 @@ namespace dezentrale { public class Program { - public static uint VersionNumber { get; private set; } = 0x19111000; + public static uint VersionNumber { get; private set; } = 0x19112700; public static string VersionString { get; private set; } = $"{VersionNumber:x}"; public static string AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); diff --git a/dezentrale-members.csproj b/dezentrale-members.csproj index 88474dc..cc4079f 100644 --- a/dezentrale-members.csproj +++ b/dezentrale-members.csproj @@ -182,6 +182,7 @@ Component + diff --git a/view/frmMain.cs b/view/frmMain.cs index 1f7cb62..3b95619 100644 --- a/view/frmMain.cs +++ b/view/frmMain.cs @@ -22,6 +22,8 @@ namespace dezentrale.view public frmMain() { + frmProcessWithLog p = new frmProcessWithLog(new HelloWorldProcess(), false); + p.Show(); this.SuspendLayout(); this.Text = $"dezentrale-members Version {Program.VersionString}"; this.StartPosition = FormStartPosition.CenterScreen; diff --git a/view/frmProcessWithLog.cs b/view/frmProcessWithLog.cs new file mode 100644 index 0000000..f6bc4f6 --- /dev/null +++ b/view/frmProcessWithLog.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading; +using System.Windows.Forms; + +namespace dezentrale.view +{ + public enum ELogLevel + { + Trace = -1, + Debug = 0, + Info = 1, + Warning = 2, + Error = 3, + } + + public interface ILogger + { + void Clear(); + void LogLine(string text, ELogLevel logLevel = ELogLevel.Info, string module = ""); + } + + public interface IProcessController : ILogger + { + void ActionCompleted(bool success); + } + + public abstract class BackgroundProcess + { + public IProcessController LogTarget { get; set; } = null; + public bool CancelButton { get; protected set; } = false; + public string Caption { get; protected set; } = "BackgroundProcess"; + + protected abstract void Run(); + + private Thread thread = null; + public bool StartRun() + { + if (thread != null) return false; + thread = new Thread(Run); + thread.IsBackground = true; + thread.Start(); + return true; + } + } + public class HelloWorldProcess : BackgroundProcess + { + public HelloWorldProcess() + { + Caption = "Hello World"; + } + protected override void Run() + { + for(int i = 0; i < 20; i++) + { + if ((i & 0x01) == 0) LogTarget.LogLine("Hello", ELogLevel.Info, "HelloWorldProcess"); else LogTarget.LogLine("World", ELogLevel.Info, "HelloWorldProcess"); + Thread.Sleep(200); + } + LogTarget.ActionCompleted(true); + } + } + public class frmProcessWithLog : Form, IProcessController + { + private TextBox tbLog = null; + private PictureBox picStatus = null; + private Button btnStart = null; + private Button btnCancel = null; + private Button btnClose = null; + + private BackgroundProcess process; + public void Clear() + { + tbLog.Invoke(new Action(() => tbLog.Clear())); + Console.Clear(); + } + + private void printline(string line) + { + tbLog.Invoke(new Action(() => tbLog.AppendText(line + "\r\n"))); + Console.WriteLine(line); + } + public void LogLine(string text, ELogLevel logLevel, string module) + { + string output = $"{DateTime.Now} [{module}] {logLevel} {text}"; + printline(output); + } + + public frmProcessWithLog(BackgroundProcess process, bool autoRun = false) + { + this.process = process; + this.process.LogTarget = this; + this.Text = $"Process - {this.process.Caption} (waiting)"; + this.Width = 800; + this.Height = 600; + + // build GUI + //[TextBox Multiline ] + //[SuccessImg] [Start] [Cancel] [Close] + + { + tbLog = new TextBox() + { + Location = new System.Drawing.Point(5, 5), + Size = new Size(this.Width - 10, this.Height - 130), + Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Multiline = true, + ScrollBars = ScrollBars.Both, + }; + this.Controls.Add(tbLog); + } + + { + picStatus = new PictureBox() + { + Location = new System.Drawing.Point(5, this.Height - 87), + Size = new Size(16, 16), + Anchor = AnchorStyles.Left | AnchorStyles.Bottom, + BackColor = Color.White, + }; + this.Controls.Add(picStatus); + } + + if (!autoRun) + { + btnStart = new Button() + { + Text = "Start", + Location = new System.Drawing.Point(this.Width - 170, this.Height - 87), + Anchor = AnchorStyles.Right | AnchorStyles.Bottom, + Enabled = true, + }; + btnStart.Height += 2; + btnStart.Click += btnStart_Click; + this.Controls.Add(btnStart); + } + + if (this.process.CancelButton) + { + btnCancel = new Button() + { + Text = "Cancel", + Location = new System.Drawing.Point(this.Width - 85, this.Height - 87), + Anchor = AnchorStyles.Right | AnchorStyles.Bottom, + Enabled = false, + }; + btnCancel.Height += 2; + btnCancel.Click += btnCancel_Click; + this.Controls.Add(btnCancel); + } + + { + btnClose = new Button() + { + Text = "Close", + Location = new System.Drawing.Point(this.Width - 85, this.Height - 87), + Anchor = AnchorStyles.Right | AnchorStyles.Bottom, + Enabled = true, + }; + btnClose.Height += 2; + btnClose.Click += btnClose_Click; + this.Controls.Add(btnClose); + } + + if (autoRun) + btnStart_Click(null,null); + } + + private void btnStart_Click(object sender, EventArgs e) + { + if (btnStart != null) btnStart.Visible = false; + if (btnCancel != null) btnCancel.Enabled = true; + btnClose.Enabled = false; + this.Text = $"Process - {this.process.Caption} (running)"; + + process.StartRun(); + } + private void btnCancel_Click(object sender, EventArgs e) + { + if (btnCancel != null) btnCancel.Enabled = false; + this.Text = $"Process - {this.process.Caption} (cancelled)"; + + } + private void btnClose_Click(object sender, EventArgs e) + { + this.Close(); + } + + public void ActionCompleted(bool success) + { + if (btnCancel != null) btnCancel.Visible = false; + btnClose.Invoke(new Action(() => btnClose.Enabled = true )); + picStatus.Invoke(new Action(() => picStatus.BackColor = (success ? Color.Green : Color.Red))); + this.Invoke(new Action(() => this.Text = $"Process - {this.process.Caption}" + (success ? " (done)" : " (failed)"))); + } + } +} From 575279e0c2a263f7ab9cb62a312114d25f61392e Mon Sep 17 00:00:00 2001 From: phantomix Date: Fri, 29 Nov 2019 14:46:06 +0100 Subject: [PATCH 2/5] 191129PX Added steps to background processes, started implementing database import process --- model/MemberImportExport.cs | 63 +++++++++++---- view/CustomListView.cs | 11 ++- view/frmProcessWithLog.cs | 149 ++++++++++++++++++++---------------- 3 files changed, 143 insertions(+), 80 deletions(-) diff --git a/model/MemberImportExport.cs b/model/MemberImportExport.cs index c6b9db3..92a62a6 100644 --- a/model/MemberImportExport.cs +++ b/model/MemberImportExport.cs @@ -6,20 +6,56 @@ using System.Diagnostics; namespace dezentrale.model { + public class ImportProcess : view.BackgroundProcess + { + + public string InputDir { get; set; } = null; + public ImportProcess() + { + Caption = "Database import"; + Steps = 20; + } + protected override bool Run() + { + if (InputDir == null) + { + LogTarget.LogLine("Input directory not set.", view.ELogLevel.Error, "ImportProcess"); + return false; + } + + /*if (HgEnabled) + { + RunProcess("hg", "init", inputDir); + RunProcess("hg", "update null", inputDir); + if (!RunProcess("hg", $"--config auth.rc.prefix={HgURL} --config auth.rc.username={HgUserName} --config auth.rc.password={HgPassword} pull {HgURL}", inputDir)) + return false; + if (!RunProcess("hg", $"update", inputDir)) + return false; + } + if (GpgEnabled) + { + Console.WriteLine($"Import: Using GPG to decrypt {Path.Combine(inputDir, GpgFile)}"); + if (!Gpg($"--output {Path.Combine(inputDir, ZipFile)} --decrypt {Path.Combine(inputDir, GpgFile)}")) return false; + } + Console.WriteLine($"Import: Extracting {Path.Combine(inputDir, ZipFile)}"); + if (ZipImport(memberDirectory, inputDir) != true) return false;*/ + return true; + } + } public class MemberImportExport { - [XmlElement] public string ZipFile { get; set; } = "fnord.zip"; - [XmlElement] public string ZipPassword { get; set; } = ""; - [XmlElement] public bool GpgEnabled { get; set; } = true; - [XmlElement] public string GpgFile { get; set; } = "fnord.gpg"; - [XmlElement] public string GpgPassword { get; set; } = "fnord"; - [XmlElement] public bool HgEnabled { get; set; } = false; - [XmlElement] public string HgUserName { get; set; } = ""; - [XmlElement] public string HgPassword { get; set; } = ""; - [XmlElement] public string HgURL { get; set; } = ""; - [XmlElement] public bool GitEnabled { get; set; } = false; - [XmlElement] public string GitUserName { get; set; } = ""; - [XmlElement] public string GitPassword { get; set; } = ""; + [XmlElement] public string ZipFile { get; set; } = "fnord.zip"; + [XmlElement] public string ZipPassword { get; set; } = ""; + [XmlElement] public bool GpgEnabled { get; set; } = true; + [XmlElement] public string GpgFile { get; set; } = "fnord.gpg"; + [XmlElement] public string GpgPassword { get; set; } = "fnord"; + [XmlElement] public bool HgEnabled { get; set; } = false; + [XmlElement] public string HgUserName { get; set; } = ""; + [XmlElement] public string HgPassword { get; set; } = ""; + [XmlElement] public string HgURL { get; set; } = ""; + [XmlElement] public bool GitEnabled { get; set; } = false; + [XmlElement] public string GitUserName { get; set; } = ""; + [XmlElement] public string GitPassword { get; set; } = ""; @@ -55,7 +91,7 @@ namespace dezentrale.model Console.WriteLine(); } return p.ExitCode == 0; - } catch(Exception ex) + } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } @@ -97,6 +133,7 @@ namespace dezentrale.model return true; } +[Obsolete] public bool Import(string memberDirectory, string inputDir) { if(HgEnabled) diff --git a/view/CustomListView.cs b/view/CustomListView.cs index 0e55fe6..d6e44ab 100644 --- a/view/CustomListView.cs +++ b/view/CustomListView.cs @@ -29,8 +29,9 @@ namespace dezentrale.view //This is a workaround for checkboxes keep changing when MultiSelect with Shift if ((ModifierKeys & (Keys.Shift | Keys.Control)) != 0) e.NewValue = e.CurrentValue; - }; - + }; + + MenuItem filterMenu = cm.MenuItems.Add("Filter by column..."); foreach (ConfigLVDataHandler col in actualColumns) { if(col.FilterAvailable) @@ -39,6 +40,12 @@ namespace dezentrale.view } if (col.Visible) this.Columns.Add(new ColumnHeader() { Width = col.Width, Text = col.Display, TextAlign = col.TextAlign, Tag = col, }); } + if(filterMenu.MenuItems.Count < 1) + { + MenuItem unavailable = new MenuItem("unavailable"); + unavailable.Enabled = false; + filterMenu.MenuItems.Add(unavailable); + } } public CustomListView(List actualColumns, EventHandler ColumnsChanged = null, bool checkBoxes = true, bool columnConfiguration = true) : base() { diff --git a/view/frmProcessWithLog.cs b/view/frmProcessWithLog.cs index f6bc4f6..db04616 100644 --- a/view/frmProcessWithLog.cs +++ b/view/frmProcessWithLog.cs @@ -26,6 +26,8 @@ namespace dezentrale.view public interface IProcessController : ILogger { void ActionCompleted(bool success); + void StepStarted(uint stepNumber = 0, string stepDescription = "Generic"); + void StepCompleted(uint stepNumber = 0, string stepDescription = "Generic", bool success = true); } public abstract class BackgroundProcess @@ -33,38 +35,48 @@ namespace dezentrale.view public IProcessController LogTarget { get; set; } = null; public bool CancelButton { get; protected set; } = false; public string Caption { get; protected set; } = "BackgroundProcess"; + public uint Steps { get; protected set; } = 1; - protected abstract void Run(); + protected abstract bool Run(); private Thread thread = null; public bool StartRun() { if (thread != null) return false; - thread = new Thread(Run); - thread.IsBackground = true; + thread = new Thread(RunAbstract) { IsBackground = true }; thread.Start(); return true; } + private void RunAbstract() + { + LogTarget.ActionCompleted(Run()); + } } public class HelloWorldProcess : BackgroundProcess { public HelloWorldProcess() { Caption = "Hello World"; + Steps = 20; } - protected override void Run() + protected override bool Run() { - for(int i = 0; i < 20; i++) + for(uint i = 0; i < 20; i++) { - if ((i & 0x01) == 0) LogTarget.LogLine("Hello", ELogLevel.Info, "HelloWorldProcess"); else LogTarget.LogLine("World", ELogLevel.Info, "HelloWorldProcess"); + string stepName = ((i & 0x01) == 0) ? "Hello" : "World"; + LogTarget.StepStarted(i, stepName); + LogTarget.LogLine(stepName, ELogLevel.Info, "HelloWorldProcess"); + Thread.Sleep(200); + LogTarget.StepCompleted(i, stepName, (i & 0x01) == 0); Thread.Sleep(200); } - LogTarget.ActionCompleted(true); + return true; } } public class frmProcessWithLog : Form, IProcessController { private TextBox tbLog = null; + private Label lblStepStatus = null; private PictureBox picStatus = null; private Button btnStart = null; private Button btnCancel = null; @@ -98,71 +110,70 @@ namespace dezentrale.view // build GUI //[TextBox Multiline ] + //[Step display label ] //[SuccessImg] [Start] [Cancel] [Close] + tbLog = new TextBox() { - tbLog = new TextBox() - { - Location = new System.Drawing.Point(5, 5), - Size = new Size(this.Width - 10, this.Height - 130), - Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, - Multiline = true, - ScrollBars = ScrollBars.Both, - }; - this.Controls.Add(tbLog); - } + Location = new System.Drawing.Point(5, 5), + Size = new Size(this.Width - 15, this.Height - 95), + Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Multiline = true, + ScrollBars = ScrollBars.Both, + }; + this.Controls.Add(tbLog); + picStatus = new PictureBox() { - picStatus = new PictureBox() - { - Location = new System.Drawing.Point(5, this.Height - 87), - Size = new Size(16, 16), - Anchor = AnchorStyles.Left | AnchorStyles.Bottom, - BackColor = Color.White, - }; - this.Controls.Add(picStatus); - } + Location = new System.Drawing.Point(5, this.Height - 80), + Size = new Size(48, 48), + Anchor = AnchorStyles.Left | AnchorStyles.Bottom, + BackColor = Color.White, + }; + this.Controls.Add(picStatus); - if (!autoRun) + lblStepStatus = new Label() { - btnStart = new Button() - { - Text = "Start", - Location = new System.Drawing.Point(this.Width - 170, this.Height - 87), - Anchor = AnchorStyles.Right | AnchorStyles.Bottom, - Enabled = true, - }; - btnStart.Height += 2; - btnStart.Click += btnStart_Click; - this.Controls.Add(btnStart); - } + Location = new System.Drawing.Point(60, this.Height - 80), + AutoSize = false, + Size = new Size(this.Width - 10, 20), + Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Text = "Ready.", + }; + this.Controls.Add(lblStepStatus); - if (this.process.CancelButton) + btnStart = new Button() { - btnCancel = new Button() - { - Text = "Cancel", - Location = new System.Drawing.Point(this.Width - 85, this.Height - 87), - Anchor = AnchorStyles.Right | AnchorStyles.Bottom, - Enabled = false, - }; - btnCancel.Height += 2; - btnCancel.Click += btnCancel_Click; - this.Controls.Add(btnCancel); - } + Text = "Start", + Location = new System.Drawing.Point(this.Width - 260, this.Height - 55), + Anchor = AnchorStyles.Right | AnchorStyles.Bottom, + Enabled = !autoRun, + }; + btnStart.Height += 2; + btnStart.Click += btnStart_Click; + this.Controls.Add(btnStart); + btnCancel = new Button() { - btnClose = new Button() - { - Text = "Close", - Location = new System.Drawing.Point(this.Width - 85, this.Height - 87), - Anchor = AnchorStyles.Right | AnchorStyles.Bottom, - Enabled = true, - }; - btnClose.Height += 2; - btnClose.Click += btnClose_Click; - this.Controls.Add(btnClose); - } + Text = "Cancel", + Location = new System.Drawing.Point(this.Width - 175, this.Height - 55), + Anchor = AnchorStyles.Right | AnchorStyles.Bottom, + Enabled = false, + }; + btnCancel.Height += 2; + btnCancel.Click += btnCancel_Click; + this.Controls.Add(btnCancel); + + btnClose = new Button() + { + Text = "Close", + Location = new System.Drawing.Point(this.Width - 90, this.Height - 55), + Anchor = AnchorStyles.Right | AnchorStyles.Bottom, + Enabled = true, + }; + btnClose.Height += 2; + btnClose.Click += btnClose_Click; + this.Controls.Add(btnClose); if (autoRun) btnStart_Click(null,null); @@ -170,8 +181,8 @@ namespace dezentrale.view private void btnStart_Click(object sender, EventArgs e) { - if (btnStart != null) btnStart.Visible = false; - if (btnCancel != null) btnCancel.Enabled = true; + btnStart.Enabled = false; + btnCancel.Enabled = true; btnClose.Enabled = false; this.Text = $"Process - {this.process.Caption} (running)"; @@ -179,7 +190,7 @@ namespace dezentrale.view } private void btnCancel_Click(object sender, EventArgs e) { - if (btnCancel != null) btnCancel.Enabled = false; + btnCancel.Enabled = false; this.Text = $"Process - {this.process.Caption} (cancelled)"; } @@ -188,9 +199,17 @@ namespace dezentrale.view this.Close(); } + public void StepStarted(uint stepNumber, string stepDescription) + { + lblStepStatus.Invoke(new Action(() => lblStepStatus.Text = $"# {stepNumber}/{this.process.Steps} : {stepDescription}")); + } + public void StepCompleted(uint stepNumber, string stepDescription, bool success) + { + lblStepStatus.Invoke(new Action(() => lblStepStatus.Text = $"# {stepNumber}/{this.process.Steps} : Completed({success}) {stepDescription}")); + } public void ActionCompleted(bool success) { - if (btnCancel != null) btnCancel.Visible = false; + btnCancel.Enabled = false; btnClose.Invoke(new Action(() => btnClose.Enabled = true )); picStatus.Invoke(new Action(() => picStatus.BackColor = (success ? Color.Green : Color.Red))); this.Invoke(new Action(() => this.Text = $"Process - {this.process.Caption}" + (success ? " (done)" : " (failed)"))); From 7a65a110fcd58f28059431e310d9cf9b81bf77a3 Mon Sep 17 00:00:00 2001 From: phantomix Date: Mon, 2 Dec 2019 13:15:33 +0100 Subject: [PATCH 3/5] 191201PX Refactoring database import to use process window/logging; unfinished yet. --- Program.cs | 2 +- model/LogEvent.cs | 11 ++ model/MemberImportExport.cs | 209 +++++++++++++++++++++++++++++------- view/frmMain.cs | 19 +++- view/frmProcessWithLog.cs | 64 +++++++---- 5 files changed, 245 insertions(+), 60 deletions(-) diff --git a/Program.cs b/Program.cs index c06cfc9..9c5fdbd 100644 --- a/Program.cs +++ b/Program.cs @@ -31,7 +31,7 @@ namespace dezentrale { public class Program { - public static uint VersionNumber { get; private set; } = 0x19112700; + public static uint VersionNumber { get; private set; } = 0x19120100; public static string VersionString { get; private set; } = $"{VersionNumber:x}"; public static string AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); diff --git a/model/LogEvent.cs b/model/LogEvent.cs index f3625c1..7b66ce9 100644 --- a/model/LogEvent.cs +++ b/model/LogEvent.cs @@ -27,8 +27,19 @@ namespace dezentrale.model EMail, + // TODO: [Obsolete("Use ELogLevel.Error instead")] Error, + } + + public enum ELogLevel + { + Trace = -1, + Debug = 0, + Info = 1, + Warning = 2, + Error = 3, } + [XmlAttribute] public DateTime Timestamp { get; set; } = DateTime.Now; [XmlAttribute] public string LocalUser { get; set; } = Program.config.LocalUser; [XmlElement("SubEvent")] public List SubEvents { get; set; } = new List(); diff --git a/model/MemberImportExport.cs b/model/MemberImportExport.cs index 92a62a6..89f11ef 100644 --- a/model/MemberImportExport.cs +++ b/model/MemberImportExport.cs @@ -8,39 +8,156 @@ namespace dezentrale.model { public class ImportProcess : view.BackgroundProcess { - + public MemberImportExport MemberImportSettings { get; set; } = null; + public string MemberDir { get; set; } = null; public string InputDir { get; set; } = null; public ImportProcess() { Caption = "Database import"; - Steps = 20; + Steps = 4; } protected override bool Run() - { - if (InputDir == null) - { - LogTarget.LogLine("Input directory not set.", view.ELogLevel.Error, "ImportProcess"); + { + + LogTarget.StepStarted(0, "Preparing import"); + + if (MemberImportSettings == null) + { + LogTarget.LogLine("MemberImportExport data class not set.", LogEvent.ELogLevel.Error, "ImportProcess"); return false; } + if (MemberDir == null) + { + LogTarget.LogLine("Member directory not set.", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + if (InputDir == null) + { + LogTarget.LogLine("Input directory not set.", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } - /*if (HgEnabled) + + + + + LogTarget.StepStarted(1, "Fetching repository"); + + if (MemberImportSettings.HgEnabled) { - RunProcess("hg", "init", inputDir); - RunProcess("hg", "update null", inputDir); - if (!RunProcess("hg", $"--config auth.rc.prefix={HgURL} --config auth.rc.username={HgUserName} --config auth.rc.password={HgPassword} pull {HgURL}", inputDir)) + MemberImportExport.RunProcess("hg", "init", InputDir, LogTarget); + MemberImportExport.RunProcess("hg", "update null", InputDir, LogTarget); + if (!MemberImportExport.RunProcess("hg", $"--config auth.rc.prefix={MemberImportSettings.HgURL} --config auth.rc.username={MemberImportSettings.HgUserName} --config auth.rc.password={MemberImportSettings.HgPassword} pull {MemberImportSettings.HgURL}", InputDir, LogTarget)) return false; - if (!RunProcess("hg", $"update", inputDir)) + if (!MemberImportExport.RunProcess("hg", $"update", InputDir, LogTarget)) return false; } - if (GpgEnabled) + if (MemberImportSettings.GpgEnabled) { - Console.WriteLine($"Import: Using GPG to decrypt {Path.Combine(inputDir, GpgFile)}"); - if (!Gpg($"--output {Path.Combine(inputDir, ZipFile)} --decrypt {Path.Combine(inputDir, GpgFile)}")) return false; + Console.WriteLine($"Import: Using GPG to decrypt {Path.Combine(InputDir, MemberImportSettings.GpgFile)}"); + if (!MemberImportExport.Gpg($"--output {Path.Combine(InputDir, MemberImportSettings.ZipFile)} --decrypt {Path.Combine(InputDir, MemberImportSettings.GpgFile)}", MemberImportSettings.GpgPassword)) return false; + } + if (!Directory.Exists(MemberDir)) + { + LogTarget.LogLine($"Cannot find directory '{MemberDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + if (!Directory.Exists(InputDir)) + { + LogTarget.LogLine($"Cannot find directory '{InputDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + + try + { + LogTarget.StepStarted(2, $"Backupping Contents of {MemberDir}"); + + string[] filenames = Directory.GetFiles(MemberDir, "*.xml"); + LogTarget.LogLine($"Renaming {filenames.Length} xml files to xml.bak in {MemberDir}", LogEvent.ELogLevel.Info, "ImportProcess"); + + foreach (string f in filenames) + { + string fileName = Path.GetFileName(f); + string backupName = $"{fileName}.bak"; + if (File.Exists(backupName)) File.Delete(backupName); + File.Move(fileName, backupName); + File.Delete(fileName); + } + + LogTarget.StepStarted(3, $"Unpacking {MemberImportSettings.ZipFile}"); + LogTarget.LogLine($"Extracting {Path.Combine(InputDir, MemberImportSettings.ZipFile)}", LogEvent.ELogLevel.Info, "ImportProcess"); + string inFile = Path.Combine(InputDir, MemberImportSettings.ZipFile); + + + using (ZipInputStream s = new ZipInputStream(File.OpenRead(inFile))) + { + s.Password = MemberImportSettings.ZipPassword; + + ZipEntry theEntry; + while ((theEntry = s.GetNextEntry()) != null) + { + + Console.WriteLine(theEntry.Name); + + string directoryName = Path.GetDirectoryName(theEntry.Name); + string fileName = Path.GetFileName(theEntry.Name); + + // create directory + if (directoryName.Length > 0) + { + Directory.CreateDirectory(directoryName); + } + + if (fileName != String.Empty) + { + using (FileStream streamWriter = File.Create(Path.Combine(MemberDir, theEntry.Name))) + { + + int size = 2048; + byte[] data = new byte[2048]; + while (true) + { + size = s.Read(data, 0, data.Length); + if (size > 0) + { + streamWriter.Write(data, 0, size); + } + else + { + break; + } + } + } + } + } + } + LogTarget.StepCompleted(3, $"Unpacking {MemberImportSettings.ZipFile}", true); + } + catch (Exception ex) + { + LogTarget.LogLine($"Exception during import: {ex.Message}", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; } - Console.WriteLine($"Import: Extracting {Path.Combine(inputDir, ZipFile)}"); - if (ZipImport(memberDirectory, inputDir) != true) return false;*/ return true; } + private bool ZipImport() + { + + + + try + { + + } + catch (Exception ex) + { + Console.WriteLine($"Exception during import: {ex.Message}"); + + // No need to rethrow the exception as for our purposes its handled. + return false; + } + return true; + } } public class MemberImportExport { @@ -59,12 +176,21 @@ namespace dezentrale.model - private bool RunProcess(string cmd, string args, string workingDir = ".") + public static bool RunProcess(string cmd, string args, string workingDir = ".", view.ILogger log = null) { try - { - Console.WriteLine($"cmd: {cmd} {args}"); - Console.WriteLine($"dir: {workingDir}"); + { + if (log != null) + { + log.LogLine($"Running {cmd}", LogEvent.ELogLevel.Info, "RunProcess"); + log.LogRaw($"args: {cmd} {args}"); + log.LogRaw($"dir: {workingDir}"); + } + else + { + Console.WriteLine($"cmd: {cmd} {args}"); + Console.WriteLine($"dir: {workingDir}"); + } Process p = new Process(); p.StartInfo.FileName = cmd; @@ -80,15 +206,23 @@ namespace dezentrale.model p.WaitForExit(); if (stdout.Length > 0) { - Console.WriteLine("stdout:"); - Console.WriteLine(stdout); - Console.WriteLine(); + if (log != null) log.LogRaw(stdout); + else + { + Console.WriteLine("stdout:"); + Console.WriteLine(stdout); + Console.WriteLine(); + } } if (stderr.Length > 0) { - Console.WriteLine("stderr:"); - Console.WriteLine(stderr); - Console.WriteLine(); + if (log != null) log.LogRaw(stderr); + else + { + Console.WriteLine("stderr:"); + Console.WriteLine(stderr); + Console.WriteLine(); + } } return p.ExitCode == 0; } catch (Exception ex) @@ -98,9 +232,9 @@ namespace dezentrale.model return false; } - private bool Gpg(string args) + public static bool Gpg(string args, string gpgPassword) { - string argsFull = $"--batch --yes --passphrase \"{GpgPassword}\" {args}"; + string argsFull = $"--batch --yes --passphrase \"{gpgPassword}\" {args}"; return RunProcess("gpg", argsFull); } public bool Export(string memberDirectory, string outputDir) @@ -111,7 +245,7 @@ namespace dezentrale.model if (GpgEnabled) { Console.WriteLine($"Export: Using GPG to encrypt {Path.Combine(outputDir, GpgFile)}"); - if (!Gpg($"--output \"{Path.Combine(outputDir, GpgFile)}\" -c \"{tmpFile}\"")) return false; + if (!Gpg($"--output \"{Path.Combine(outputDir, GpgFile)}\" -c \"{tmpFile}\"", GpgPassword)) return false; tmpFile = Path.Combine(outputDir, GpgFile); } if (HgEnabled) @@ -136,7 +270,8 @@ namespace dezentrale.model [Obsolete] public bool Import(string memberDirectory, string inputDir) { - if(HgEnabled) + throw new NotImplementedException(); + /*if(HgEnabled) { RunProcess("hg", "init", inputDir); RunProcess("hg", "update null", inputDir); @@ -148,11 +283,11 @@ namespace dezentrale.model if (GpgEnabled) { Console.WriteLine($"Import: Using GPG to decrypt {Path.Combine(inputDir, GpgFile)}"); - if (!Gpg($"--output {Path.Combine(inputDir, ZipFile)} --decrypt {Path.Combine(inputDir, GpgFile)}")) return false; + if (!Gpg($"--output {Path.Combine(inputDir, ZipFile)} --decrypt {Path.Combine(inputDir, GpgFile)}", GpgPassword)) return false; } Console.WriteLine($"Import: Extracting {Path.Combine(inputDir, ZipFile)}"); if (ZipImport(memberDirectory, inputDir) != true) return false; - return true; + return true;*/ } private bool FilesCompare(string file1, string file2) { @@ -334,11 +469,13 @@ namespace dezentrale.model } return true; - } + } + /* + [Obsolete] private bool ZipImport(string memberDirectory, string inputDir) { - - + throw new NotImplementedException(); + /* if (!Directory.Exists(memberDirectory)) { Console.WriteLine($"Cannot find directory '{memberDirectory}'"); @@ -415,6 +552,6 @@ namespace dezentrale.model return false; } return true; - } + }*/ } } diff --git a/view/frmMain.cs b/view/frmMain.cs index 3b95619..d5ae397 100644 --- a/view/frmMain.cs +++ b/view/frmMain.cs @@ -22,8 +22,6 @@ namespace dezentrale.view public frmMain() { - frmProcessWithLog p = new frmProcessWithLog(new HelloWorldProcess(), false); - p.Show(); this.SuspendLayout(); this.Text = $"dezentrale-members Version {Program.VersionString}"; this.StartPosition = FormStartPosition.CenterScreen; @@ -138,11 +136,22 @@ namespace dezentrale.view { DialogResult dr = MessageBox.Show("Overwrite database?", "Warning\nImporting database from external source will overwrite all\nlocal changes! Really import?", MessageBoxButtons.YesNo); if(dr == DialogResult.Yes) - { - if (!Program.config.ImportExport.Import(Program.config.DbDirectory, Program.DmDirectory)) + { + ImportProcess import = new ImportProcess() + { + MemberImportSettings = Program.config.ImportExport, + MemberDir = Program.config.DbDirectory, + InputDir = Program.DmDirectory, + }; + frmProcessWithLog frmImport = new frmProcessWithLog(import, false); + dr = frmImport.ShowDialog(); + MessageBox.Show("DialogResult=" + dr); + //Reload member list + //Reload moneytransfers, if loaded + /*if (!Program.config.ImportExport.Import(Program.config.DbDirectory, Program.DmDirectory)) { MessageBox.Show("Import failed!"); - } + }*/ Application.Restart(); } } diff --git a/view/frmProcessWithLog.cs b/view/frmProcessWithLog.cs index db04616..d177348 100644 --- a/view/frmProcessWithLog.cs +++ b/view/frmProcessWithLog.cs @@ -6,21 +6,15 @@ using System.Text; using System.Threading; using System.Windows.Forms; +using dezentrale.model; + namespace dezentrale.view { - public enum ELogLevel - { - Trace = -1, - Debug = 0, - Info = 1, - Warning = 2, - Error = 3, - } - public interface ILogger { void Clear(); - void LogLine(string text, ELogLevel logLevel = ELogLevel.Info, string module = ""); + void LogRaw(string text); + void LogLine(string text, LogEvent.ELogLevel logLevel = LogEvent.ELogLevel.Info, string module = ""); } public interface IProcessController : ILogger @@ -65,7 +59,7 @@ namespace dezentrale.view { string stepName = ((i & 0x01) == 0) ? "Hello" : "World"; LogTarget.StepStarted(i, stepName); - LogTarget.LogLine(stepName, ELogLevel.Info, "HelloWorldProcess"); + LogTarget.LogLine(stepName, LogEvent.ELogLevel.Info, "HelloWorldProcess"); Thread.Sleep(200); LogTarget.StepCompleted(i, stepName, (i & 0x01) == 0); Thread.Sleep(200); @@ -75,6 +69,7 @@ namespace dezentrale.view } public class frmProcessWithLog : Form, IProcessController { + private DialogResult toReturn = DialogResult.Cancel; private TextBox tbLog = null; private Label lblStepStatus = null; private PictureBox picStatus = null; @@ -91,10 +86,19 @@ namespace dezentrale.view private void printline(string line) { - tbLog.Invoke(new Action(() => tbLog.AppendText(line + "\r\n"))); +// tbLog.Invoke(new Action(() => + tbLog.AppendText(line + "\r\n") +// )) + ; Console.WriteLine(line); } - public void LogLine(string text, ELogLevel logLevel, string module) + public void LogRaw(string text) + { + printline(text); + } + + + public void LogLine(string text, LogEvent.ELogLevel logLevel, string module) { string output = $"{DateTime.Now} [{module}] {logLevel} {text}"; printline(output); @@ -108,6 +112,12 @@ namespace dezentrale.view this.Width = 800; this.Height = 600; +/* if (autoRun) + { + MessageBox.Show("autoRun=true leads to problems with Invoke on gui access from background thread"); + this.Close(); + return; + }*/ // build GUI //[TextBox Multiline ] //[Step display label ] @@ -177,6 +187,8 @@ namespace dezentrale.view if (autoRun) btnStart_Click(null,null); + + this.FormClosing += (sender, e) => DialogResult = toReturn; } private void btnStart_Click(object sender, EventArgs e) @@ -201,18 +213,34 @@ namespace dezentrale.view public void StepStarted(uint stepNumber, string stepDescription) { - lblStepStatus.Invoke(new Action(() => lblStepStatus.Text = $"# {stepNumber}/{this.process.Steps} : {stepDescription}")); +// lblStepStatus.Invoke(new Action(() => + lblStepStatus.Text = $"# {stepNumber + 1}/{this.process.Steps} : {stepDescription}" +// )) + ; } public void StepCompleted(uint stepNumber, string stepDescription, bool success) { - lblStepStatus.Invoke(new Action(() => lblStepStatus.Text = $"# {stepNumber}/{this.process.Steps} : Completed({success}) {stepDescription}")); +// lblStepStatus.Invoke(new Action(() => + lblStepStatus.Text = $"# {stepNumber + 1}/{this.process.Steps} : Completed({success}) {stepDescription}" +// )) + ; } public void ActionCompleted(bool success) { + toReturn = success? DialogResult.Yes : DialogResult.No; btnCancel.Enabled = false; - btnClose.Invoke(new Action(() => btnClose.Enabled = true )); - picStatus.Invoke(new Action(() => picStatus.BackColor = (success ? Color.Green : Color.Red))); - this.Invoke(new Action(() => this.Text = $"Process - {this.process.Caption}" + (success ? " (done)" : " (failed)"))); +// btnClose.Invoke(new Action(() => + btnClose.Enabled = true +// )) + ; +// picStatus.Invoke(new Action(() => + picStatus.BackColor = (success ? Color.Green : Color.Red) +// )) + ; +// this.Invoke(new Action(() => + this.Text = $"Process - {this.process.Caption}" + (success ? " (done)" : " (failed)") +// )) + ; } } } From 20c32ab7a3f5bd135faf2c798aaa8462400ff7d6 Mon Sep 17 00:00:00 2001 From: phantomix Date: Mon, 2 Dec 2019 17:23:35 +0100 Subject: [PATCH 4/5] 191202PX Added namespace dezentrale.core for the code running in background (thus, completing the mvc structure) --- Program.cs | 22 +- core/BackgroundProcess.cs | 50 +++++ core/ExportProcess.cs | 128 +++++++++++ core/ILogger.cs | 13 ++ core/IProcessController.cs | 11 + core/ImportExportBase.cs | 78 +++++++ core/ImportProcess.cs | 149 +++++++++++++ dezentrale-members.csproj | 10 + model/MemberImportExport.cs | 411 +----------------------------------- view/ConsoleLogger.cs | 54 +++++ view/frmMain.cs | 9 +- view/frmProcessWithLog.cs | 74 +------ 12 files changed, 524 insertions(+), 485 deletions(-) create mode 100644 core/BackgroundProcess.cs create mode 100644 core/ExportProcess.cs create mode 100644 core/ILogger.cs create mode 100644 core/IProcessController.cs create mode 100644 core/ImportExportBase.cs create mode 100644 core/ImportProcess.cs create mode 100644 view/ConsoleLogger.cs diff --git a/Program.cs b/Program.cs index 9c5fdbd..aa18131 100644 --- a/Program.cs +++ b/Program.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Windows.Forms; +using dezentrale.core; using dezentrale.model; using dezentrale.model.money; using dezentrale.view; @@ -31,7 +32,7 @@ namespace dezentrale { public class Program { - public static uint VersionNumber { get; private set; } = 0x19120100; + public static uint VersionNumber { get; private set; } = 0x19120200; public static string VersionString { get; private set; } = $"{VersionNumber:x}"; public static string AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); @@ -184,10 +185,23 @@ namespace dezentrale if (!config.ImportExport.VerifyExport(config.DbDirectory, DmDirectory)) return 1; break; case eMode.Import: + { + ImportProcess import = new ImportProcess() + { + MemberImportSettings = Program.config.ImportExport, + MemberDir = Program.config.DbDirectory, + InputDir = Program.DmDirectory, + }; + ConsoleLogger logger = new ConsoleLogger(); + System.Threading.Thread t = logger.StartRunProcess(import); + if (t != null) t.Join(); + else return 1; - if (!config.ImportExport.Import(config.DbDirectory, DmDirectory)) - return 1; - break; + //frmProcessWithLog frmImport = new frmProcessWithLog(import, true); + //if (!config.ImportExport.Import(config.DbDirectory, DmDirectory)) + //return 1; + if(logger.DialogResult != DialogResult.OK) return 1; + } break; case eMode.BankImport: if (string.IsNullOrEmpty(csvInput)) { diff --git a/core/BackgroundProcess.cs b/core/BackgroundProcess.cs new file mode 100644 index 0000000..a19f09e --- /dev/null +++ b/core/BackgroundProcess.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace dezentrale.core +{ + public abstract class BackgroundProcess + { + public IProcessController LogTarget { get; set; } = null; + public bool CancelButton { get; protected set; } = false; + public string Caption { get; protected set; } = "BackgroundProcess"; + public uint Steps { get; protected set; } = 1; + + protected abstract bool Run(); + + private Thread thread = null; + public Thread StartRun() + { + if (thread != null) return null; + thread = new Thread(RunAbstract) { IsBackground = true }; + thread.Start(); + return thread; + } + private void RunAbstract() + { + LogTarget.ActionCompleted(Run()); + } + } + public class HelloWorldProcess : BackgroundProcess + { + public HelloWorldProcess() + { + Caption = "Hello World"; + Steps = 20; + } + protected override bool Run() + { + for (uint i = 0; i < 20; i++) + { + string stepName = ((i & 0x01) == 0) ? "Hello" : "World"; + LogTarget.StepStarted(i, stepName); + LogTarget.LogLine(stepName, dezentrale.model.LogEvent.ELogLevel.Info, "HelloWorldProcess"); + Thread.Sleep(200); + LogTarget.StepCompleted(i, stepName, (i & 0x01) == 0); + Thread.Sleep(200); + } + return true; + } + } +} diff --git a/core/ExportProcess.cs b/core/ExportProcess.cs new file mode 100644 index 0000000..70c857b --- /dev/null +++ b/core/ExportProcess.cs @@ -0,0 +1,128 @@ +using System; +using System.IO; +using ICSharpCode.SharpZipLib.Zip; + +using dezentrale.model; + +namespace dezentrale.core +{ + public class ExportProcess : ImportExportBase + { + public ExportProcess() + { + Caption = "Database export"; + Steps = 4; + } + + protected override bool Run() + { + string tmpFile = Path.Combine(OutputDir, MemberImportSettings.ZipFile); + Console.WriteLine($"Export: Packing {tmpFile}"); + + if (!Directory.Exists(MemberDir)) + { + Console.WriteLine($"Cannot find directory '{MemberDir}'"); + return false; + } + if (!Directory.Exists(OutputDir)) + { + Console.WriteLine($"Cannot find directory '{OutputDir}'"); + return false; + } + try + { + // Depending on the directory this could be very large and would require more attention + // in a commercial package. + string[] filenames = Directory.GetFiles(MemberDir, "*.xml"); + string outFile = Path.Combine(OutputDir, MemberImportSettings.ZipFile); + if (File.Exists(outFile)) + { + string backupName = $"{outFile}.bak"; + if (File.Exists(backupName)) File.Delete(backupName); + File.Move(outFile, backupName); + } + + // 'using' statements guarantee the stream is closed properly which is a big source + // of problems otherwise. Its exception safe as well which is great. + using (ZipOutputStream s = new ZipOutputStream(File.Create(outFile))) + { + s.SetLevel(9); // 0 - store only to 9 - means best compression + s.Password = MemberImportSettings.ZipPassword; //null is a desired value for "no encryption" + + byte[] buffer = new byte[4096]; + + foreach (string file in filenames) + { + Console.WriteLine(file); + // Using GetFileName makes the result compatible with XP + // as the resulting path is not absolute. + var entry = new ZipEntry(Path.GetFileName(file)); + + // Setup the entry data as required. + + // Crc and size are handled by the library for seakable streams + // so no need to do them here. + + // Could also use the last write time or similar for the file. + entry.DateTime = DateTime.Now; + s.PutNextEntry(entry); + + using (FileStream fs = File.OpenRead(file)) + { + + // Using a fixed size buffer here makes no noticeable difference for output + // but keeps a lid on memory usage. + int sourceBytes; + do + { + sourceBytes = fs.Read(buffer, 0, buffer.Length); + s.Write(buffer, 0, sourceBytes); + } while (sourceBytes > 0); + } + } + + // Finish/Close arent needed strictly as the using statement does this automatically + + // Finish is important to ensure trailing information for a Zip file is appended. Without this + // the created file would be invalid. + s.Finish(); + + // Close is important to wrap things up and unlock the file. + s.Close(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Exception during export: {ex.Message}"); + + // No need to rethrow the exception as for our purposes its handled. + return false; + } + + + if (MemberImportSettings.GpgEnabled) + { + Console.WriteLine($"Export: Using GPG to encrypt {Path.Combine(OutputDir, MemberImportSettings.GpgFile)}"); + if (!Gpg($"--output \"{Path.Combine(OutputDir, MemberImportSettings.GpgFile)}\" -c \"{tmpFile}\"", MemberImportSettings.GpgPassword)) return false; + tmpFile = Path.Combine(OutputDir, MemberImportSettings.GpgFile); + } + if (MemberImportSettings.HgEnabled) + { + Console.WriteLine($"Export: Using HG to commit / push {tmpFile}"); + //this might fail as repo might be existing / file might be already in repo + RunProcess("hg", "init", OutputDir); + RunProcess("hg", $"add {tmpFile}", OutputDir); + //now, committing is more interesting + if (!RunProcess("hg", + "commit" + + $" --message \"dezentrale-members.exe --mode=export\nProgram version={Program.VersionString}\"" + + $" --user \"{MemberImportSettings.HgUserName}\"", OutputDir)) + return false; + if (!RunProcess("hg", $"--config auth.rc.prefix={MemberImportSettings.HgURL} --config auth.rc.username={MemberImportSettings.HgUserName} --config auth.rc.password={MemberImportSettings.HgPassword} push {MemberImportSettings.HgURL}", OutputDir)) + return false; + + } + return true; + } + } +} diff --git a/core/ILogger.cs b/core/ILogger.cs new file mode 100644 index 0000000..c0986a2 --- /dev/null +++ b/core/ILogger.cs @@ -0,0 +1,13 @@ +using System; + +using dezentrale.model; + +namespace dezentrale.core +{ + public interface ILogger + { + void Clear(); + void LogRaw(string text); + void LogLine(string text, LogEvent.ELogLevel logLevel = LogEvent.ELogLevel.Info, string module = ""); + } +} diff --git a/core/IProcessController.cs b/core/IProcessController.cs new file mode 100644 index 0000000..cd98a4b --- /dev/null +++ b/core/IProcessController.cs @@ -0,0 +1,11 @@ +using System; +namespace dezentrale.core +{ + public interface IProcessController : ILogger + { + void ActionCompleted(bool success); + void StepStarted(uint stepNumber = 0, string stepDescription = "Generic"); + void StepCompleted(uint stepNumber = 0, string stepDescription = "Generic", bool success = true); + System.Windows.Forms.DialogResult DialogResult { get; set; } + } +} diff --git a/core/ImportExportBase.cs b/core/ImportExportBase.cs new file mode 100644 index 0000000..c56e208 --- /dev/null +++ b/core/ImportExportBase.cs @@ -0,0 +1,78 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace dezentrale.core +{ + public abstract class ImportExportBase : BackgroundProcess + { + public model.MemberImportExport MemberImportSettings { get; set; } = null; + public string MemberDir { get; set; } = null; //! Directory that holds the application settings + public string InputDir { get; set; } = null; //! Import working directory + public string OutputDir { get; set; } = null; //! Export working directory + + public static bool Gpg(string args, string gpgPassword) + { + string argsFull = $"--batch --yes --passphrase \"{gpgPassword}\" {args}"; + return RunProcess("gpg", argsFull); + } + + public static bool RunProcess(string cmd, string args, string workingDir = ".", core.ILogger log = null) + { + try + { + if (log != null) + { + log.LogLine($"Running {cmd}", model.LogEvent.ELogLevel.Info, "RunProcess"); + log.LogRaw($"args: {cmd} {args}"); + log.LogRaw($"dir: {workingDir}"); + } + else + { + Console.WriteLine($"cmd: {cmd} {args}"); + Console.WriteLine($"dir: {workingDir}"); + } + Process p = new Process(); + + p.StartInfo.FileName = cmd; + p.StartInfo.Arguments = args; + p.StartInfo.WorkingDirectory = workingDir; + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.RedirectStandardError = true; + p.StartInfo.CreateNoWindow = true; + p.Start(); + string stdout = p.StandardOutput.ReadToEnd(); + string stderr = p.StandardError.ReadToEnd(); + p.WaitForExit(); + if (stdout.Length > 0) + { + if (log != null) log.LogRaw(stdout); + else + { + Console.WriteLine("stdout:"); + Console.WriteLine(stdout); + Console.WriteLine(); + } + } + if (stderr.Length > 0) + { + if (log != null) log.LogRaw(stderr); + else + { + Console.WriteLine("stderr:"); + Console.WriteLine(stderr); + Console.WriteLine(); + } + } + return p.ExitCode == 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + + return false; + } + } +} \ No newline at end of file diff --git a/core/ImportProcess.cs b/core/ImportProcess.cs new file mode 100644 index 0000000..a5f34b7 --- /dev/null +++ b/core/ImportProcess.cs @@ -0,0 +1,149 @@ +using System; +using System.IO; +using ICSharpCode.SharpZipLib.Zip; + +using dezentrale.model; + +namespace dezentrale.core +{ + public class ImportProcess : ImportExportBase + { + public ImportProcess() + { + Caption = "Database import"; + Steps = 4; + } + protected override bool Run() + { + + LogTarget.StepStarted(0, "Preparing import"); + + if (MemberImportSettings == null) + { + LogTarget.LogLine("MemberImportExport data class not set.", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + if (MemberDir == null) + { + LogTarget.LogLine("Member directory not set.", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + if (InputDir == null) + { + LogTarget.LogLine("Input directory not set.", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + + + + + + LogTarget.StepStarted(1, "Fetching repository"); + + if (MemberImportSettings.HgEnabled) + { + RunProcess("hg", "init", InputDir, LogTarget); + RunProcess("hg", "update null", InputDir, LogTarget); + if (!RunProcess("hg", $"--config auth.rc.prefix={MemberImportSettings.HgURL} --config auth.rc.username={MemberImportSettings.HgUserName} --config auth.rc.password={MemberImportSettings.HgPassword} pull {MemberImportSettings.HgURL}", InputDir, LogTarget)) + return false; + if (!RunProcess("hg", $"update", InputDir, LogTarget)) + return false; + } + if (MemberImportSettings.GpgEnabled) + { + Console.WriteLine($"Import: Using GPG to decrypt {Path.Combine(InputDir, MemberImportSettings.GpgFile)}"); + if (!Gpg($"--output {Path.Combine(InputDir, MemberImportSettings.ZipFile)} --decrypt {Path.Combine(InputDir, MemberImportSettings.GpgFile)}", MemberImportSettings.GpgPassword)) return false; + } + if (!Directory.Exists(MemberDir)) + { + LogTarget.LogLine($"Cannot find directory '{MemberDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + if (!Directory.Exists(InputDir)) + { + LogTarget.LogLine($"Cannot find directory '{InputDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + + try + { + LogTarget.StepStarted(2, $"Backupping Contents of {MemberDir}"); + + string[] filenames = Directory.GetFiles(MemberDir, "*.xml"); + LogTarget.LogLine($"Renaming {filenames.Length} xml files to xml.bak in {MemberDir}", LogEvent.ELogLevel.Info, "ImportProcess"); + + foreach (string f in filenames) + { + + string fileName = Path.GetFileName(f); + string backupName = $"{fileName}.bak"; + if (File.Exists(backupName)) + { + LogTarget.LogLine($"Deleting previous {backupName}", LogEvent.ELogLevel.Trace, "ImportProcess"); + File.Delete(backupName); + } + LogTarget.LogLine($"Renaming {fileName} ({f}) to {backupName}", LogEvent.ELogLevel.Trace, "ImportProcess"); + File.Move(fileName, backupName); + + LogTarget.LogLine($"Deleting backupped {fileName}", LogEvent.ELogLevel.Trace, "ImportProcess"); + File.Delete(fileName); + } + + LogTarget.StepStarted(3, $"Unpacking {MemberImportSettings.ZipFile}"); + LogTarget.LogLine($"Extracting {Path.Combine(InputDir, MemberImportSettings.ZipFile)}", LogEvent.ELogLevel.Info, "ImportProcess"); + string inFile = Path.Combine(InputDir, MemberImportSettings.ZipFile); + + + using (ZipInputStream s = new ZipInputStream(File.OpenRead(inFile))) + { + s.Password = MemberImportSettings.ZipPassword; + + ZipEntry theEntry; + while ((theEntry = s.GetNextEntry()) != null) + { + + Console.WriteLine(theEntry.Name); + + string directoryName = Path.GetDirectoryName(theEntry.Name); + string fileName = Path.GetFileName(theEntry.Name); + + // create directory + if (directoryName.Length > 0) + { + Directory.CreateDirectory(directoryName); + } + + if (fileName != String.Empty) + { + using (FileStream streamWriter = File.Create(Path.Combine(MemberDir, theEntry.Name))) + { + + int size = 2048; + byte[] data = new byte[2048]; + while (true) + { + size = s.Read(data, 0, data.Length); + if (size > 0) + { + streamWriter.Write(data, 0, size); + } + else + { + break; + } + } + } + } + } + } + LogTarget.StepCompleted(3, $"Unpacking {MemberImportSettings.ZipFile}", true); + } + catch (Exception ex) + { + LogTarget.LogLine($"Exception during import: {ex.Message}", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } + return true; + } + } +} diff --git a/dezentrale-members.csproj b/dezentrale-members.csproj index cc4079f..de50b98 100644 --- a/dezentrale-members.csproj +++ b/dezentrale-members.csproj @@ -183,6 +183,13 @@ Component + + + + + + + @@ -196,5 +203,8 @@ + + + \ No newline at end of file diff --git a/model/MemberImportExport.cs b/model/MemberImportExport.cs index 89f11ef..ad5fc72 100644 --- a/model/MemberImportExport.cs +++ b/model/MemberImportExport.cs @@ -6,159 +6,6 @@ using System.Diagnostics; namespace dezentrale.model { - public class ImportProcess : view.BackgroundProcess - { - public MemberImportExport MemberImportSettings { get; set; } = null; - public string MemberDir { get; set; } = null; - public string InputDir { get; set; } = null; - public ImportProcess() - { - Caption = "Database import"; - Steps = 4; - } - protected override bool Run() - { - - LogTarget.StepStarted(0, "Preparing import"); - - if (MemberImportSettings == null) - { - LogTarget.LogLine("MemberImportExport data class not set.", LogEvent.ELogLevel.Error, "ImportProcess"); - return false; - } - if (MemberDir == null) - { - LogTarget.LogLine("Member directory not set.", LogEvent.ELogLevel.Error, "ImportProcess"); - return false; - } - if (InputDir == null) - { - LogTarget.LogLine("Input directory not set.", LogEvent.ELogLevel.Error, "ImportProcess"); - return false; - } - - - - - - LogTarget.StepStarted(1, "Fetching repository"); - - if (MemberImportSettings.HgEnabled) - { - MemberImportExport.RunProcess("hg", "init", InputDir, LogTarget); - MemberImportExport.RunProcess("hg", "update null", InputDir, LogTarget); - if (!MemberImportExport.RunProcess("hg", $"--config auth.rc.prefix={MemberImportSettings.HgURL} --config auth.rc.username={MemberImportSettings.HgUserName} --config auth.rc.password={MemberImportSettings.HgPassword} pull {MemberImportSettings.HgURL}", InputDir, LogTarget)) - return false; - if (!MemberImportExport.RunProcess("hg", $"update", InputDir, LogTarget)) - return false; - } - if (MemberImportSettings.GpgEnabled) - { - Console.WriteLine($"Import: Using GPG to decrypt {Path.Combine(InputDir, MemberImportSettings.GpgFile)}"); - if (!MemberImportExport.Gpg($"--output {Path.Combine(InputDir, MemberImportSettings.ZipFile)} --decrypt {Path.Combine(InputDir, MemberImportSettings.GpgFile)}", MemberImportSettings.GpgPassword)) return false; - } - if (!Directory.Exists(MemberDir)) - { - LogTarget.LogLine($"Cannot find directory '{MemberDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); - return false; - } - if (!Directory.Exists(InputDir)) - { - LogTarget.LogLine($"Cannot find directory '{InputDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); - return false; - } - - try - { - LogTarget.StepStarted(2, $"Backupping Contents of {MemberDir}"); - - string[] filenames = Directory.GetFiles(MemberDir, "*.xml"); - LogTarget.LogLine($"Renaming {filenames.Length} xml files to xml.bak in {MemberDir}", LogEvent.ELogLevel.Info, "ImportProcess"); - - foreach (string f in filenames) - { - string fileName = Path.GetFileName(f); - string backupName = $"{fileName}.bak"; - if (File.Exists(backupName)) File.Delete(backupName); - File.Move(fileName, backupName); - File.Delete(fileName); - } - - LogTarget.StepStarted(3, $"Unpacking {MemberImportSettings.ZipFile}"); - LogTarget.LogLine($"Extracting {Path.Combine(InputDir, MemberImportSettings.ZipFile)}", LogEvent.ELogLevel.Info, "ImportProcess"); - string inFile = Path.Combine(InputDir, MemberImportSettings.ZipFile); - - - using (ZipInputStream s = new ZipInputStream(File.OpenRead(inFile))) - { - s.Password = MemberImportSettings.ZipPassword; - - ZipEntry theEntry; - while ((theEntry = s.GetNextEntry()) != null) - { - - Console.WriteLine(theEntry.Name); - - string directoryName = Path.GetDirectoryName(theEntry.Name); - string fileName = Path.GetFileName(theEntry.Name); - - // create directory - if (directoryName.Length > 0) - { - Directory.CreateDirectory(directoryName); - } - - if (fileName != String.Empty) - { - using (FileStream streamWriter = File.Create(Path.Combine(MemberDir, theEntry.Name))) - { - - int size = 2048; - byte[] data = new byte[2048]; - while (true) - { - size = s.Read(data, 0, data.Length); - if (size > 0) - { - streamWriter.Write(data, 0, size); - } - else - { - break; - } - } - } - } - } - } - LogTarget.StepCompleted(3, $"Unpacking {MemberImportSettings.ZipFile}", true); - } - catch (Exception ex) - { - LogTarget.LogLine($"Exception during import: {ex.Message}", LogEvent.ELogLevel.Error, "ImportProcess"); - return false; - } - return true; - } - private bool ZipImport() - { - - - - try - { - - } - catch (Exception ex) - { - Console.WriteLine($"Exception during import: {ex.Message}"); - - // No need to rethrow the exception as for our purposes its handled. - return false; - } - return true; - } - } public class MemberImportExport { [XmlElement] public string ZipFile { get; set; } = "fnord.zip"; @@ -174,97 +21,10 @@ namespace dezentrale.model [XmlElement] public string GitUserName { get; set; } = ""; [XmlElement] public string GitPassword { get; set; } = ""; - - - public static bool RunProcess(string cmd, string args, string workingDir = ".", view.ILogger log = null) - { - try - { - if (log != null) - { - log.LogLine($"Running {cmd}", LogEvent.ELogLevel.Info, "RunProcess"); - log.LogRaw($"args: {cmd} {args}"); - log.LogRaw($"dir: {workingDir}"); - } - else - { - Console.WriteLine($"cmd: {cmd} {args}"); - Console.WriteLine($"dir: {workingDir}"); - } - Process p = new Process(); - - p.StartInfo.FileName = cmd; - p.StartInfo.Arguments = args; - p.StartInfo.WorkingDirectory = workingDir; - p.StartInfo.UseShellExecute = false; - p.StartInfo.RedirectStandardOutput = true; - p.StartInfo.RedirectStandardError = true; - p.StartInfo.CreateNoWindow = true; - p.Start(); - string stdout = p.StandardOutput.ReadToEnd(); - string stderr = p.StandardError.ReadToEnd(); - p.WaitForExit(); - if (stdout.Length > 0) - { - if (log != null) log.LogRaw(stdout); - else - { - Console.WriteLine("stdout:"); - Console.WriteLine(stdout); - Console.WriteLine(); - } - } - if (stderr.Length > 0) - { - if (log != null) log.LogRaw(stderr); - else - { - Console.WriteLine("stderr:"); - Console.WriteLine(stderr); - Console.WriteLine(); - } - } - return p.ExitCode == 0; - } catch (Exception ex) - { - Console.WriteLine($"Error: {ex.Message}"); - } - - return false; - } - public static bool Gpg(string args, string gpgPassword) - { - string argsFull = $"--batch --yes --passphrase \"{gpgPassword}\" {args}"; - return RunProcess("gpg", argsFull); - } + [Obsolete] public bool Export(string memberDirectory, string outputDir) { - string tmpFile = Path.Combine(outputDir, ZipFile); - Console.WriteLine($"Export: Packing {tmpFile}"); - if (ZipExport(memberDirectory, outputDir) != true) return false; - if (GpgEnabled) - { - Console.WriteLine($"Export: Using GPG to encrypt {Path.Combine(outputDir, GpgFile)}"); - if (!Gpg($"--output \"{Path.Combine(outputDir, GpgFile)}\" -c \"{tmpFile}\"", GpgPassword)) return false; - tmpFile = Path.Combine(outputDir, GpgFile); - } - if (HgEnabled) - { - Console.WriteLine($"Export: Using HG to commit / push {tmpFile}"); - //this might fail as repo might be existing / file might be already in repo - RunProcess("hg", "init", outputDir); - RunProcess("hg", $"add {tmpFile}", outputDir); - //now, committing is more interesting - if (!RunProcess("hg", - "commit" - + $" --message \"dezentrale-members.exe --mode=export\nProgram version={Program.VersionString}\"" - + $" --user \"{HgUserName}\"", outputDir)) - return false; - if (!RunProcess("hg", $"--config auth.rc.prefix={HgURL} --config auth.rc.username={HgUserName} --config auth.rc.password={HgPassword} push {HgURL}", outputDir)) - return false; - - } - return true; + throw new NotImplementedException(); } [Obsolete] @@ -308,6 +68,7 @@ namespace dezentrale.model } return true; } + [Obsolete] public bool VerifyExport(string memberDirectory, string outputDir) { if (!Directory.Exists(memberDirectory)) @@ -386,172 +147,6 @@ namespace dezentrale.model return true; } - private bool ZipExport(string memberDirectory, string outputDir) - { - if (!Directory.Exists(memberDirectory)) - { - Console.WriteLine($"Cannot find directory '{memberDirectory}'"); - return false; - } - if (!Directory.Exists(outputDir)) - { - Console.WriteLine($"Cannot find directory '{outputDir}'"); - return false; - } - try - { - // Depending on the directory this could be very large and would require more attention - // in a commercial package. - string[] filenames = Directory.GetFiles(memberDirectory, "*.xml"); - string outFile = Path.Combine(outputDir, ZipFile); - if (File.Exists(outFile)) - { - string backupName = $"{outFile}.bak"; - if (File.Exists(backupName)) File.Delete(backupName); - File.Move(outFile, backupName); - } - // 'using' statements guarantee the stream is closed properly which is a big source - // of problems otherwise. Its exception safe as well which is great. - using (ZipOutputStream s = new ZipOutputStream(File.Create(outFile))) - { - s.SetLevel(9); // 0 - store only to 9 - means best compression - s.Password = ZipPassword; //null is a desired value for "no encryption" - - byte[] buffer = new byte[4096]; - - foreach (string file in filenames) - { - Console.WriteLine(file); - // Using GetFileName makes the result compatible with XP - // as the resulting path is not absolute. - var entry = new ZipEntry(Path.GetFileName(file)); - - // Setup the entry data as required. - - // Crc and size are handled by the library for seakable streams - // so no need to do them here. - - // Could also use the last write time or similar for the file. - entry.DateTime = DateTime.Now; - s.PutNextEntry(entry); - - using (FileStream fs = File.OpenRead(file)) - { - - // Using a fixed size buffer here makes no noticeable difference for output - // but keeps a lid on memory usage. - int sourceBytes; - do - { - sourceBytes = fs.Read(buffer, 0, buffer.Length); - s.Write(buffer, 0, sourceBytes); - } while (sourceBytes > 0); - } - } - - // Finish/Close arent needed strictly as the using statement does this automatically - - // Finish is important to ensure trailing information for a Zip file is appended. Without this - // the created file would be invalid. - s.Finish(); - - // Close is important to wrap things up and unlock the file. - s.Close(); - } - } - catch (Exception ex) - { - Console.WriteLine($"Exception during export: {ex.Message}"); - - // No need to rethrow the exception as for our purposes its handled. - return false; - } - - return true; - } - /* - [Obsolete] - private bool ZipImport(string memberDirectory, string inputDir) - { - throw new NotImplementedException(); - /* - if (!Directory.Exists(memberDirectory)) - { - Console.WriteLine($"Cannot find directory '{memberDirectory}'"); - return false; - } - if (!Directory.Exists(inputDir)) - { - Console.WriteLine($"Cannot find directory '{inputDir}'"); - return false; - } - - - try - { - //First, rename all member-*.xml to member-*.xml.bak - string[] filenames = Directory.GetFiles(memberDirectory, "*.xml"); - foreach (string f in filenames) - { - string fileName = Path.GetFileName(f); - string backupName = $"{fileName}.bak"; - if (File.Exists(backupName)) File.Delete(backupName); - File.Move(fileName, backupName); - File.Delete(fileName); - } - string inFile = Path.Combine(inputDir, ZipFile); - using (ZipInputStream s = new ZipInputStream(File.OpenRead(inFile))) - { - s.Password = ZipPassword; - - ZipEntry theEntry; - while ((theEntry = s.GetNextEntry()) != null) - { - - Console.WriteLine(theEntry.Name); - - string directoryName = Path.GetDirectoryName(theEntry.Name); - string fileName = Path.GetFileName(theEntry.Name); - - // create directory - if (directoryName.Length > 0) - { - Directory.CreateDirectory(directoryName); - } - - if (fileName != String.Empty) - { - using (FileStream streamWriter = File.Create(Path.Combine(memberDirectory, theEntry.Name))) - { - - int size = 2048; - byte[] data = new byte[2048]; - while (true) - { - size = s.Read(data, 0, data.Length); - if (size > 0) - { - streamWriter.Write(data, 0, size); - } - else - { - break; - } - } - } - } - } - } - } - catch (Exception ex) - { - Console.WriteLine($"Exception during import: {ex.Message}"); - - // No need to rethrow the exception as for our purposes its handled. - return false; - } - return true; - }*/ } } diff --git a/view/ConsoleLogger.cs b/view/ConsoleLogger.cs new file mode 100644 index 0000000..3f583f0 --- /dev/null +++ b/view/ConsoleLogger.cs @@ -0,0 +1,54 @@ +using System; +using dezentrale.model; + +namespace dezentrale.view +{ + public class ConsoleLogger : core.IProcessController + { + public System.Windows.Forms.DialogResult DialogResult { get; set; } = System.Windows.Forms.DialogResult.Cancel; + public ConsoleLogger() + { + + } + public System.Threading.Thread StartRunProcess(core.BackgroundProcess process) + { + Console.WriteLine($"Process - {process.Caption} (running)"); + + System.Threading.Thread t = process.StartRun(); + if(t == null) + { + Console.WriteLine($"Process - {process.Caption} (starting error)"); + } + return t; + } + public void Clear() + { + Console.Clear(); + } + + public void LogLine(string text, LogEvent.ELogLevel logLevel = LogEvent.ELogLevel.Info, string module = "") + { + Console.WriteLine($"{DateTime.Now} [{module}] {logLevel} {text}"); + } + + public void LogRaw(string text) + { + Console.WriteLine(text); + } + + public void StepStarted(uint stepNumber = 0, string stepDescription = "Generic") + { + Console.WriteLine($"# {stepNumber + 1}: Started {stepDescription}"); + } + + public void StepCompleted(uint stepNumber = 0, string stepDescription = "Generic", bool success = true) + { + Console.WriteLine($"# {stepNumber + 1}: Completed({success}) {stepDescription}"); + } + public void ActionCompleted(bool success) + { + if (success) Console.WriteLine($"Process - done (OK)"); + else Console.WriteLine($"Process - done (ERROR)"); + } + } +} diff --git a/view/frmMain.cs b/view/frmMain.cs index d5ae397..c0fed26 100644 --- a/view/frmMain.cs +++ b/view/frmMain.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; +using dezentrale.core; using dezentrale.model; using dezentrale.model.money; @@ -134,7 +135,7 @@ namespace dezentrale.view private void mnuMain_File_Import(object sender, EventArgs e) { - DialogResult dr = MessageBox.Show("Overwrite database?", "Warning\nImporting database from external source will overwrite all\nlocal changes! Really import?", MessageBoxButtons.YesNo); + DialogResult dr = MessageBox.Show("Warning\nImporting database from external source will overwrite all\nlocal changes! Really import?", "Overwrite local version of the database?", MessageBoxButtons.YesNo); if(dr == DialogResult.Yes) { ImportProcess import = new ImportProcess() @@ -143,16 +144,16 @@ namespace dezentrale.view MemberDir = Program.config.DbDirectory, InputDir = Program.DmDirectory, }; - frmProcessWithLog frmImport = new frmProcessWithLog(import, false); + frmProcessWithLog frmImport = new frmProcessWithLog(import, true); dr = frmImport.ShowDialog(); - MessageBox.Show("DialogResult=" + dr); + //MessageBox.Show("DialogResult=" + dr); //Reload member list //Reload moneytransfers, if loaded /*if (!Program.config.ImportExport.Import(Program.config.DbDirectory, Program.DmDirectory)) { MessageBox.Show("Import failed!"); }*/ - Application.Restart(); + if(dr == DialogResult.OK) Application.Restart(); } } diff --git a/view/frmProcessWithLog.cs b/view/frmProcessWithLog.cs index d177348..587edc2 100644 --- a/view/frmProcessWithLog.cs +++ b/view/frmProcessWithLog.cs @@ -3,70 +3,13 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; -using System.Threading; using System.Windows.Forms; +using dezentrale.core; using dezentrale.model; namespace dezentrale.view { - public interface ILogger - { - void Clear(); - void LogRaw(string text); - void LogLine(string text, LogEvent.ELogLevel logLevel = LogEvent.ELogLevel.Info, string module = ""); - } - - public interface IProcessController : ILogger - { - void ActionCompleted(bool success); - void StepStarted(uint stepNumber = 0, string stepDescription = "Generic"); - void StepCompleted(uint stepNumber = 0, string stepDescription = "Generic", bool success = true); - } - - public abstract class BackgroundProcess - { - public IProcessController LogTarget { get; set; } = null; - public bool CancelButton { get; protected set; } = false; - public string Caption { get; protected set; } = "BackgroundProcess"; - public uint Steps { get; protected set; } = 1; - - protected abstract bool Run(); - - private Thread thread = null; - public bool StartRun() - { - if (thread != null) return false; - thread = new Thread(RunAbstract) { IsBackground = true }; - thread.Start(); - return true; - } - private void RunAbstract() - { - LogTarget.ActionCompleted(Run()); - } - } - public class HelloWorldProcess : BackgroundProcess - { - public HelloWorldProcess() - { - Caption = "Hello World"; - Steps = 20; - } - protected override bool Run() - { - for(uint i = 0; i < 20; i++) - { - string stepName = ((i & 0x01) == 0) ? "Hello" : "World"; - LogTarget.StepStarted(i, stepName); - LogTarget.LogLine(stepName, LogEvent.ELogLevel.Info, "HelloWorldProcess"); - Thread.Sleep(200); - LogTarget.StepCompleted(i, stepName, (i & 0x01) == 0); - Thread.Sleep(200); - } - return true; - } - } public class frmProcessWithLog : Form, IProcessController { private DialogResult toReturn = DialogResult.Cancel; @@ -111,13 +54,11 @@ namespace dezentrale.view this.Text = $"Process - {this.process.Caption} (waiting)"; this.Width = 800; this.Height = 600; + this.StartPosition = FormStartPosition.CenterParent; + + this.FormClosing += (sender, e) => { DialogResult = toReturn; }; + this.Load += (sender, e) => { if (autoRun) btnStart_Click(null, null); }; -/* if (autoRun) - { - MessageBox.Show("autoRun=true leads to problems with Invoke on gui access from background thread"); - this.Close(); - return; - }*/ // build GUI //[TextBox Multiline ] //[Step display label ] @@ -184,11 +125,6 @@ namespace dezentrale.view btnClose.Height += 2; btnClose.Click += btnClose_Click; this.Controls.Add(btnClose); - - if (autoRun) - btnStart_Click(null,null); - - this.FormClosing += (sender, e) => DialogResult = toReturn; } private void btnStart_Click(object sender, EventArgs e) From 4f9aeb743f777ad998717830697a622860d68324 Mon Sep 17 00:00:00 2001 From: phantomix Date: Tue, 3 Dec 2019 17:43:02 +0100 Subject: [PATCH 5/5] 191203PX Worked on import/export and backupping. Starts to work but sometimes locks up without CPU load during backup generation. --- Program.cs | 27 +++++-- core/ExportProcess.cs | 141 +++++++++++++++++++++++++++++------- core/ImportExportBase.cs | 36 ++++++--- core/ImportProcess.cs | 72 +++++++++--------- model/Configuration.cs | 6 +- model/MemberImportExport.cs | 108 --------------------------- model/XmlData.cs | 50 +++++++++++-- view/frmMain.cs | 20 +++-- 8 files changed, 255 insertions(+), 205 deletions(-) diff --git a/Program.cs b/Program.cs index aa18131..99f83e4 100644 --- a/Program.cs +++ b/Program.cs @@ -32,7 +32,7 @@ namespace dezentrale { public class Program { - public static uint VersionNumber { get; private set; } = 0x19120200; + public static uint VersionNumber { get; private set; } = 0x19120300; public static string VersionString { get; private set; } = $"{VersionNumber:x}"; public static string AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); @@ -181,14 +181,28 @@ namespace dezentrale Cronjob.Run(); break; case eMode.Export: - if (!config.ImportExport.Export(config.DbDirectory, DmDirectory)) return 1; - if (!config.ImportExport.VerifyExport(config.DbDirectory, DmDirectory)) return 1; - break; + { + ExportProcess export = new ExportProcess() + { + ImportExportSettings = Program.config.ImportExport, + MemberDir = Program.config.DbDirectory, + OutputDir = Program.DmDirectory, + }; + ConsoleLogger logger = new ConsoleLogger(); + System.Threading.Thread t = logger.StartRunProcess(export); + if (t != null) t.Join(); + else return 1; + //if (!config.ImportExport.Export(config.DbDirectory, DmDirectory)) return 1; + //if (!config.ImportExport.VerifyExport(config.DbDirectory, DmDirectory)) return 1; + + if(logger.DialogResult != DialogResult.OK) return 1; + } break; + case eMode.Import: { ImportProcess import = new ImportProcess() { - MemberImportSettings = Program.config.ImportExport, + ImportExportSettings = Program.config.ImportExport, MemberDir = Program.config.DbDirectory, InputDir = Program.DmDirectory, }; @@ -201,7 +215,8 @@ namespace dezentrale //if (!config.ImportExport.Import(config.DbDirectory, DmDirectory)) //return 1; if(logger.DialogResult != DialogResult.OK) return 1; - } break; + } break; + case eMode.BankImport: if (string.IsNullOrEmpty(csvInput)) { diff --git a/core/ExportProcess.cs b/core/ExportProcess.cs index 70c857b..a724b2c 100644 --- a/core/ExportProcess.cs +++ b/core/ExportProcess.cs @@ -6,54 +6,76 @@ using dezentrale.model; namespace dezentrale.core { + /** \brief Export functionality for the database contents + */ public class ExportProcess : ImportExportBase { public ExportProcess() { Caption = "Database export"; - Steps = 4; + Steps = 6; } protected override bool Run() { - string tmpFile = Path.Combine(OutputDir, MemberImportSettings.ZipFile); - Console.WriteLine($"Export: Packing {tmpFile}"); + LogTarget.StepStarted(0, "Preparing export"); + if (ImportExportSettings == null) + { + LogTarget.LogLine("MemberImportExport data class not set.", LogEvent.ELogLevel.Error, "ExportProcess"); + return false; + } - if (!Directory.Exists(MemberDir)) + if (MemberDir == null) { - Console.WriteLine($"Cannot find directory '{MemberDir}'"); + LogTarget.LogLine("Member directory not set.", LogEvent.ELogLevel.Error, "ExportProcess"); + return false; + } else if (!Directory.Exists(MemberDir)) + { + LogTarget.LogLine($"Cannot find directory '{MemberDir}'", LogEvent.ELogLevel.Error, "ExportProcess"); return false; } - if (!Directory.Exists(OutputDir)) + + if (OutputDir == null) { - Console.WriteLine($"Cannot find directory '{OutputDir}'"); + LogTarget.LogLine("Input directory not set.", LogEvent.ELogLevel.Error, "ExportProcess"); + return false; + } else if (!Directory.Exists(OutputDir)) + { + LogTarget.LogLine($"Cannot find directory '{OutputDir}'", LogEvent.ELogLevel.Error, "ExportProcess"); return false; } + + //TBD: Check if newer version of database is online + LogTarget.StepStarted(1, $"Fetching commits from remote location"); + + + string outFile = Path.Combine(OutputDir, ImportExportSettings.ZipFile); try { // Depending on the directory this could be very large and would require more attention // in a commercial package. string[] filenames = Directory.GetFiles(MemberDir, "*.xml"); - string outFile = Path.Combine(OutputDir, MemberImportSettings.ZipFile); + if (File.Exists(outFile)) { - string backupName = $"{outFile}.bak"; - if (File.Exists(backupName)) File.Delete(backupName); - File.Move(outFile, backupName); + LogTarget.LogLine($"Creating backup of old {outFile}", LogEvent.ELogLevel.Info, "ExportProcess"); + XmlData.CreateBackup(outFile, true, "ExportProcess", true); } + LogTarget.StepStarted(2, $"Packing {ImportExportSettings.ZipFile} ({filenames.Length} files)"); // 'using' statements guarantee the stream is closed properly which is a big source // of problems otherwise. Its exception safe as well which is great. using (ZipOutputStream s = new ZipOutputStream(File.Create(outFile))) { - s.SetLevel(9); // 0 - store only to 9 - means best compression - s.Password = MemberImportSettings.ZipPassword; //null is a desired value for "no encryption" + s.SetLevel(9); // 0 - store only ... 9 - best compression + s.Password = ImportExportSettings.ZipPassword; //null is a desired value for "no encryption" byte[] buffer = new byte[4096]; foreach (string file in filenames) { - Console.WriteLine(file); + //LogTarget.LogLine($"Packing {file}", LogEvent.ELogLevel.Trace, "ExportProcess"); + // Using GetFileName makes the result compatible with XP // as the resulting path is not absolute. var entry = new ZipEntry(Path.GetFileName(file)); @@ -93,35 +115,104 @@ namespace dezentrale.core } catch (Exception ex) { - Console.WriteLine($"Exception during export: {ex.Message}"); + LogTarget.LogLine($"Exception during packing: {ex.Message}", LogEvent.ELogLevel.Error, "ExportProcess"); // No need to rethrow the exception as for our purposes its handled. return false; } - if (MemberImportSettings.GpgEnabled) + if (ImportExportSettings.GpgEnabled) { - Console.WriteLine($"Export: Using GPG to encrypt {Path.Combine(OutputDir, MemberImportSettings.GpgFile)}"); - if (!Gpg($"--output \"{Path.Combine(OutputDir, MemberImportSettings.GpgFile)}\" -c \"{tmpFile}\"", MemberImportSettings.GpgPassword)) return false; - tmpFile = Path.Combine(OutputDir, MemberImportSettings.GpgFile); + LogTarget.StepStarted(3, $"Encrypting {ImportExportSettings.GpgFile}"); + LogTarget.LogLine($"Using GPG to encrypt {Path.Combine(OutputDir, ImportExportSettings.GpgFile)}", LogEvent.ELogLevel.Info, "ExportProcess"); + if (!Gpg($"--output \"{Path.Combine(OutputDir, ImportExportSettings.GpgFile)}\" -c \"{outFile}\"", ImportExportSettings.GpgPassword, LogTarget)) return false; + outFile = Path.Combine(OutputDir, ImportExportSettings.GpgFile); } - if (MemberImportSettings.HgEnabled) + + LogTarget.StepStarted(4, $"Committing and pushing"); + if (ImportExportSettings.HgEnabled) { - Console.WriteLine($"Export: Using HG to commit / push {tmpFile}"); + LogTarget.LogLine($"Using HG to commit / push {outFile}", LogEvent.ELogLevel.Info, "ExportProcess"); //this might fail as repo might be existing / file might be already in repo - RunProcess("hg", "init", OutputDir); - RunProcess("hg", $"add {tmpFile}", OutputDir); + RunProcess("hg", "init", OutputDir, LogTarget); + RunProcess("hg", $"add {outFile}", OutputDir, LogTarget); //now, committing is more interesting if (!RunProcess("hg", "commit" + $" --message \"dezentrale-members.exe --mode=export\nProgram version={Program.VersionString}\"" - + $" --user \"{MemberImportSettings.HgUserName}\"", OutputDir)) + + $" --user \"{ImportExportSettings.HgUserName}\"", OutputDir, LogTarget)) return false; - if (!RunProcess("hg", $"--config auth.rc.prefix={MemberImportSettings.HgURL} --config auth.rc.username={MemberImportSettings.HgUserName} --config auth.rc.password={MemberImportSettings.HgPassword} push {MemberImportSettings.HgURL}", OutputDir)) + if (!RunProcess("hg", $"--config auth.rc.prefix={ImportExportSettings.HgURL} --config auth.rc.username={ImportExportSettings.HgUserName} --config auth.rc.password={ImportExportSettings.HgPassword} push {ImportExportSettings.HgURL}", OutputDir, LogTarget)) return false; } + + + //TBD: Validate + LogTarget.StepStarted(5, $"Validating the exported data"); + + //extract all to + /*string verifyDirectory = Path.Combine(memberDirectory, "verify"); + try { Directory.CreateDirectory(verifyDirectory); } + catch (Exception ex) { Console.WriteLine($"Error while creating verify directory:\n{ex.Message}"); return false; } + + //Move all *.xml to *.bak in verify directory + string[] filenames = Directory.GetFiles(verifyDirectory, "*.xml"); + foreach (string f in filenames) + { + string fileName = Path.Combine(verifyDirectory, f); + string backupName = $"{fileName}.bak"; + if (File.Exists(backupName)) File.Delete(backupName); + File.Move(fileName, backupName); + File.Delete(fileName); + } + + if (!Import(verifyDirectory, outputDir)) return false; + + try + { + Console.WriteLine("Verify: Checking files"); + filenames = Directory.GetFiles(memberDirectory, "*.xml"); + foreach (string fileWithPath in filenames) + { + string fileName = Path.GetFileName(fileWithPath); + Console.WriteLine($"Checking for {fileName}"); + string origFile = Path.Combine(memberDirectory, fileName); + string compareFile = Path.Combine(verifyDirectory, fileName); + if (!File.Exists(compareFile)) + { + Console.WriteLine($"File doesn't exist: {compareFile}"); + return false; + } + if (!FilesCompare(origFile, compareFile)) + { + Console.WriteLine($"File comparison failed between: \"{compareFile}\" and \"{compareFile}\""); + return false; + } + } + filenames = Directory.GetFiles(verifyDirectory, "*.xml"); + foreach (string fileWithPath in filenames) + { + string fileName = Path.GetFileName(fileWithPath); + string origFile = Path.Combine(memberDirectory, fileName); + string compareFile = Path.Combine(verifyDirectory, fileName); + if (!File.Exists(origFile)) + { + Console.WriteLine($"Found extra xml in verify folder: {compareFile}"); + return false; + } + } + Console.WriteLine("Verify: Done. All OK"); + } + catch (Exception ex) + { + Console.WriteLine($"Exception during verify: {ex.Message}"); + + // No need to rethrow the exception as for our purposes its handled. + return false; + }*/ + return true; } } diff --git a/core/ImportExportBase.cs b/core/ImportExportBase.cs index c56e208..1950896 100644 --- a/core/ImportExportBase.cs +++ b/core/ImportExportBase.cs @@ -6,15 +6,15 @@ namespace dezentrale.core { public abstract class ImportExportBase : BackgroundProcess { - public model.MemberImportExport MemberImportSettings { get; set; } = null; + public model.MemberImportExport ImportExportSettings { get; set; } = null; public string MemberDir { get; set; } = null; //! Directory that holds the application settings public string InputDir { get; set; } = null; //! Import working directory public string OutputDir { get; set; } = null; //! Export working directory - public static bool Gpg(string args, string gpgPassword) + public static bool Gpg(string args, string gpgPassword, core.ILogger log = null) { string argsFull = $"--batch --yes --passphrase \"{gpgPassword}\" {args}"; - return RunProcess("gpg", argsFull); + return RunProcess("gpg", argsFull, ".", log); } public static bool RunProcess(string cmd, string args, string workingDir = ".", core.ILogger log = null) @@ -24,7 +24,7 @@ namespace dezentrale.core if (log != null) { log.LogLine($"Running {cmd}", model.LogEvent.ELogLevel.Info, "RunProcess"); - log.LogRaw($"args: {cmd} {args}"); + log.LogRaw($"args: {args}"); log.LogRaw($"dir: {workingDir}"); } else @@ -41,10 +41,28 @@ namespace dezentrale.core p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardError = true; p.StartInfo.CreateNoWindow = true; - p.Start(); - string stdout = p.StandardOutput.ReadToEnd(); - string stderr = p.StandardError.ReadToEnd(); - p.WaitForExit(); + + string stdout; + string stderr; + bool toReturn; + //For testing the import/export, the versioning can be omitted by using gpgOnly = true. + bool gpgOnly = false; + + if (gpgOnly && !cmd.Equals("gpg")) + { + stdout = "std\r\nout"; + stderr = "std\r\nerr"; + Thread.Sleep(2000); + toReturn = true; + } else + { + p.Start(); + stdout = p.StandardOutput.ReadToEnd(); + stderr = p.StandardError.ReadToEnd(); + p.WaitForExit(); + toReturn = p.ExitCode == 0; + } + if (stdout.Length > 0) { if (log != null) log.LogRaw(stdout); @@ -65,7 +83,7 @@ namespace dezentrale.core Console.WriteLine(); } } - return p.ExitCode == 0; + return toReturn; } catch (Exception ex) { diff --git a/core/ImportProcess.cs b/core/ImportProcess.cs index a5f34b7..e0a6025 100644 --- a/core/ImportProcess.cs +++ b/core/ImportProcess.cs @@ -11,28 +11,39 @@ namespace dezentrale.core public ImportProcess() { Caption = "Database import"; - Steps = 4; + Steps = 5; } protected override bool Run() { LogTarget.StepStarted(0, "Preparing import"); - if (MemberImportSettings == null) + if (ImportExportSettings == null) { LogTarget.LogLine("MemberImportExport data class not set.", LogEvent.ELogLevel.Error, "ImportProcess"); return false; } + if (MemberDir == null) { LogTarget.LogLine("Member directory not set.", LogEvent.ELogLevel.Error, "ImportProcess"); return false; + } else if (!Directory.Exists(MemberDir)) + { + LogTarget.LogLine($"Cannot find directory '{MemberDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; } + if (InputDir == null) { LogTarget.LogLine("Input directory not set.", LogEvent.ELogLevel.Error, "ImportProcess"); return false; } + else if (!Directory.Exists(InputDir)) + { + LogTarget.LogLine($"Cannot find directory '{InputDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); + return false; + } @@ -40,63 +51,50 @@ namespace dezentrale.core LogTarget.StepStarted(1, "Fetching repository"); - if (MemberImportSettings.HgEnabled) + if (ImportExportSettings.HgEnabled) { RunProcess("hg", "init", InputDir, LogTarget); RunProcess("hg", "update null", InputDir, LogTarget); - if (!RunProcess("hg", $"--config auth.rc.prefix={MemberImportSettings.HgURL} --config auth.rc.username={MemberImportSettings.HgUserName} --config auth.rc.password={MemberImportSettings.HgPassword} pull {MemberImportSettings.HgURL}", InputDir, LogTarget)) + if (!RunProcess("hg", $"--config auth.rc.prefix={ImportExportSettings.HgURL} --config auth.rc.username={ImportExportSettings.HgUserName} --config auth.rc.password={ImportExportSettings.HgPassword} pull {ImportExportSettings.HgURL}", InputDir, LogTarget)) return false; if (!RunProcess("hg", $"update", InputDir, LogTarget)) return false; } - if (MemberImportSettings.GpgEnabled) + + if (ImportExportSettings.GpgEnabled) { - Console.WriteLine($"Import: Using GPG to decrypt {Path.Combine(InputDir, MemberImportSettings.GpgFile)}"); - if (!Gpg($"--output {Path.Combine(InputDir, MemberImportSettings.ZipFile)} --decrypt {Path.Combine(InputDir, MemberImportSettings.GpgFile)}", MemberImportSettings.GpgPassword)) return false; - } - if (!Directory.Exists(MemberDir)) - { - LogTarget.LogLine($"Cannot find directory '{MemberDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); - return false; - } - if (!Directory.Exists(InputDir)) - { - LogTarget.LogLine($"Cannot find directory '{InputDir}'", LogEvent.ELogLevel.Error, "ImportProcess"); - return false; + LogTarget.StepStarted(2, $"Decrypting {ImportExportSettings.GpgFile}"); + LogTarget.LogLine($"Import: Using GPG to decrypt {Path.Combine(InputDir, ImportExportSettings.GpgFile)}", LogEvent.ELogLevel.Info, "ImportProcess"); + if (!Gpg($"--output {Path.Combine(InputDir, ImportExportSettings.ZipFile)} --decrypt {Path.Combine(InputDir, ImportExportSettings.GpgFile)}", ImportExportSettings.GpgPassword, LogTarget)) return false; } try { - LogTarget.StepStarted(2, $"Backupping Contents of {MemberDir}"); + LogTarget.StepStarted(3, $"Backupping Contents of {MemberDir}"); string[] filenames = Directory.GetFiles(MemberDir, "*.xml"); - LogTarget.LogLine($"Renaming {filenames.Length} xml files to xml.bak in {MemberDir}", LogEvent.ELogLevel.Info, "ImportProcess"); + LogTarget.LogLine($"Backing up all xml in {MemberDir} ({filenames.Length} files)", LogEvent.ELogLevel.Info, "ImportProcess"); foreach (string f in filenames) { - - string fileName = Path.GetFileName(f); - string backupName = $"{fileName}.bak"; - if (File.Exists(backupName)) - { - LogTarget.LogLine($"Deleting previous {backupName}", LogEvent.ELogLevel.Trace, "ImportProcess"); - File.Delete(backupName); - } - LogTarget.LogLine($"Renaming {fileName} ({f}) to {backupName}", LogEvent.ELogLevel.Trace, "ImportProcess"); - File.Move(fileName, backupName); - - LogTarget.LogLine($"Deleting backupped {fileName}", LogEvent.ELogLevel.Trace, "ImportProcess"); - File.Delete(fileName); + //LogTarget.LogLine($"Creating backup of {f}", LogEvent.ELogLevel.Trace, "ImportProcess"); + XmlData.CreateBackup(f, false, "DbImport", true); } + foreach (string f in filenames) + { + //LogTarget.LogLine($"Deleting backupped {f}", LogEvent.ELogLevel.Trace, "ImportProcess"); + File.Delete(f); + } + System.Threading.Thread.Sleep(10); - LogTarget.StepStarted(3, $"Unpacking {MemberImportSettings.ZipFile}"); - LogTarget.LogLine($"Extracting {Path.Combine(InputDir, MemberImportSettings.ZipFile)}", LogEvent.ELogLevel.Info, "ImportProcess"); - string inFile = Path.Combine(InputDir, MemberImportSettings.ZipFile); + LogTarget.StepStarted(4, $"Unpacking {ImportExportSettings.ZipFile}"); + LogTarget.LogLine($"Extracting {Path.Combine(InputDir, ImportExportSettings.ZipFile)}", LogEvent.ELogLevel.Info, "ImportProcess"); + string inFile = Path.Combine(InputDir, ImportExportSettings.ZipFile); using (ZipInputStream s = new ZipInputStream(File.OpenRead(inFile))) { - s.Password = MemberImportSettings.ZipPassword; + s.Password = ImportExportSettings.ZipPassword; ZipEntry theEntry; while ((theEntry = s.GetNextEntry()) != null) @@ -136,7 +134,7 @@ namespace dezentrale.core } } } - LogTarget.StepCompleted(3, $"Unpacking {MemberImportSettings.ZipFile}", true); + LogTarget.StepCompleted(4, $"Unpacking {ImportExportSettings.ZipFile}", true); } catch (Exception ex) { diff --git a/model/Configuration.cs b/model/Configuration.cs index 527ee17..f336f46 100644 --- a/model/Configuration.cs +++ b/model/Configuration.cs @@ -13,6 +13,8 @@ namespace dezentrale.model [XmlElement] public MemberImportExport ImportExport{ get; set; } = new MemberImportExport(); [XmlElement] public string DbDirectory { get; set; } = DefaultDbDirectory; + //[XmlElement] public string DbBackupDirectory { get; set; } = DefaultDbBackupDirectory; + //[XmlElement] public string ImportExportDirectory { get; set; } = DefaultImportExportDirectory; [XmlElement] public uint RegularPaymentAmount { get; set; } = 3200; //cents [XmlElement] public string RegularPaymentCurrency { get; set; } = "EUR"; [XmlElement] public string LocalUser { get; set; } = "John Doe"; @@ -26,6 +28,8 @@ namespace dezentrale.model [XmlElement] public List MoneyTransferRegEx { get; set; } = new List(); //This doesn't belong here! Move to new file within db-data! [XmlElement] public DateTime LastCronjobRun { get; set; } = DateTime.Now; //This doesn't belong here! Move to new file within db-data! - [XmlIgnore] public static string DefaultDbDirectory{ get; private set; } = "db-data"; + [XmlIgnore] public static string DefaultDbDirectory { get; private set; } = "db-data"; + //[XmlIgnore] public static string DefaultDbBackupDirectory { get; private set; } = "db-backup"; + //[XmlIgnore] public static string DefaultImportExportDirectory { get; private set; } = "import-export"; } } diff --git a/model/MemberImportExport.cs b/model/MemberImportExport.cs index ad5fc72..e2b5ba9 100644 --- a/model/MemberImportExport.cs +++ b/model/MemberImportExport.cs @@ -21,34 +21,6 @@ namespace dezentrale.model [XmlElement] public string GitUserName { get; set; } = ""; [XmlElement] public string GitPassword { get; set; } = ""; - [Obsolete] - public bool Export(string memberDirectory, string outputDir) - { - throw new NotImplementedException(); - } - -[Obsolete] - public bool Import(string memberDirectory, string inputDir) - { - throw new NotImplementedException(); - /*if(HgEnabled) - { - RunProcess("hg", "init", inputDir); - RunProcess("hg", "update null", inputDir); - if (!RunProcess("hg", $"--config auth.rc.prefix={HgURL} --config auth.rc.username={HgUserName} --config auth.rc.password={HgPassword} pull {HgURL}", inputDir)) - return false; - if (!RunProcess("hg", $"update", inputDir)) - return false; - } - if (GpgEnabled) - { - Console.WriteLine($"Import: Using GPG to decrypt {Path.Combine(inputDir, GpgFile)}"); - if (!Gpg($"--output {Path.Combine(inputDir, ZipFile)} --decrypt {Path.Combine(inputDir, GpgFile)}", GpgPassword)) return false; - } - Console.WriteLine($"Import: Extracting {Path.Combine(inputDir, ZipFile)}"); - if (ZipImport(memberDirectory, inputDir) != true) return false; - return true;*/ - } private bool FilesCompare(string file1, string file2) { try @@ -68,85 +40,5 @@ namespace dezentrale.model } return true; } - [Obsolete] - public bool VerifyExport(string memberDirectory, string outputDir) - { - if (!Directory.Exists(memberDirectory)) - { - Console.WriteLine($"Cannot find directory '{memberDirectory}'"); - return false; - } - if (!Directory.Exists(outputDir)) - { - Console.WriteLine($"Cannot find directory '{outputDir}'"); - return false; - } - - Console.WriteLine("Verifying exported"); - //extract all to - string verifyDirectory = Path.Combine(memberDirectory, "verify"); - try { Directory.CreateDirectory(verifyDirectory); } - catch (Exception ex) { Console.WriteLine($"Error while creating verify directory:\n{ex.Message}"); return false; } - - //Move all *.xml to *.bak in verify directory - string[] filenames = Directory.GetFiles(verifyDirectory, "*.xml"); - foreach (string f in filenames) - { - string fileName = Path.Combine(verifyDirectory, f); - string backupName = $"{fileName}.bak"; - if (File.Exists(backupName)) File.Delete(backupName); - File.Move(fileName, backupName); - File.Delete(fileName); - } - - if (!Import(verifyDirectory, outputDir)) return false; - - try - { - Console.WriteLine("Verify: Checking files"); - filenames = Directory.GetFiles(memberDirectory, "*.xml"); - foreach (string fileWithPath in filenames) - { - string fileName = Path.GetFileName(fileWithPath); - Console.WriteLine($"Checking for {fileName}"); - string origFile = Path.Combine(memberDirectory, fileName); - string compareFile = Path.Combine(verifyDirectory, fileName); - if (!File.Exists(compareFile)) - { - Console.WriteLine($"File doesn't exist: {compareFile}"); - return false; - } - if (!FilesCompare(origFile, compareFile)) - { - Console.WriteLine($"File comparison failed between: \"{compareFile}\" and \"{compareFile}\""); - return false; - } - } - filenames = Directory.GetFiles(verifyDirectory, "*.xml"); - foreach (string fileWithPath in filenames) - { - string fileName = Path.GetFileName(fileWithPath); - string origFile = Path.Combine(memberDirectory, fileName); - string compareFile = Path.Combine(verifyDirectory, fileName); - if (!File.Exists(origFile)) - { - Console.WriteLine($"Found extra xml in verify folder: {compareFile}"); - return false; - } - } - Console.WriteLine("Verify: Done. All OK"); - } - catch (Exception ex) - { - Console.WriteLine($"Exception during verify: {ex.Message}"); - - // No need to rethrow the exception as for our purposes its handled. - return false; - } - - return true; - } - - } } diff --git a/model/XmlData.cs b/model/XmlData.cs index b235dc7..24e31d5 100644 --- a/model/XmlData.cs +++ b/model/XmlData.cs @@ -10,7 +10,36 @@ namespace dezentrale.model public class XmlData { [XmlAttribute] public DateTime LastChanged { get; set; } = DateTime.Now; - [XmlAttribute] public string ProgramVersion { get; set; } = ""; + [XmlAttribute] public string ProgramVersion { get; set; } = ""; + + /** \brief Move or make a backup copy of the specified file to the same folder. Will overwrite old backups if @ref permanentWithTimestamp is false + * \param name="fileWithPath" Source file to copy. File must exist. + * \param name="backupName" Optional name for a backup. E.g. "PreImport" or "NewProgVersion" + * \param param name="permanentWithTimestamp" Stores the backup including a timestamp (@ref DateTime.Now) and fails if the target file already exists. + * \return the resulting name on success. + * \note this method throws Exceptions when failing. + */ + public static string CreateBackup(string fileWithPath, bool fileMove = true, string backupName = null, bool permanentWithTimestamp = false) + { + if (!File.Exists(fileWithPath)) throw new FileNotFoundException(fileWithPath); + string backupFile = fileWithPath; + if (backupName != null) backupFile += "." + backupName; + if (permanentWithTimestamp) backupFile += "." + DateTime.Now.ToString("yyMMdd_HHmmss"); + backupFile += ".bak"; + + if (File.Exists(backupFile)) + { + if (permanentWithTimestamp) + throw new IOException($"File {backupFile} already exists"); + + File.Delete(backupFile); + } + + if (fileMove) File.Move(fileWithPath, backupFile); + else File.Copy(fileWithPath, backupFile); + + return backupName; + } public static XmlData LoadFromFile(string fileName, Type type = null) { @@ -26,10 +55,14 @@ namespace dezentrale.model { Console.WriteLine($"Object of type {ds.GetType()} was stored in Version {ds.ProgramVersion:X8}, Re-Storing with {Program.VersionNumber:X8}"); //backup file - string backupName = $"{fileName}.v{ds.ProgramVersion:X8}.bak"; - if (File.Exists(backupName)) File.Delete(backupName); - File.Move(fileName, backupName); - if (!SaveToFile(fileName, ds)) throw new Exception("Saving returned false"); + try + { + CreateBackup(fileName, true, $"v{ds.ProgramVersion:X8}", true); + if (!SaveToFile(fileName, ds)) throw new Exception("Saving returned false"); + } catch(Exception e2) + { + Console.WriteLine($"Error {e2.GetType()}: {e2.Message}"); + } } return ds; } @@ -48,9 +81,10 @@ namespace dezentrale.model if (File.Exists(fileName)) { - string backupName = $"{fileName}.bak"; - if (File.Exists(backupName)) File.Delete(backupName); - File.Move(fileName, backupName); + CreateBackup(fileName); + //string backupName = $"{fileName}.bak"; + //if (File.Exists(backupName)) File.Delete(backupName); + //File.Move(fileName, backupName); } Console.WriteLine($"XmlData.SaveToFile({ds.GetType()})"); ds.ProgramVersion = Program.VersionString; diff --git a/view/frmMain.cs b/view/frmMain.cs index c0fed26..a3a25ed 100644 --- a/view/frmMain.cs +++ b/view/frmMain.cs @@ -121,16 +121,14 @@ namespace dezentrale.view } private void mnuMain_File_Export(object sender, EventArgs e) { - if (!Program.config.ImportExport.Export(Program.config.DbDirectory, Program.DmDirectory)) - { - MessageBox.Show("Export failed!"); - return; - } - if (!Program.config.ImportExport.VerifyExport(Program.config.DbDirectory, Program.DmDirectory)) - { - MessageBox.Show("Export verify failed!"); - return; - } + ExportProcess export = new ExportProcess() + { + ImportExportSettings = Program.config.ImportExport, + MemberDir = Program.config.DbDirectory, + OutputDir = Program.DmDirectory, + }; + frmProcessWithLog frmImport = new frmProcessWithLog(export, false); + frmImport.ShowDialog(); } private void mnuMain_File_Import(object sender, EventArgs e) @@ -140,7 +138,7 @@ namespace dezentrale.view { ImportProcess import = new ImportProcess() { - MemberImportSettings = Program.config.ImportExport, + ImportExportSettings = Program.config.ImportExport, MemberDir = Program.config.DbDirectory, InputDir = Program.DmDirectory, };