using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Tango.Core; using Tango.PPC.Common.Application; using Tango.PPC.Common.BackupRestore; using Tango.Core.ExtensionMethods; using Tango.Core.DI; using Tango.PPC.Common.Connection; using System.IO; using Tango.Core.Helpers; using Tango.BL; using Tango.Settings; using Tango.Core.DB; using System.Data.SqlClient; using Ionic.Zip; using Tango.BL.Entities; using Tango.PPC.Common.Authentication; using Tango.Integration.Upgrade; using Tango.Core.IO; using Tango.BL.Enumerations; namespace Tango.PPC.Common.BackupRestore { public class DefaultBackupManager : ExtendedObject, IBackupManager { private const string BACKUP_FILE_NAME = "Backup.json"; private const string DATABASE_FILE_NAME = "Tango.bak"; private const int VERSION = 1; private const string PASSWORD = "1Creativity"; [TangoInject(TangoInjectMode.WhenAvailable)] private IPPCApplicationManager _applicationManager; [TangoInject(TangoInjectMode.WhenAvailable)] private IMachineProvider _machineProvider; [TangoInject(TangoInjectMode.WhenAvailable)] private IAuthenticationProvider _authenticationProvider; public DefaultBackupManager() { TangoIOC.Default.Inject(this); } public event EventHandler Progress; public Task CreateBackup(string filePath, String name, BackupSettings settings) { return Task.Factory.StartNew(() => { var tempFolder = TemporaryManager.CreateFolder(); try { //Basic LogManager.Log($"Starting backup operation to file '{filePath}'..."); LogManager.Log($"Backup settings:\n{settings.ToJsonString()}"); OnProgress(BackupRestoreStage.Initializing); LogManager.Log($"Temporary folder created on {tempFolder.Path}."); BackupFile backupFile = new BackupFile(); backupFile.Version = VERSION; backupFile.Date = DateTime.Now; backupFile.Settings = settings; backupFile.Name = name; backupFile.MachineSerialNumber = _machineProvider.Machine.SerialNumber; backupFile.MachineGuid = _machineProvider.Machine.Guid; backupFile.ApplicationVersion = _applicationManager.Version.ToString(); //Firmware try { LogManager.Log("Extracting firmware version from local tfp package..."); using (var st = File.OpenRead(Path.Combine(AssemblyHelper.GetCurrentAssemblyFolder(), "firmware_package.tfp"))) { backupFile.FirmwareVersion = _machineProvider.MachineOperator.GetFirmwarePackageInfo(st).Result.FileDescriptors.SingleOrDefault(x => x.Destination == PMR.FirmwareUpgrade.VersionFileDestination.Mcu).Version; } } catch (Exception ex) { throw new FileLoadException("Could extract the firmware version from the TFP package.", ex); } LogManager.Log($"Backup file generated:\n{backupFile.ToJsonString()}"); if (settings.Mode == BackupMode.Jobs) { //Jobs LogManager.Log("Starting jobs backup..."); OnProgress(BackupRestoreStage.BackingupJobs); using (ObservablesContext db = ObservablesContext.CreateDefault()) { var jobs = db.Jobs.ToList(); foreach (var job in jobs) { try { LogManager.Log($"Backing up job '{job.Name}'..."); var jobFile = job.ToJobFile().Result; backupFile.JobFiles.Add(jobFile); OnProgress(BackupRestoreStage.BackingupJobs, jobs.IndexOf(job) + 1, jobs.Count, false); } catch (Exception ex) { throw new InvalidOperationException($"Error extracting job {job.Name}.", ex); } } } LogManager.Log("Jobs backup completed."); } else { //User Settings LogManager.Log("Backing up application settings..."); OnProgress(BackupRestoreStage.BackingupSettings); backupFile.SettingsFile = File.ReadAllText(SettingsManager.Default.FilePath); //Application Version LogManager.Log("Backing up application files..."); OnProgress(BackupRestoreStage.BackingupApplication); try { PathHelper.CopyDirectory(AssemblyHelper.GetCurrentAssemblyFolder(), tempFolder, true, (current, total) => { OnProgress(BackupRestoreStage.BackingupApplication, current, total, false); }); } catch (Exception ex) { throw new IOException($"Error occurred while copying application files.", ex); } //Database LogManager.Log("Backing up database..."); OnProgress(BackupRestoreStage.BackingupDatabase); try { var dataSource = ObservablesContext.GetActualDataSource(); using (var dbManager = DbManager.FromDataSource(dataSource)) { Directory.CreateDirectory("C:\\Backups"); var dbBackupFile = $"C:\\Backups\\{DATABASE_FILE_NAME}"; if (File.Exists(dbBackupFile)) { File.Delete(dbBackupFile); } dbManager.Backup(dataSource.Catalog, dbBackupFile); File.Move(dbBackupFile, Path.Combine(tempFolder, DATABASE_FILE_NAME)); } } catch (Exception ex) { throw new IOException("Error creating database backup", ex); } LogManager.Log("Database backup completed."); } //Backup.json try { OnProgress(BackupRestoreStage.WritingConfiguration); var backupFilePath = Path.Combine(tempFolder, BACKUP_FILE_NAME); LogManager.Log($"Writing backup configuration file '{backupFilePath}'..."); File.WriteAllText(backupFilePath, backupFile.ToJsonString()); } catch (Exception ex) { throw new IOException("Error writing backup configuration file.", ex); } //Compression LogManager.Log($"Generating {filePath}..."); using (ZipFile zip = new ZipFile()) { zip.Password = PASSWORD; zip.AddDirectory(tempFolder); zip.SaveProgress += (x, e) => { if (e.EventType == ZipProgressEventType.Saving_AfterWriteEntry) { LogManager.Log($"Compressing '{e.CurrentEntry.FileName}'..."); OnProgress(BackupRestoreStage.CompressingFiles, e.EntriesSaved + 1, e.EntriesTotal, false); } }; zip.ParallelDeflateThreshold = -1; zip.Save(filePath); } //Done LogManager.Log("Backup operation completed!!!"); OnProgress(BackupRestoreStage.Done, 100, 100, false); tempFolder.Delete(); } catch (Exception ex) { tempFolder.Delete(); OnProgress(BackupRestoreStage.Error, 100, 100, false); LogManager.Log(ex, "Could not complete the backup operation."); throw ex; } }); } /// /// Extracts the backup configuration from the specified backup file. /// /// The file path. /// public Task ExtractBackupConfiguration(string filePath) { return Task.Factory.StartNew(() => { using (ZipFile zip = ZipFile.Read(filePath)) { zip.Password = PASSWORD; var reader = zip.Entries.SingleOrDefault(x => x.FileName == BACKUP_FILE_NAME).OpenReader(); String json = String.Empty; using (StreamReader stReader = new StreamReader(reader)) { json = stReader.ReadToEnd(); } var backupFile = BackupFile.FromJson(json); reader.Close(); reader.Dispose(); return backupFile; } }); } public Task Restore(string filePath, RestoreSettings settings) { TaskCompletionSource completionSource = new TaskCompletionSource(); String dbRollbackFile = null; bool shouldRollback = false; Task.Factory.StartNew(() => { LogManager.Log($"Starting restore operation from file '{filePath}'..."); OnProgress(BackupRestoreStage.Initializing); var tempFolder = TemporaryManager.CreateFolder(); tempFolder.Persist = true; var restoreResult = new RestoreResult() { FolderPath = tempFolder }; try { LogManager.Log("Extracting backup file configuration..."); BackupFile backupFile = null; //Extract Configuration try { OnProgress(BackupRestoreStage.ExtractingBackupConfiguration); backupFile = ExtractBackupConfiguration(filePath).Result; restoreResult.BackupFile = backupFile; LogManager.Log($"Backup settings:\n{backupFile.Settings.ToJsonString()}"); } catch (Exception ex) { throw new IOException("Error extracting backup configuration.", ex); } //Validate Version if (backupFile.Version > VERSION) { throw new NotSupportedException($"Backup file version {backupFile} is not supported."); } //Validate Machine Serial Number if (backupFile.MachineGuid != null) { if (backupFile.Settings.Mode == BackupMode.Full && backupFile.MachineGuid != _machineProvider.Machine.Guid) { throw new InvalidOperationException($"The specified backup file targets machine '{backupFile.MachineSerialNumber}'. Cannot perform the restore operation."); } } //Validate Machine State LogManager.Log("Validating machine state..."); OnProgress(BackupRestoreStage.ValidatingMachineState); if (_machineProvider.MachineOperator.IsPrinting) { LogManager.Log("The machine is currently printing. Aborting!"); throw new InvalidOperationException("Cannot perform restore operation while machine is dyeing."); } if (backupFile.Settings.Mode == BackupMode.Full && _machineProvider.MachineOperator.State != Transport.TransportComponentState.Connected) { LogManager.Log("Backup is configured to restore the firmware but machine is not connected!"); throw new InvalidOperationException("The restore operation is configured to restore the firmware version but the machine is currently disconnected."); } //Create Restore Point try { LogManager.Log("Creating database rollback file..."); var dataSource = ObservablesContext.GetActualDataSource(); using (var dbManager = DbManager.FromDataSource(dataSource)) { Directory.CreateDirectory("C:\\Backups"); dbRollbackFile = $"C:\\Backups\\{Path.GetRandomFileName()}.bak"; LogManager.Log($"Creating database rollback to '{dbRollbackFile}'..."); dbManager.Backup(dataSource.Catalog, dbRollbackFile); LogManager.Log("Database rollback created successfully."); shouldRollback = true; } } catch (Exception ex) { throw new InvalidDataException("Error creating database rollback file.", ex); } if (backupFile.Settings.Mode == BackupMode.Jobs) { //Restore Jobs OnProgress(BackupRestoreStage.RestoringJobs); LogManager.Log("Starting jobs restore..."); using (ObservablesContext db = ObservablesContext.CreateDefault()) { var jobs = db.Jobs.ToList(); var jobFiles = backupFile.JobFiles; if (settings.AllowDeleteJobs) { try { LogManager.Log("Removing existing jobs..."); foreach (var job in jobs.ToList()) { LogManager.Log($"Removing job '{job.Name}'..."); job.Delete(db); jobs.Remove(job); } db.SaveChanges(); } catch (Exception ex) { throw new Exception("Error removing existing jobs from database.", ex); } } foreach (var jobFile in jobFiles) { LogManager.Log($"Importing job '{jobFile.Name}'..."); try { var existingJob = jobs.FirstOrDefault(x => x.Name == jobFile.Name); if (existingJob != null) { if (settings.OverwriteExistingJobs) { try { LogManager.Log("Job already exist, overwriting..."); var newJob = Job.FromJobFile(jobFile, _machineProvider.Machine.Guid, null).Result; newJob.Guid = existingJob.Guid; existingJob.Delete(db); jobs.Remove(existingJob); db.SaveChanges(); db.Jobs.Add(newJob); db.SaveChanges(); } catch (Exception ex) { throw new InvalidOperationException("Error overwriting job.", ex); } } } else { var newJob = Job.FromJobFile(jobFile, _machineProvider.Machine.Guid, null).Result; db.Jobs.Add(newJob); } } catch (Exception ex) { throw new InvalidOperationException("Error importing job.", ex); } OnProgress(BackupRestoreStage.RestoringJobs, jobFiles.IndexOf(jobFile) + 1, jobFiles.Count, false); } OnProgress(BackupRestoreStage.RestoringJobs); db.SaveChanges(); } LogManager.Log("Jobs restored successfully."); OnProgress(BackupRestoreStage.Done, 100, 100, false); completionSource.SetResult(restoreResult); } else { //Extract zip file LogManager.Log("Starting backup file extraction..."); OnProgress(BackupRestoreStage.ExtractingContent); try { using (ZipFile zip = new ZipFile(filePath)) { zip.Password = PASSWORD; zip.ExtractProgress += (x, e) => { if (e.EventType == ZipProgressEventType.Extracting_AfterExtractEntry) { LogManager.Log($"Extracting '{e.CurrentEntry.FileName}'..."); OnProgress(BackupRestoreStage.ExtractingContent, e.EntriesExtracted + 1, e.EntriesTotal, false); } }; zip.ParallelDeflateThreshold = -1; zip.ExtractAll(tempFolder); } } catch (Exception ex) { throw new IOException("Error extracting backup content.", ex); } //Overwrite settings LogManager.Log("Validating user settings..."); if (backupFile.SettingsFile != null) { try { LogManager.Log("Overwriting settings file..."); OnProgress(BackupRestoreStage.RestoringSettings); File.WriteAllText(SettingsManager.Default.FilePath, backupFile.SettingsFile); } catch (Exception ex) { throw new IOException("Error overwriting user settings.", ex); } } else { LogManager.Log("No user settings, skipping..."); } //Restore database var backupFilePath = Path.Combine(tempFolder, DATABASE_FILE_NAME); LogManager.Log($"Looking for file database backup on '{backupFilePath}'..."); if (File.Exists(backupFilePath)) { LogManager.Log("Restoring database..."); OnProgress(BackupRestoreStage.RestoringDatabase); try { var dataSource = ObservablesContext.GetActualDataSource(); using (var dbManager = DbManager.FromDataSource(dataSource)) { Directory.CreateDirectory("C:\\Backups"); var dbBackupFile = $"C:\\Backups\\{DATABASE_FILE_NAME}"; File.Copy(backupFilePath, dbBackupFile, true); dbManager.Restore(dataSource.Catalog, dbBackupFile); File.Delete(dbBackupFile); } } catch (Exception ex) { throw new IOException("Error restoring database backup", ex); } LogManager.Log("Database backup completed."); } else { LogManager.Log("Database backup file not found, skipping..."); } //Remove extra files from application temp folder OnProgress(BackupRestoreStage.RemovingTemporaryFiles); LogManager.Log("Removing redundant files from temp folder..."); try { File.Delete(backupFilePath); } catch { } try { File.Delete(Path.Combine(tempFolder, BACKUP_FILE_NAME)); } catch { } //Update firmware var tfpFile = Path.Combine(tempFolder, "firmware_package.tfp"); LogManager.Log($"Looking for tfp file on '{tfpFile}'..."); if (File.Exists(tfpFile)) { OnProgress(BackupRestoreStage.RestoringFirmware); LogManager.Log("Restoring firmware version..."); var stream = new FileStream(tfpFile, FileMode.Open); _machineProvider.MachineOperator.FirmwareUpgradeMode = FirmwareUpgradeModes.DFU | FirmwareUpgradeModes.TFP_PACKAGE; var handler = _machineProvider.MachineOperator.UpgradeFirmware(stream, _machineProvider.Machine.IsDemo).Result; handler.Failed += (_, ex) => { stream.Dispose(); OnRestoreException(ex, completionSource, shouldRollback, dbRollbackFile, tempFolder); }; handler.Completed += (_, __) => { OnProgress(BackupRestoreStage.RestoringFirmware, 100, 100, false); stream.Dispose(); LogManager.Log("Full backup restored successfully."); UpdateTangoVersionAfterRestore(backupFile); OnProgress(BackupRestoreStage.Done, 100, 100, false); completionSource.SetResult(restoreResult); }; handler.Canceled += (_, __) => { stream.Dispose(); OnRestoreException(new Exception("The operation has been canceled."), completionSource, shouldRollback, dbRollbackFile, tempFolder); }; handler.Progress += (_, e) => { OnProgress(BackupRestoreStage.RestoringFirmware, e.Current, e.Total, false); }; } else { LogManager.Log("Firmware package file not found, skipping..."); UpdateTangoVersionAfterRestore(backupFile); OnProgress(BackupRestoreStage.Done, 100, 100, false); completionSource.SetResult(restoreResult); } } } catch (Exception ex) { OnRestoreException(ex, completionSource, shouldRollback, dbRollbackFile, tempFolder); } }); return completionSource.Task; } private void UpdateTangoVersionAfterRestore(BackupFile backupFile) { LogManager.Log("Creating a new TangoUpdate entry after successful restore..."); try { using (ObservablesContext db = ObservablesContext.CreateDefault()) { db.TangoUpdates.Add(new TangoUpdate() { EndDate = DateTime.UtcNow, LastUpdated = DateTime.UtcNow, MachineGuid = _machineProvider.Machine.Guid, StartDate = DateTime.UtcNow, Status = (int)TangoUpdateStatuses.OfflineUpdateCompleted, ApplicationVersion = backupFile.ApplicationVersion, FirmwareVersion = backupFile.FirmwareVersion, }); db.SaveChanges(); } } catch (Exception ex) { LogManager.Log(ex, "Error creating TangoUpdate entry..."); } } private void OnRestoreException(Exception ex, TaskCompletionSource completionSource, bool shouldRollback, String dbRollbackFile, TemporaryFolder tempFolder) { if (shouldRollback) { LogManager.Log("Rolling back database changes..."); var dataSource = ObservablesContext.GetActualDataSource(); using (var dbManager = DbManager.FromDataSource(dataSource)) { try { OnProgress(BackupRestoreStage.RollingBackChanges); dbManager.Restore(dataSource.Catalog, dbRollbackFile); LogManager.Log("Database restored successfully."); } catch (Exception e) { LogManager.Log(e, "Error rolling back database."); } finally { try { File.Delete(dbRollbackFile); } catch { } } } } tempFolder.Delete(); OnProgress(BackupRestoreStage.Error, 100, 100, false); LogManager.Log(ex, "Could not complete the restore operation."); completionSource.SetException(ex); } protected virtual void OnProgress(BackupRestoreStage stage, double progress = 0, double maxProgress = 100, bool isIntermediate = true) { Progress?.Invoke(this, new BackupRestoreProgressEventArgs() { Stage = stage, Progress = progress, MaxProgress = maxProgress, IsIntermediate = isIntermediate, }); } } }