Files
sqltoolsservice/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs
Karl Burtram f288bee294 Make nullable warnings a per file opt-in (#1842)
* Make nullable warnings a per file opt-in

* Remove unneeded compiler directives

* Remove compiler directive for User Data
2023-02-03 18:10:07 -08:00

1416 lines
52 KiB
C#

//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#nullable disable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.ServiceLayer.Connection;
using System.Diagnostics;
namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation
{
public interface IRestoreDatabaseTaskDataObject
{
string DataFilesFolder { get; set; }
string DefaultDataFileFolder { get; }
bool RelocateAllFiles { get; set; }
string LogFilesFolder { get; set; }
string DefaultLogFileFolder { get; }
List<DbFile> DbFiles { get; }
string DefaultBackupFolder { get; }
RestoreOptions RestoreOptions { get; }
string DefaultStandbyFile { get; }
bool IsTailLogBackupPossible { get; }
bool IsTailLogBackupWithNoRecoveryPossible { get; }
bool TailLogWithNoRecovery { get; set; }
string TailLogBackupFile { get; set; }
string DefaultTailLogbackupFile { get; }
RestorePlan RestorePlan { get; }
bool CloseExistingConnections { get; set; }
RestoreParams RestoreParams { get; set; }
bool BackupTailLog { get; set; }
string DefaultSourceDbName { get; }
string SourceDatabaseName { get; set; }
List<String> SourceDbNames { get; }
bool OverwriteTargetDatabase { get; set; }
string DefaultTargetDbName { get; }
string TargetDatabaseName { get; set; }
bool CanDropExistingConnections { get; }
}
/// <summary>
/// Includes the plan with all the data required to do a restore operation on server
/// </summary>
public class RestoreDatabaseTaskDataObject : SmoScriptableOperationWithFullDbAccess, IRestoreDatabaseTaskDataObject
{
private const char BackupMediaNameSeparator = ',';
private DatabaseRestorePlanner restorePlanner;
private string tailLogBackupFile;
private BackupSetsFilterInfo backupSetsFilterInfo = new BackupSetsFilterInfo();
private bool? isTailLogBackupPossible = null;
private bool? isTailLogBackupWithNoRecoveryPossible = null;
private string backupMediaList = string.Empty;
private Server server;
private static readonly DeviceType[] managedInstanceSupportedDeviceTypes = { DeviceType.Url };
private static readonly DeviceType[] defaultSupportedDeviceTypes = { DeviceType.File, DeviceType.Url };
private static readonly Dictionary<Edition, DeviceType[]> specialEngineEditionSupportedDeviceTypes = new Dictionary<Edition, DeviceType[]>
{
{ Edition.SqlManagedInstance, managedInstanceSupportedDeviceTypes },
};
public RestoreDatabaseTaskDataObject(Server server, String databaseName)
{
PlanUpdateRequired = true;
this.server = server;
this.Util = new RestoreUtil(server);
restorePlanner = new DatabaseRestorePlanner(server);
if (String.IsNullOrEmpty(databaseName))
{
this.restorePlanner = new DatabaseRestorePlanner(server);
}
else
{
this.restorePlanner = new DatabaseRestorePlanner(server, databaseName);
this.targetDbName = databaseName;
}
this.restorePlanner.TailLogBackupFile = this.Util.GetDefaultTailLogbackupFile(databaseName);
this.restoreOptions = new RestoreOptions();
//the server will send events in intervals of 5 percent
this.restoreOptions.PercentCompleteNotification = 5;
}
/// <summary>
/// Boolean indicating whether the relocate all files checkbox was checked
/// </summary>
public bool RelocateAllFiles { get; set; }
/// <summary>
/// Restore session id
/// </summary>
public string SessionId { get; set; }
public string TargetDatabaseName
{
get
{
return string.IsNullOrEmpty(targetDbName) ? DefaultSourceDbName : targetDbName;
}
set
{
this.targetDbName = value;
}
}
public bool IsValid
{
get
{
return this.Server != null && this.RestorePlanner != null && ActiveException == null;
}
}
public RestoreParams RestoreParams { get; set; }
/// <summary>
/// Database names includes in the restore plan
/// </summary>
/// <returns></returns>
public List<String> SourceDbNames
{
get
{
if (RestorePlanner.ReadHeaderFromMedia)
{
return Util.GetSourceDbNames(this.restorePlanner.BackupMediaList, this.CredentialName);
}
else
{
return Util.GetSourceDbNames();
}
}
}
/// <summary>
/// Returns the db names that have backupsets
/// </summary>
/// <returns></returns>
public List<String> GetDatabaseNamesWithBackupSets()
{
return Util.GetSourceDbNames();
}
public bool OverwriteTargetDatabase { get; set; }
public string DefaultTargetDbName
{
get
{
return Server.ConnectionContext.DatabaseName;
}
}
/// <summary>
/// Current sqlserver instance
/// </summary>
public override Server Server
{
get
{
return server;
}
}
/// <summary>
/// Recent exception that was thrown
/// Displayed at the top of the dialog
/// </summary>
public Exception ActiveException { get; set; }
public Exception CreateOrUpdateRestorePlanException { get; set; }
/// <summary>
/// Add a backup file to restore plan media list
/// </summary>
/// <param name="filePaths"></param>
/// <param name="deviceType"></deviceType>
public void AddDevices(string filePaths, DeviceType deviceType)
{
ThrowIfUnsupportedDeviceType(this.Server.EngineEdition, deviceType);
backupMediaList = filePaths;
PlanUpdateRequired = true;
if (!string.IsNullOrWhiteSpace(filePaths))
{
string[] files = filePaths.Split(BackupMediaNameSeparator);
files = files.Select(x => x.Trim()).ToArray();
foreach (var file in files)
{
if (!this.RestorePlanner.BackupMediaList.Any(x => x.Name == file))
{
this.RestorePlanner.BackupMediaList.Add(new BackupDeviceItem
{
DeviceType = deviceType,
Name = file
});
}
}
var itemsToRemove = this.RestorePlanner.BackupMediaList.Where(x => !files.Contains(x.Name)).ToList();
foreach (var item in itemsToRemove)
{
this.RestorePlanner.BackupMediaList.Remove(item);
}
}
}
private void ThrowIfUnsupportedDeviceType(Edition engineEdition, DeviceType deviceType)
{
if (!IsSupportedDeviceType(engineEdition, deviceType))
{
throw new UnsupportedDeviceTypeException(engineEdition, deviceType);
}
}
private bool IsSupportedDeviceType(Edition engineEdition, DeviceType deviceType)
{
return (defaultSupportedDeviceTypes.Contains(deviceType) && !specialEngineEditionSupportedDeviceTypes.ContainsKey(engineEdition))
|| (specialEngineEditionSupportedDeviceTypes.ContainsKey(engineEdition) && specialEngineEditionSupportedDeviceTypes[engineEdition].Contains(deviceType));
}
/// <summary>
/// Returns the last backup taken
/// </summary>
/// <returns></returns>
public string GetLastBackupTaken()
{
string lastBackup = string.Empty;
int lastSelectedIndex = GetLastSelectedBackupSetIndex();
BackupSet lastSelectedBackupSet = lastSelectedIndex >= 0 && this.RestorePlan.RestoreOperations != null && this.RestorePlan.RestoreOperations.Count > 0 ?
this.RestorePlan.RestoreOperations[lastSelectedIndex].BackupSet : null;
if (this.RestorePlanner.RestoreToLastBackup &&
lastSelectedBackupSet != null &&
this.RestorePlan.RestoreOperations.Count > 0 &&
lastSelectedBackupSet != null)
{
bool isTheLastOneSelected = lastSelectedIndex == this.RestorePlan.RestoreOperations.Count - 1;
DateTime backupTime = lastSelectedBackupSet.BackupStartDate;
string backupTimeStr = backupTime.ToLongDateString() + " " + backupTime.ToLongTimeString();
lastBackup = isTheLastOneSelected ?
string.Format(CultureInfo.CurrentCulture, SR.TheLastBackupTaken, (backupTimeStr)) : backupTimeStr;
}
else if (GetFirstSelectedBackupSetIndex() == 0 && !this.RestorePlanner.RestoreToLastBackup)
{
lastBackup = this.CurrentRestorePointInTime.Value.ToLongDateString() +
" " + this.CurrentRestorePointInTime.Value.ToLongTimeString();
}
return lastBackup;
}
public override void Execute(TaskExecutionMode mode)
{
RestorePlanToExecute = null;
UpdateRestoreTaskObject();
if (IsValid && RestorePlan.RestoreOperations != null && RestorePlan.RestoreOperations.Any())
{
GetRestorePlanForExecutionAndScript();
}
base.Execute(mode);
}
public ConnectionInfo ConnectionInfo { get; set; }
public override string ServerName
{
get
{
if (this.ConnectionInfo != null)
{
return this.ConnectionInfo.ConnectionDetails.ServerName;
}
return this.Server.Name;
}
}
public override string DatabaseName
{
get
{
return TargetDatabaseName;
}
}
/// <summary>
/// Executes the restore operations
/// </summary>
public override void Execute()
{
try
{
if (RestorePlanToExecute != null)
{
// Restore Plan should be already created and updated at this point
if (RestorePlanToExecute != null && RestorePlanToExecute.RestoreOperations.Count > 0)
{
RestorePlanToExecute.PercentComplete += (object sender, PercentCompleteEventArgs e) =>
{
OnMessageAdded(new TaskMessage { Description = $"{e.Percent}%", Status = SqlTaskStatus.InProgress });
};
RestorePlanToExecute.Execute();
}
}
else
{
throw new InvalidOperationException(SR.RestoreNotSupported);
}
}
catch(Exception ex)
{
Logger.Write(TraceEventType.Information, $"Failed to execute restore task. error: {ex.Message}");
throw;
}
finally
{
if (this.Server.ConnectionContext.IsOpen)
{
this.Server.ConnectionContext.Disconnect();
}
}
}
/// <summary>
/// Gets RestorePlan to perform restore and to script
/// </summary>
public RestorePlan GetRestorePlanForExecutionAndScript()
{
// One of the reasons the existing plan is not used and a new plan is created to execute is the order of the backupsets and
// the last backupset is important. User can choose specific backupset in the plan to restore and
// if the last backup set is not selected from the list, it means the item before that should be the real last item
// with simply removing the item from the list, the order won't work correctlly. Smo considered the last item removed and
// doesn't consider the previous item as the last item. Creating a new list and adding backpsets is the only way to make it work
// and the reason the last backup set is important is that it has to restore with recovery mode while the others are restored with no recovery
this.ActiveException = null; //Clear any existing exceptions as the plan is getting recreated.
//Clear any existing exceptions as new plan is getting recreated.
this.CreateOrUpdateRestorePlanException = null;
bool tailLogBackup = this.RestorePlanner.BackupTailLog;
if (this.PlanUpdateRequired)
{
this.RestorePlan = this.RestorePlanner.CreateRestorePlan(this.RestoreOptions);
this.Util.AddCredentialNameForUrlBackupSet(this.RestorePlan, this.CredentialName);
}
RestorePlan rp = new RestorePlan(this.Server);
rp.RestoreAction = RestoreActionType.Database;
if (this.RestorePlan != null)
{
if (this.RestorePlan.TailLogBackupOperation != null && tailLogBackup)
{
rp.TailLogBackupOperation = this.RestorePlan.TailLogBackupOperation;
}
int i = 0;
foreach (Restore res in this.RestorePlan.RestoreOperations)
{
if (this.backupSetsFilterInfo.IsBackupSetSelected(res.BackupSet))
{
rp.RestoreOperations.Add(res);
}
i++;
}
}
this.SetRestorePlanProperties(rp);
RestorePlanToExecute = rp;
return rp;
}
/// <summary>
/// For test purpose only. The restore plan that's used to execute
/// </summary>
internal RestorePlan RestorePlanToExecute { get; set; }
/// <summary>
/// Restore Util
/// </summary>
public RestoreUtil Util { get; set; }
/// <summary>
/// SMO database restore planner used to create a restore plan
/// </summary>
public DatabaseRestorePlanner RestorePlanner
{
get { return restorePlanner; }
}
public bool PlanUpdateRequired { get; private set; }
/// <summary>
/// File to backup tail log before doing the restore
/// </summary>
public string TailLogBackupFile
{
get { return tailLogBackupFile; }
set
{
if (tailLogBackupFile == null || !tailLogBackupFile.Equals(value))
{
this.RestorePlanner.TailLogBackupFile = value;
this.PlanUpdateRequired = true;
this.tailLogBackupFile = value;
}
}
}
private RestoreOptions restoreOptions;
public RestoreOptions RestoreOptions
{
get { return restoreOptions; }
}
private string dataFilesFolder = string.Empty;
/// <summary>
/// Folder for all data files when relocate all files option is used
/// </summary>
public string DataFilesFolder
{
get
{
if (string.IsNullOrEmpty(this.dataFilesFolder))
{
this.dataFilesFolder = this.DefaultDataFileFolder;
}
return this.dataFilesFolder;
}
set
{
if (this.dataFilesFolder == null || !this.dataFilesFolder.Equals(value))
{
try
{
Uri pathUri;
bool fUriCreated = Uri.TryCreate(value, UriKind.Absolute, out pathUri);
if (fUriCreated && pathUri.Scheme == "https")
{
this.dataFilesFolder = value;
}
else
{
this.dataFilesFolder = GetDirectoryName(value + PathWrapper.PathSeparatorFromServerConnection(Server.ConnectionContext));
}
if (string.IsNullOrEmpty(this.dataFilesFolder))
{
this.dataFilesFolder = this.Server.DefaultFile;
}
}
catch (Exception ex)
{
this.ActiveException = ex;
}
this.UpdateDbFiles();
}
}
}
private string logFilesFolder = string.Empty;
/// <summary>
/// Folder for all log files when relocate all files option is used
/// </summary>
public string LogFilesFolder
{
get
{
if (string.IsNullOrEmpty(this.logFilesFolder))
{
this.logFilesFolder = this.DefaultLogFileFolder;
}
return this.logFilesFolder;
}
set
{
if (this.logFilesFolder == null || !this.logFilesFolder.Equals(value))
{
try
{
Uri pathUri;
bool fUriCreated = Uri.TryCreate(value, UriKind.Absolute, out pathUri);
if (fUriCreated && pathUri.Scheme == "https")
{
this.logFilesFolder = value;
}
else
{
this.logFilesFolder = GetDirectoryName(value + PathWrapper.PathSeparatorFromServerConnection(Server.ConnectionContext));
}
if (string.IsNullOrEmpty(this.logFilesFolder))
{
this.logFilesFolder = Server.DefaultLog;
}
}
catch (Exception ex)
{
this.ActiveException = ex;
}
this.UpdateDbFiles();
}
}
}
/// <summary>
/// Determines whether [is tail log backup possible].
/// </summary>
/// <returns>
/// <c>true</c> if [is tail log backup possible]; otherwise, <c>false</c>.
/// </returns>
public bool IsTailLogBackupPossible
{
get
{
if (!isTailLogBackupPossible.HasValue)
{
//TODO: get the value from DatabaseRestorePlanner.IsTailLogBackupPossible
if (this.Server.Version.Major < 9 || String.IsNullOrEmpty(this.restorePlanner.DatabaseName))
{
isTailLogBackupPossible = false;
}
else
{
Database db = this.Server.Databases[this.RestorePlanner.DatabaseName];
if (db == null)
{
isTailLogBackupPossible = false;
}
else
{
db.Refresh();
if (db.Status != DatabaseStatus.Normal && db.Status != DatabaseStatus.Suspect && db.Status != DatabaseStatus.EmergencyMode)
{
isTailLogBackupPossible = false;
}
else if (db.RecoveryModel == RecoveryModel.Full || db.RecoveryModel == RecoveryModel.BulkLogged)
{
isTailLogBackupPossible = true;
}
else
{
isTailLogBackupPossible = false;
}
}
}
}
return isTailLogBackupPossible.Value;
}
}
/// <summary>
/// Determines whether [is tail log backup with NORECOVERY possible].
/// </summary>
/// <returns>
/// <c>true</c> if [is tail log backup with NORECOVERY possible]; otherwise, <c>false</c>.
/// </returns>
public bool IsTailLogBackupWithNoRecoveryPossible
{
get
{
if (!isTailLogBackupWithNoRecoveryPossible.HasValue)
{
//TODO: get the value from DatabaseRestorePlanner.IsTailLogBackupWithNoRecoveryPossible
string databaseName = this.RestorePlanner.DatabaseName;
if (!IsTailLogBackupPossible)
{
isTailLogBackupWithNoRecoveryPossible = false;
}
else
{
Database db = this.Server.Databases[databaseName];
if (db == null)
{
isTailLogBackupWithNoRecoveryPossible = false;
}
else if (Server.Version.Major > 10 && db.DatabaseEngineType == DatabaseEngineType.Standalone && !String.IsNullOrEmpty(db.AvailabilityGroupName))
{
isTailLogBackupWithNoRecoveryPossible = false;
}
else if (db.DatabaseEngineType == DatabaseEngineType.Standalone && db.IsMirroringEnabled)
{
isTailLogBackupWithNoRecoveryPossible = false;
}
else
{
isTailLogBackupWithNoRecoveryPossible = true;
}
}
}
return isTailLogBackupWithNoRecoveryPossible.Value;
}
}
public string DefaultStandbyFile
{
get
{
return Util.GetDefaultStandbyFile(this.RestorePlan != null ? this.RestorePlan.DatabaseName : string.Empty);
}
}
public string DefaultTailLogbackupFile
{
get
{
return Util.GetDefaultTailLogbackupFile(this.RestorePlan != null ? this.RestorePlan.DatabaseName : string.Empty);
}
}
/// <summary>
/// Gets or sets a value indicating whether [prompt before each backup].
/// </summary>
/// <value>
/// <c>true</c> if [prompt before each backup]; otherwise, <c>false</c>.
/// </value>
public bool PromptBeforeEachBackup { get; set; }
private void UpdateDbFiles()
{
try
{
foreach (DbFile dbFile in this.DbFiles)
{
string fileName = this.GetTargetDbFilePhysicalName(dbFile.PhysicalName);
if (!dbFile.DbFileType.Equals("Log"))
{
if (!string.IsNullOrEmpty(this.DataFilesFolder))
{
dbFile.PhysicalNameRelocate = CombineToServerConnectionPath(this.DataFilesFolder, fileName);
}
else
{
dbFile.PhysicalNameRelocate = fileName;
}
}
else
{
if (!string.IsNullOrEmpty(this.LogFilesFolder))
{
dbFile.PhysicalNameRelocate = CombineToServerConnectionPath(this.LogFilesFolder, fileName);
}
else
{
dbFile.PhysicalNameRelocate = fileName;
}
}
}
}
catch (Exception ex)
{
this.ActiveException = ex;
}
}
/// <summary>
/// Combining the root and file name using the server connection file path seperator
/// </summary>
private string CombineToServerConnectionPath(string root, string filePath)
{
string pathSeparatorFromServerConnection = PathWrapper.PathSeparatorFromServerConnection(Server.ConnectionContext);
string combinedPath = PathWrapper.Combine(root, filePath);
// Make sure all path seperators are server connection separator
string result = combinedPath.Replace(Path.DirectorySeparatorChar.ToString(), pathSeparatorFromServerConnection);
// Make sure there's not any double file seperator in the path
result = result.Replace(pathSeparatorFromServerConnection + pathSeparatorFromServerConnection,
pathSeparatorFromServerConnection);
return result;
}
private string GetDirectoryName(string filePath)
{
string localPath = ConvertToLocalMachinePath(filePath);
localPath = PathWrapper.GetDirectoryName(localPath);
return ConvertToServerConnectionPath(localPath);
}
private string ConvertToLocalMachinePath(string filePath) {
string pathSeparator = Path.DirectorySeparatorChar.ToString();
string localPath = filePath.Replace("/", pathSeparator);
localPath = localPath.Replace("\\", pathSeparator);
return localPath;
}
private string ConvertToServerConnectionPath(string filePath) {
string pathSeparator = PathWrapper.PathSeparatorFromServerConnection(Server.ConnectionContext);
string serverPath = filePath.Replace("/", pathSeparator);
serverPath = serverPath.Replace("\\", pathSeparator);
return serverPath;
}
/// <summary>
/// Updates the Restore folder location of those db files whose orginal directory location
/// is not present in the destination computer.
/// </summary>
internal void UpdateDBFilesPhysicalRelocate()
{
foreach (DbFile item in DbFiles)
{
string fileName = this.GetTargetDbFilePhysicalName(item.PhysicalName);
item.PhysicalNameRelocate = PathWrapper.Combine(PathWrapper.GetDirectoryName(item.PhysicalName),
fileName);
Uri pathUri;
bool fUriCreated = Uri.TryCreate(item.PhysicalNameRelocate, UriKind.Absolute, out pathUri);
if ((!fUriCreated || pathUri.Scheme != Uri.UriSchemeHttps) &&
!Directory.Exists(Path.GetDirectoryName(item.PhysicalNameRelocate)))
{
string directoryPath = string.Empty;
if (string.Compare(item.DbFileType, SR.Log, StringComparison.OrdinalIgnoreCase) == 0)
{
directoryPath = Util.GetDefaultLogFileFolder();
}
else
{
directoryPath = Util.GetDefaultDataFileFolder();
}
item.PhysicalNameRelocate = PathWrapper.Combine(directoryPath, fileName);
}
}
}
private List<DbFile> dbFiles = new List<DbFile>();
/// <summary>
/// List of files of the source database or in the backup file
/// </summary>
public List<DbFile> DbFiles
{
get { return dbFiles; }
}
internal RestorePlan restorePlan;
/// <summary>
/// Restore plan to do the restore
/// </summary>
public RestorePlan RestorePlan
{
get
{
return this.restorePlan;
}
internal set
{
this.restorePlan = value;
}
}
/// <summary>
/// The database being restored
/// </summary>
public string targetDbName = string.Empty;
/// <summary>
/// The database from the backup file used to restore to by default
/// </summary>
public string DefaultSourceDbName
{
get
{
var dbNames = SourceDbNames;
string dbName = dbNames.FirstOrDefault();
return dbName;
}
}
/// <summary>
/// Current value of source db name in the planner
/// </summary>
public string SourceDatabaseName
{
get
{
return this.RestorePlanner == null ? string.Empty : this.RestorePlanner.DatabaseName;
}
set
{
if(this.RestorePlanner != null)
{
string currentDatabaseName = this.RestorePlanner.DatabaseName;
this.RestorePlanner.DatabaseName = value;
}
}
}
/// <summary>
/// Gets or sets a value indicating whether [close existing connections].
/// </summary>
/// <value>
/// <c>true</c> if [close existing connections]; otherwise, <c>false</c>.
/// </value>
public bool CloseExistingConnections { get; set; }
/*
private BackupTimeLine.TimeLineDuration timeLineDuration = BackupTimeLine.TimeLineDuration.Day;
public BackupTimeLine.TimeLineDuration TimeLineDuration
{
get { return this.timeLineDuration; }
set { this.timeLineDuration = value; }
}
*/
/// <summary>
/// Sql server credential name used to restore from Microsoft Azure url
/// </summary>
internal string CredentialName = string.Empty;
/// <summary>
/// Azure container SAS policy
/// </summary>
internal string ContainerSharedAccessPolicy = string.Empty;
/// <summary>
/// Returns the physical name for the target Db file.
/// It is the sourceDbName replaced with targetDbName in sourceFilename.
/// If either sourceDbName or TargetDbName is empty, the source Db filename is returned.
/// </summary>
/// <param name="sourceDbFilePhysicalLocation">source DbFile physical location</param>
/// <returns></returns>
private string GetTargetDbFilePhysicalName(string sourceDbFilePhysicalLocation)
{
string filePath = ConvertToLocalMachinePath(sourceDbFilePhysicalLocation);
string fileName = Path.GetFileName(filePath);
if (!string.IsNullOrEmpty(this.SourceDatabaseName) && !string.IsNullOrEmpty(this.targetDbName))
{
string sourceFilename = fileName;
fileName = sourceFilename.Replace(this.SourceDatabaseName, this.targetDbName);
}
return fileName;
}
public IEnumerable<BackupSetInfo> GetBackupSetInfo()
{
List<BackupSetInfo> result = new List<BackupSetInfo>();
foreach (Restore restore in RestorePlan.RestoreOperations)
{
BackupSetInfo backupSetInfo = BackupSetInfo.Create(restore, Server);
result.Add(backupSetInfo);
}
return result;
}
/// <summary>
/// List of selected backupsets
/// </summary>
public DatabaseFileInfo[] GetSelectedBakupSets()
{
List<DatabaseFileInfo> result = new List<DatabaseFileInfo>();
IEnumerable<BackupSetInfo> backupSetInfos = GetBackupSetInfo();
foreach (var backupSetInfo in backupSetInfos)
{
var item = new DatabaseFileInfo(backupSetInfo.ConvertPropertiesToArray());
Guid backupSetGuid;
if(!Guid.TryParse(item.Id, out backupSetGuid))
{
backupSetGuid = Guid.Empty;
}
item.IsSelected = this.backupSetsFilterInfo.IsBackupSetSelected(backupSetGuid);
result.Add(item);
}
return result.ToArray();
}
/// <summary>
/// Gets the files of the database
/// </summary>
public List<DbFile> GetDbFiles()
{
Database db = null;
List<DbFile> ret = new List<DbFile>();
try
{
if (!this.RestorePlanner.ReadHeaderFromMedia)
{
db = this.Server.Databases[this.RestorePlanner.DatabaseName];
}
if (restorePlan != null && restorePlan.RestoreOperations.Count > 0)
{
if (db != null && db.Status == DatabaseStatus.Normal)
{
ret = this.Util.GetDbFiles(db);
}
else
{
ret = this.Util.GetDbFiles(restorePlan.RestoreOperations[0]);
}
}
}
catch(Exception ex )
{
Logger.Write(TraceEventType.Information, $"Failed to get restore db files. error: {ex.Message}");
}
return ret;
}
public string DefaultDataFileFolder
{
get
{
return Util.GetDefaultDataFileFolder();
}
}
public string DefaultLogFileFolder
{
get
{
return Util.GetDefaultLogFileFolder();
}
}
public string DefaultBackupFolder
{
get
{
return CommonUtilities.GetDefaultBackupFolder(this.server.ConnectionContext);
}
}
internal RestorePlan CreateRestorePlan(DatabaseRestorePlanner planner, RestoreOptions restoreOptions)
{
this.CreateOrUpdateRestorePlanException = null;
RestorePlan ret = null;
try
{
ret = planner.CreateRestorePlan(restoreOptions);
if (ret == null || ret.RestoreOperations.Count == 0)
{
this.ActiveException = planner.GetBackupDeviceReadErrors();
}
}
catch (Exception ex)
{
this.ActiveException = ex;
this.CreateOrUpdateRestorePlanException = this.ActiveException;
}
finally
{
}
return ret;
}
private void ResetOptions()
{
isTailLogBackupPossible = null;
isTailLogBackupWithNoRecoveryPossible = null;
bool isTailLogBackupPossibleValue = IsTailLogBackupPossible;
bool isTailLogBackupWithNoRecoveryPossibleValue = isTailLogBackupPossibleValue;
}
public void CreateNewRestorePlan()
{
this.ActiveException = null; //Clear any existing exceptions as the plan is getting recreated.
//Clear any existing exceptions as new plan is getting recreated.
this.CreateOrUpdateRestorePlanException = null;
this.DbFiles.Clear();
this.PlanUpdateRequired = false;
this.restorePlan = null;
if (String.IsNullOrEmpty(this.RestorePlanner.DatabaseName))
{
this.RestorePlan = new RestorePlan(this.Server);
this.Util.AddCredentialNameForUrlBackupSet(this.RestorePlan, this.CredentialName);
}
else
{
this.RestorePlan = this.CreateRestorePlan(this.RestorePlanner, this.RestoreOptions);
this.Util.AddCredentialNameForUrlBackupSet(this.restorePlan, this.CredentialName);
if (this.ActiveException == null)
{
this.dbFiles = this.GetDbFiles();
UpdateDBFilesPhysicalRelocate();
}
}
if (this.restorePlan == null)
{
this.RestorePlan = new RestorePlan(this.Server);
this.Util.AddCredentialNameForUrlBackupSet(this.RestorePlan, this.CredentialName);
}
else
{
ResetOptions();
}
}
public bool ShouldCreateNewPlan()
{
return RestorePlan == null ||
string.Compare(RestorePlanner.DatabaseName, this.RestoreParams.GetOptionValue<string>(RestoreOptionsHelper.SourceDatabaseName), StringComparison.InvariantCultureIgnoreCase) != 0 ||
RestorePlanner.ReadHeaderFromMedia != this.RestoreParams.ReadHeaderFromMedia ||
this.RelocateAllFiles != this.RestoreParams.GetOptionValue<bool>(RestoreOptionsHelper.RelocateDbFiles) ||
string.Compare(TargetDatabaseName, this.RestoreParams.GetOptionValue<string>(RestoreOptionsHelper.TargetDatabaseName), StringComparison.InvariantCultureIgnoreCase) != 0 ||
string.Compare(this.backupMediaList, RestoreParams.BackupFilePaths, StringComparison.InvariantCultureIgnoreCase) != 0;
}
/// <summary>
/// Updates restore plan
/// </summary>
public void UpdateRestorePlan()
{
RestoreOptionsHelper.UpdateOptionsInPlan(this);
if (RelocateAllFiles)
{
UpdateDbFiles();
}
this.SetRestorePlanProperties(this.restorePlan);
UpdateSelectedBackupSets();
}
/// <summary>
/// Determine if restore plan of selected database does have Url
/// </summary>
private bool IfRestorePlanHasUrl()
{
return (restorePlan.RestoreOperations.Any(
res => res.BackupSet.BackupMediaSet.BackupMediaList.Any(t => t.MediaType == DeviceType.Url)));
}
/// <summary>
/// Sets restore plan properties
/// </summary>
private void SetRestorePlanProperties(RestorePlan rp)
{
if (rp == null || rp.RestoreOperations.Count < 1)
{
return;
}
rp.SetRestoreOptions(this.RestoreOptions);
rp.CloseExistingConnections = this.CloseExistingConnections;
if (this.TargetDatabaseName != null && !this.TargetDatabaseName.Equals(string.Empty))
{
rp.DatabaseName = TargetDatabaseName;
}
rp.RestoreOperations[0].RelocateFiles.Clear();
foreach (DbFile dbFile in this.DbFiles)
{
// For XStore path, we don't want to try the getFullPath.
string newPhysicalPath;
Uri pathUri;
bool uriCreated = Uri.TryCreate(dbFile.PhysicalNameRelocate, UriKind.Absolute, out pathUri);
if (uriCreated && pathUri.Scheme == "https")
{
newPhysicalPath = dbFile.PhysicalNameRelocate;
}
else
{
newPhysicalPath = Path.GetFullPath(dbFile.PhysicalNameRelocate);
}
if (!dbFile.PhysicalName.Equals(newPhysicalPath))
{
RelocateFile relocFile = new RelocateFile(dbFile.LogicalName, dbFile.PhysicalNameRelocate);
rp.RestoreOperations[0].RelocateFiles.Add(relocFile);
}
}
}
/// <summary>
/// Bool indicating whether a tail log backup will be taken
/// </summary>
public bool BackupTailLog
{
get
{
return this.RestorePlanner.BackupTailLog;
}
set
{
if (this.RestorePlanner.BackupTailLog != value)
{
this.RestorePlanner.BackupTailLog = value;
this.PlanUpdateRequired = true;
}
}
}
/// <summary>
/// bool indicating whether the database will be left in restoring state
/// </summary>
public bool TailLogWithNoRecovery
{
get
{
return this.RestorePlanner.TailLogWithNoRecovery;
}
set
{
if (this.RestorePlanner.TailLogWithNoRecovery != value)
{
this.RestorePlanner.TailLogWithNoRecovery = value;
this.PlanUpdateRequired = true;
}
}
}
public DateTime? CurrentRestorePointInTime
{
get
{
if (!IsAnyFullBackupSetSelected())
{
return null;
}
for (int i = this.RestorePlan.RestoreOperations.Count - 1; i >= 0; i--)
{
BackupSet backupSet = this.RestorePlan.RestoreOperations[i].BackupSet;
if (this.backupSetsFilterInfo.IsBackupSetSelected(backupSet))
{
if (backupSet == null
|| (backupSet.BackupSetType == BackupSetType.Log &&
this.RestorePlan.RestoreOperations[i].ToPointInTime != null))
{
return this.RestorePlanner.RestoreToPointInTime;
}
return backupSet.BackupStartDate;
}
}
return null;
}
}
/// <summary>
/// Returns the restore plan error message
/// </summary>
public override string ErrorMessage
{
get
{
if (ActiveException != null)
{
return ActiveException.Message;
}
return string.Empty;
}
}
private bool IsAnyFullBackupSetSelected()
{
bool isSelected = false;
if (this.RestorePlan != null && this.RestorePlan.RestoreOperations.Any() && this.backupSetsFilterInfo.AnySelected)
{
var fullBackupSet = this.RestorePlan.RestoreOperations.FirstOrDefault(x => x.BackupSet.BackupSetType == BackupSetType.Database);
isSelected = fullBackupSet != null && this.backupSetsFilterInfo.IsBackupSetSelected(fullBackupSet.BackupSet.BackupSetGuid);
}
return isSelected;
}
private int GetLastSelectedBackupSetIndex()
{
if (this.RestorePlan != null && this.RestorePlan.RestoreOperations.Any() && this.backupSetsFilterInfo.AnySelected)
{
for (int i = this.RestorePlan.RestoreOperations.Count -1; i >= 0; i--)
{
BackupSet backupSet = this.RestorePlan.RestoreOperations[i].BackupSet;
if (this.backupSetsFilterInfo.IsBackupSetSelected(backupSet))
{
return i;
}
}
}
return -1;
}
private int GetFirstSelectedBackupSetIndex()
{
if (this.RestorePlan != null && this.RestorePlan.RestoreOperations.Any() && this.backupSetsFilterInfo.AnySelected)
{
for (int i = 0; i < this.RestorePlan.RestoreOperations.Count - 1; i++)
{
BackupSet backupSet = this.RestorePlan.RestoreOperations[i].BackupSet;
if (this.backupSetsFilterInfo.IsBackupSetSelected(backupSet))
{
return i;
}
}
}
return -1;
}
public void UpdateSelectedBackupSets()
{
this.backupSetsFilterInfo.Clear();
var selectedBackupSetsFromClient = this.RestoreParams.SelectedBackupSets;
if (this.RestorePlan != null && this.RestorePlan.RestoreOperations != null)
{
for (int index = 0; index < this.RestorePlan.RestoreOperations.Count; index++)
{
BackupSet backupSet = this.RestorePlan.RestoreOperations[index].BackupSet;
if (backupSet != null)
{
// If the collection client sent is null, select everything; otherwise select the items that are selected in client
bool backupSetSelected = selectedBackupSetsFromClient == null || selectedBackupSetsFromClient.Any(x => BackUpSetGuidEqualsId(backupSet, x));
if (backupSetSelected)
{
AddBackupSetsToSelected(index, index);
//the last index - this will include tail-Log restore operation if present
//If the last item is selected, select all the items before that because the last item
//is tail-log
if (index == this.RestorePlan.RestoreOperations.Count - 1)
{
AddBackupSetsToSelected(0, index);
}
//If the second item is selected and it's a diff backup, the fist item (full backup) has to be selected
if (index == 1 && backupSet.BackupSetType == BackupSetType.Differential)
{
AddBackupSetsToSelected(0, 0);
}
//If the selected item is a log backup, select all the items before that
if (backupSet.BackupSetType == BackupSetType.Log)
{
AddBackupSetsToSelected(0, index);
}
}
else
{
// If the first item is not selected which is always the full backup, other backupsets cannot be selected
if (index == 0)
{
selectedBackupSetsFromClient = new string[] { };
}
//The a log is not selected, the logs after that cannot be selected
if (backupSet.BackupSetType == BackupSetType.Log)
{
break;
}
//If the second item is not selected and it's a diff backup
if (index == 1 && backupSet.BackupSetType == BackupSetType.Differential)
{
if (this.Server.Version.Major < 9 ||
(this.RestorePlan.RestoreOperations.Count >= 3 &&
BackupSet.IsBackupSetsInSequence(this.RestorePlan.RestoreOperations[0].BackupSet, this.RestorePlan.RestoreOperations[2].BackupSet)))
{
// only the item at index 1 won't be selected
}
else
{
// nothing after index 1 should be selected
break;
}
}
}
}
}
}
}
private bool BackUpSetGuidEqualsId(BackupSet backupSet, string id)
{
return backupSet != null && string.Compare(backupSet.BackupSetGuid.ToString(), id, StringComparison.OrdinalIgnoreCase) == 0;
}
private void AddBackupSetsToSelected(int from, int to)
{
if (this.RestorePlan != null && this.RestorePlan.RestoreOperations != null
&& from < this.RestorePlan.RestoreOperations.Count && to < this.RestorePlan.RestoreOperations.Count)
{
for (int i = from; i <= to; i++)
{
BackupSet backupSet = this.RestorePlan.RestoreOperations[i].BackupSet;
this.backupSetsFilterInfo.Add(backupSet);
}
}
}
/// <summary>
/// Verifies the backup files location.
/// </summary>
internal void CheckBackupFilesLocation()
{
if (this.RestorePlan != null)
{
foreach (Restore restore in this.RestorePlan.RestoreOperations)
{
if (restore.BackupSet != null)
{
restore.BackupSet.CheckBackupFilesExistence();
}
}
}
}
internal bool DbFilesLocationAreValid()
{
foreach (DbFile dbFile in this.DbFiles)
{
string newPhysicalPath = Path.GetFullPath(dbFile.PhysicalNameRelocate);
if (string.Compare(dbFile.PhysicalName, dbFile.PhysicalNameRelocate, true) != 0)
{
bool isValidFolder = false;
bool isValidPath = Util.IsDestinationPathValid(Path.GetDirectoryName(newPhysicalPath), ref isValidFolder);
if (!(isValidFolder && isValidPath))
{
return false;
}
}
}
return true;
}
/// <summary>
/// Returns true if can close eixisting connections for give database
/// </summary>
public bool CanDropExistingConnections
{
get
{
if (RestorePlan != null && RestorePlanner != null)
{
return RestorePlan.CanDropExistingConnections(RestorePlanner.DatabaseName);
}
return false;
}
}
/// <summary>
/// Cancels the restore operations
/// </summary>
public override void Cancel()
{
foreach (Restore restore in RestorePlan.RestoreOperations)
{
restore.Abort();
}
}
/// <summary>
/// Create a restore data object that includes the plan to do the restore operation
/// </summary>
/// <param name="requestParam"></param>
/// <returns></returns>
internal void UpdateRestoreTaskObject()
{
bool shouldCreateNewPlan = ShouldCreateNewPlan();
if (!string.IsNullOrEmpty(RestoreParams.BackupFilePaths) && RestoreParams.ReadHeaderFromMedia)
{
AddDevices(RestoreParams.BackupFilePaths, (DeviceType)RestoreParams.DeviceType);
}
else
{
RestorePlanner.BackupMediaList.Clear();
}
RestorePlanner.ReadHeaderFromMedia = RestoreParams.ReadHeaderFromMedia;
RestoreOptionFactory.Instance.SetAndValidate(RestoreOptionsHelper.SourceDatabaseName, this);
RestoreOptionFactory.Instance.SetAndValidate(RestoreOptionsHelper.TargetDatabaseName, this);
if (shouldCreateNewPlan)
{
CreateNewRestorePlan();
}
UpdateRestorePlan();
}
}
}