From 46bddc7c5fb3f130ac31c76564d7afb15673cdd5 Mon Sep 17 00:00:00 2001 From: Garritt McCune Date: Sun, 10 Dec 2017 02:07:52 -0600 Subject: [PATCH] Updated the backup and restore functions to be more verbose and stable. Changed the version number slightly for appearance reasons. Fixed a bug on the Main Form that prevented it from refreshing the controls properly after all years have been wiped and after a database restore. --- AdvertisingProfitControlData/Backup.cs | 96 +++++++++++++++++-- AdvertisingProfitControlData/Restore.cs | 42 +++++--- AdvertsingProfitControl/DatabaseRecovery.cs | 84 ++++++++++++++-- AdvertsingProfitControl/FrmMain.cs | 11 +++ .../Properties/AssemblyInfo.cs | 2 +- 5 files changed, 204 insertions(+), 31 deletions(-) diff --git a/AdvertisingProfitControlData/Backup.cs b/AdvertisingProfitControlData/Backup.cs index 25d98df..7234154 100644 --- a/AdvertisingProfitControlData/Backup.cs +++ b/AdvertisingProfitControlData/Backup.cs @@ -10,6 +10,12 @@ namespace AdvertisingProfitControlData { public static readonly string DefaultBackupLocation = Path.GetPathRoot(Environment.SystemDirectory) + @"Users\" + Environment.UserName + @"\AppData\Local\APC\Backups\"; public static readonly string DefaultBackupExtension = "apcdbbak"; + //Define the error states that certain operations may return. + public const byte PrimaryBackupFailed = 5; + public const byte PrimaryBackupVerifyFailed = 10; + public const byte OptionalBackupFailed = 15; + public const byte OptionalBackupVerifyFailed = 25; + public byte State; public string ErrorMessage = string.Empty; public bool SingleFileBackup(string optionalBackupFilePath = "") @@ -18,22 +24,94 @@ namespace AdvertisingProfitControlData var sqlConStrBuilder = new SqlConnectionStringBuilder(AdvertisingProfitControlDbContext.ConnectionString); try { - using (var s = new SqlConnection(sqlConStrBuilder.ConnectionString)) + using (var sqlConnection = new SqlConnection(sqlConStrBuilder.ConnectionString)) { - var query = $"BACKUP DATABASE {sqlConStrBuilder.InitialCatalog} TO DISK='{defaultBackupFile}'"; - - using (var command = new SqlCommand(query, s)) + sqlConnection.InfoMessage += (s, e) => { - s.Open(); + if (e.Errors.Count > 0) + { + State += e.Errors[e.Errors.Count - 1].State % 5 == 0 ? e.Errors[e.Errors.Count - 1].State : (byte) 0; + } + }; + //Note: To get a SqlException an error must be raised at level 16 or higher. + //Checksum command needs to be applied in order to properly verify the media as pointed out here: + //http://www.edwinmsarmiento.com/the-scariest-lie-we-believed-about-backup-verification/ + //RAISERROR(message string, level, state) levels above 11 throw SQLExceptions. + var query = + @"DECLARE @BackupSuccess BIT --1 is true 0 is false + SET @BackupSuccess = 0 + BEGIN TRY + BACKUP DATABASE " + sqlConStrBuilder.InitialCatalog + @" TO DISK = N'" + defaultBackupFile + @"' WITH NOFORMAT, NOINIT, SKIP, REWIND, NOUNLOAD, CHECKSUM + SET @BackupSuccess = 1 + RESTORE VERIFYONLY FROM DISK = N'"+ defaultBackupFile + @"' WITH NOUNLOAD, NOREWIND, CHECKSUM + END TRY + BEGIN CATCH + IF @BackupSuccess = 0 + BEGIN + RAISERROR('Backup failed', 9, " + PrimaryBackupFailed + @") + END + ELSE + BEGIN + RAISERROR('Verify failed', 9, " + PrimaryBackupVerifyFailed + @") + END + END CATCH"; + using (var command = new SqlCommand(query, sqlConnection)) + { + sqlConnection.Open(); command.ExecuteNonQuery(); - if (optionalBackupFilePath == string.Empty) return true; + var defaultBackupVerificationFailedFile = + defaultBackupFile.Insert(defaultBackupFile.IndexOf(".", StringComparison.Ordinal), + " (Verification Failed)"); + if (File.Exists(defaultBackupVerificationFailedFile)) + { + File.Delete(defaultBackupVerificationFailedFile); + } + if (State == PrimaryBackupVerifyFailed) + { + File.Move(defaultBackupFile, defaultBackupVerificationFailedFile); + } + if (optionalBackupFilePath == string.Empty) + { + return State != PrimaryBackupFailed; + } + var optionalBackupFile = optionalBackupFilePath + DateTime.Now.ToString("yy-MM-dd") + "." + DefaultBackupExtension; command.CommandText = - $"BACKUP DATABASE {sqlConStrBuilder.InitialCatalog} TO DISK='{optionalBackupFilePath + DateTime.Now.ToString("yy-MM-dd") + "." + DefaultBackupExtension}'"; + @"DECLARE @BackupSuccess BIT + SET @BackupSuccess = 0 + BEGIN TRY + BACKUP DATABASE " + sqlConStrBuilder.InitialCatalog + @" TO DISK = N'" + optionalBackupFile + @"' WITH NOFORMAT, NOINIT, SKIP, REWIND, NOUNLOAD, CHECKSUM + SET @BackupSuccess = 1 + RESTORE VERIFYONLY FROM DISK = N'" + optionalBackupFile + @"' WITH NOUNLOAD, NOREWIND, CHECKSUM + END TRY + BEGIN CATCH + IF @BackupSuccess = 0 + BEGIN + RAISERROR('Optional backup failed', 9, " + OptionalBackupFailed + @") + END + ELSE + BEGIN + RAISERROR('Optional backup verify failed', 9, " + OptionalBackupVerifyFailed + @") + END + END CATCH"; command.ExecuteNonQuery(); + var optionalBackupVerficationFailedFile = + optionalBackupFile.Insert(optionalBackupFile.IndexOf(".", StringComparison.Ordinal), + " (Verification Failed)"); + if (State == OptionalBackupFailed || State == (OptionalBackupFailed + PrimaryBackupFailed) || State == (OptionalBackupFailed + PrimaryBackupVerifyFailed)) + { + return false; + } + if (File.Exists(optionalBackupVerficationFailedFile)) + { + File.Delete(optionalBackupVerficationFailedFile); + } + if (State == OptionalBackupVerifyFailed || State == (OptionalBackupVerifyFailed + PrimaryBackupVerifyFailed) || State == (OptionalBackupVerifyFailed + PrimaryBackupFailed)) + { + File.Move(optionalBackupFile, optionalBackupVerficationFailedFile); + } + return State == PrimaryBackupFailed; } } - - return true; } catch (SqlException e) { diff --git a/AdvertisingProfitControlData/Restore.cs b/AdvertisingProfitControlData/Restore.cs index 6153338..53864e7 100644 --- a/AdvertisingProfitControlData/Restore.cs +++ b/AdvertisingProfitControlData/Restore.cs @@ -6,28 +6,42 @@ namespace AdvertisingProfitControlData { public string ErrorMessage = string.Empty; - public bool SingleFileRestore(string databaseFilePath) + public bool SingleFileRestore(string databaseFilePath, string defaultServerName) { var sqlConStrBuilder = new SqlConnectionStringBuilder(AdvertisingProfitControlDbContext.ConnectionString); - + var masterConnection = @"data source=" + defaultServerName + @";integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"; try { - using (var conn = new SqlConnection(sqlConStrBuilder.ConnectionString)) + using (var conn = new SqlConnection(masterConnection)) { - var query = $"RESTORE DATABASE {sqlConStrBuilder.InitialCatalog} FROM DISK='{databaseFilePath}'"; - + var db = new AdvertisingProfitControlDbContext(); + //Drop the database to make sure it can be recreated and restored (necessary since the database MUST be on line for its file to be wiped from disk). + db.Database.Delete(); + var query = + @"BEGIN TRY + DECLARE @DefaultDataPath varchar(max) + DECLARE @DefaultLogPath varchar(max) + SELECT @DefaultDataPath = CONVERT(varchar(max),SERVERPROPERTY('INSTANCEDEFAULTDATAPATH')) + SELECT @DefaultLogPath = CONVERT(varchar(max),SERVERPROPERTY('INSTANCEDEFAULTLOGPATH')) + SELECT @DefaultDataPath = CONCAT(@DefaultDataPath, N'" + sqlConStrBuilder.InitialCatalog + @".mdf') + SELECT @DefaultLogPath = CONCAT(@DefaultLogPath, N'" + sqlConStrBuilder.InitialCatalog + @".ldf') + IF db_id(N'" + sqlConStrBuilder.InitialCatalog + @"') IS NOT NULL + BEGIN + CREATE DATABASE " + sqlConStrBuilder.InitialCatalog + @" + END + RESTORE DATABASE " + sqlConStrBuilder.InitialCatalog + @" FROM DISK ='" + databaseFilePath + @"' WITH REPLACE, + MOVE '" + sqlConStrBuilder.InitialCatalog + @"' TO @DefaultDataPath, + MOVE '" + sqlConStrBuilder.InitialCatalog + @"_log' TO @DefaultLogPath + END TRY + BEGIN CATCH + DECLARE @Error varchar(max) + SELECT @Error = ERROR_MESSAGE() + RAISERROR(@Error, 11, 1) + END CATCH"; + //TODO: Verify that the database is good with DBCC command. conn.Open(); - var useMasterCommand = new SqlCommand("USE master", conn); - useMasterCommand.ExecuteNonQuery(); - - var alter1Cmd = new SqlCommand($"ALTER DATABASE {sqlConStrBuilder.InitialCatalog} SET Single_User WITH Rollback Immediate", conn); - alter1Cmd.ExecuteNonQuery(); - var restoreCmd = new SqlCommand(query, conn); restoreCmd.ExecuteNonQuery(); - - var alter2Cmd = new SqlCommand($"ALTER DATABASE {sqlConStrBuilder.InitialCatalog} SET Multi_User", conn); - alter2Cmd.ExecuteNonQuery(); conn.Close(); return true; } diff --git a/AdvertsingProfitControl/DatabaseRecovery.cs b/AdvertsingProfitControl/DatabaseRecovery.cs index 97c0174..95c7665 100644 --- a/AdvertsingProfitControl/DatabaseRecovery.cs +++ b/AdvertsingProfitControl/DatabaseRecovery.cs @@ -7,7 +7,8 @@ namespace AdvertsingProfitControl { public partial class DatabaseRecovery : Form { - public bool RestoredDatabase; + public bool RestoredDatabase; + private string _informationText = string.Empty; public DatabaseRecovery() { @@ -24,6 +25,11 @@ namespace AdvertsingProfitControl UpdateBackupList(); } + private void OnBackupLabelClick(object sender, EventArgs e) + { + MessageBox.Show(_informationText, @"Verification Failed Information", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + private void UpdateRestoreButtonOnRestoreLocationTextChange(object sender, EventArgs e) { restoreBackupButton.Enabled = restoreLocationTextBox.Text.Length != 0; @@ -65,6 +71,7 @@ namespace AdvertsingProfitControl { var backup = new Backup(); var customBackupLocation = string.Empty; + backupResultLabel.Click -= OnBackupLabelClick; if (!string.IsNullOrWhiteSpace(customBackupLocationTextBox.Text)) { @@ -87,13 +94,67 @@ namespace AdvertsingProfitControl if (backup.SingleFileBackup(customBackupLocation)) { - backupResultLabel.Text = @"Successfully created backup(s)."; - customBackupLocationTextBox.Text = string.Empty; + //Check the state of the backup. + if (backup.State == 0) + { + backupResultLabel.Text = @"Successfully created backup(s)."; + customBackupLocationTextBox.Text = string.Empty; + } + else + { + switch (backup.State) + { + case Backup.PrimaryBackupVerifyFailed: + backupResultLabel.Text = @"Verification failed click to learn more."; + SetVerifcationInformationText("default backup file"); + backupResultLabel.Click += OnBackupLabelClick; + break; + case Backup.OptionalBackupVerifyFailed: + backupResultLabel.Text = @"Verification failed click to learn more."; + SetVerifcationInformationText("optional backup file in '" + customBackupLocation + @"'"); + backupResultLabel.Click += OnBackupLabelClick; + break; + case Backup.PrimaryBackupVerifyFailed + Backup.OptionalBackupVerifyFailed: + backupResultLabel.Text = @"Verification failed click to learn more."; + SetVerifcationInformationText("default and optional backup (located in " + customBackupLocation + ") files"); + backupResultLabel.Click += OnBackupLabelClick; + break; + } + } } else { - MessageBox.Show(backup.ErrorMessage, @"Failed to Create Backup", MessageBoxButtons.OK, MessageBoxIcon.Error); - backupResultLabel.Text = @"Failed to create backup(s)."; + switch (backup.State) + { + case Backup.PrimaryBackupFailed: + MessageBox.Show(@"Failed to create the default backup file but if an optional backup location was supplied, that backup was created successfully.", @"Default Backup Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + backupResultLabel.Text = @"Failed to default backup."; + break; + case Backup.OptionalBackupFailed: + MessageBox.Show(@"Default backup was successfully created, however the backup to be created in '" + customBackupLocation + @"' failed to be created.", @"Optional Backup Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + backupResultLabel.Text = @"Failed to create optional backup."; + break; + case Backup.PrimaryBackupFailed + Backup.OptionalBackupFailed: + MessageBox.Show(@"Both the default and the optional backup (in the folder '" + customBackupLocation + @"') failed to be created entirely.", @"Backups Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + backupResultLabel.Text = @"Failed to create backups."; + break; + case Backup.PrimaryBackupFailed + Backup.OptionalBackupVerifyFailed: + MessageBox.Show(@"The default backup file failed to be created and the optional backup (in the folder '" + customBackupLocation + @"') failed verification by the SQL server, click the text to the left of the 'Create Backup' button to learn more.", @"Backup And Verification Errors Detected", MessageBoxButtons.OK, MessageBoxIcon.Error); + backupResultLabel.Text = @"Verification failed click to learn more."; + SetVerifcationInformationText("optional backup (located in " + customBackupLocation + ") file"); + backupResultLabel.Click += OnBackupLabelClick; + break; + case Backup.PrimaryBackupVerifyFailed + Backup.OptionalBackupFailed: + MessageBox.Show(@"The optional backup file (in the folder '" + customBackupLocation + @"') failed to be created. The default backup file failed verification by the SQL server, click the text to the left of the 'Create Backup' button to learn more.", @"Backup And Verification Errors Detected", MessageBoxButtons.OK, MessageBoxIcon.Error); + backupResultLabel.Text = @"Verification failed click to learn more."; + SetVerifcationInformationText("default backup file"); + backupResultLabel.Click += OnBackupLabelClick; + break; + default: + MessageBox.Show(backup.ErrorMessage, @"Unknown Error", MessageBoxButtons.OK, + MessageBoxIcon.Error); + break; + } } UpdateBackupList(); @@ -135,7 +196,7 @@ namespace AdvertsingProfitControl return; } - if (restore.SingleFileRestore(restoreLocationTextBox.Text)) + if (restore.SingleFileRestore(restoreLocationTextBox.Text, Properties.Settings.Default.DefaultServer)) { restoreResultLabel.Text = @"Successfully restored the database."; restoreLocationTextBox.Text = string.Empty; @@ -144,7 +205,7 @@ namespace AdvertsingProfitControl else { MessageBox.Show(restore.ErrorMessage, @"Failed to Restore Backup", MessageBoxButtons.OK, MessageBoxIcon.Error); - restoreInfoLabel.Text = @"Failed to restore the database."; + restoreResultLabel.Text = @"Failed to restore the database."; } } @@ -172,5 +233,14 @@ namespace AdvertsingProfitControl UpdateBackupList(); } + + private void SetVerifcationInformationText(string backupType) + { + _informationText = @"The recently created " + backupType + " failed verification checks by the SQL Server." + + Environment.NewLine + + @"This is shown by the '(Verification Failed)' text in the 'Current Backups' column. This means that the backup data could be damaged." + + Environment.NewLine + + @"While it is NOT recommended that you use this backup data to restore the database you may do so but it could result in data loss."; + } } } diff --git a/AdvertsingProfitControl/FrmMain.cs b/AdvertsingProfitControl/FrmMain.cs index e2c6501..e93653f 100644 --- a/AdvertsingProfitControl/FrmMain.cs +++ b/AdvertsingProfitControl/FrmMain.cs @@ -170,6 +170,7 @@ namespace AdvertsingProfitControl form.ShowDialog(); if (!form.RestoredDatabase) return; //If a database restore operation occurred then load the most recent date. + RefreshDateListing(); var db = new AdvertisingProfitControlDbContext(); var recentDate = db.WeekEndingDates.OrderByDescending(x => x.EndingDate).FirstOrDefault(); if (recentDate == null) return; @@ -892,6 +893,7 @@ namespace AdvertsingProfitControl private void RefreshDateListing() { var db = new AdvertisingProfitControlDbContext(); + monthCalendar.RemoveAllBoldedDates(); monthCalendar.BoldedDates = db.WeekEndingDates.Select(zdate => zdate.EndingDate).ToArray(); if (monthCalendar.BoldedDates.Length > 0) { @@ -902,6 +904,8 @@ namespace AdvertsingProfitControl clearCurrentActiveYearTools.Enabled = true; clearCurrentActiveYearTools.ToolTipText = string.Empty; printPreviewButton.Enabled = true; + generateReportMenuItem.ToolTipText = string.Empty; + generateReportMenuItem.Enabled = true; } else { @@ -913,6 +917,8 @@ namespace AdvertsingProfitControl clearCurrentActiveYearTools.Enabled = false; clearCurrentActiveYearTools.ToolTipText = @"There are no records to clear."; printPreviewButton.Enabled = false; + generateReportMenuItem.ToolTipText = @"There are no records to display."; + generateReportMenuItem.Enabled = false; } } @@ -956,7 +962,12 @@ namespace AdvertsingProfitControl } if (ClearSelectedDate(_currentActiveDate.Year)) { + ClearForm(); RefreshDateListing(); + var date = db.WeekEndingDates.OrderByDescending(x => x.EndingDate).FirstOrDefault(); + if (date == null) return; + if (date.EndingDate == _currentActiveDate) return; + LoadDate(date.EndingDate); } else { diff --git a/AdvertsingProfitControl/Properties/AssemblyInfo.cs b/AdvertsingProfitControl/Properties/AssemblyInfo.cs index d96e68a..5e19a28 100644 --- a/AdvertsingProfitControl/Properties/AssemblyInfo.cs +++ b/AdvertsingProfitControl/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.5.1.0")] +[assembly: AssemblyVersion("3.5.*")] [assembly: AssemblyFileVersion("3.5.1.0")]