Rewrote HyperV and MSSQL parser to support paths under each instance.

This also cleans up the filters slightly, so the logic for "first match" still holds.

If an item is excluded, and subfolders are also automatically excluded.
This commit is contained in:
Kenneth Skovhede
2025-09-04 20:14:52 +02:00
committed by Svend Roperos
parent 18485e9f05
commit 0d57f2ceea
4 changed files with 217 additions and 98 deletions
@@ -35,8 +35,8 @@ namespace Duplicati.Library.Modules.Builtin
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging.Log.LogTagFromType<HyperVOptions>();
private const string m_HyperVPathGuidRegExp = @"\%HYPERV\%\\([0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})";
private const string m_HyperVPathAllRegExp = @"%HYPERV%";
private const string m_HyperVPathGuidRegExp = @"\%HYPERV\%\\(?<id>[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})(\\(?<path>.*))?";
private const string m_HyperVPathAllMarker = @"%HYPERV%";
private const string IGNORE_CONSISTENCY_WARNING_OPTION = "hyperv-ignore-client-warning";
@@ -84,7 +84,7 @@ namespace Duplicati.Library.Modules.Builtin
Logging.Log.WriteWarningMessage(LOGTAG, "HyperVWindowsOnly", null, "Hyper-V backup works only on Windows OS");
if (paths != null)
paths = paths.Where(x => !x.Equals(m_HyperVPathAllRegExp, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray();
paths = paths.Where(x => !x.Equals(m_HyperVPathAllMarker, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray();
if (!string.IsNullOrEmpty(filter))
{
@@ -104,26 +104,21 @@ namespace Duplicati.Library.Modules.Builtin
// referenced types from System.Management here
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
[SupportedOSPlatform("windows")]
private Dictionary<string, string> RealParseSourcePaths(ref string[] paths, ref string filter, Dictionary<string, string> commandlineOptions)
public Dictionary<string, string> RealParseSourcePaths(ref string[] paths, ref string filter, Dictionary<string, string> commandlineOptions, IHyperVUtility hypervUtility = null)
{
var changedOptions = new Dictionary<string, string>();
var filtersInclude = new List<string>();
var filtersExclude = new List<string>();
var hypervFilters = new List<string>();
if (!string.IsNullOrEmpty(filter))
{
var filters = filter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries);
filtersInclude = filters.Where(x => x.StartsWith("+", StringComparison.Ordinal) && Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
.Select(x => Regex.Match(x.Substring(1), m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Groups[1].Value).ToList();
filtersExclude = filters.Where(x => x.StartsWith("-", StringComparison.Ordinal) && Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
.Select(x => Regex.Match(x.Substring(1), m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Groups[1].Value).ToList();
hypervFilters = filters.Where(x => Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToList();
var remainingfilters = filters.Where(x => !Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray();
filter = string.Join(System.IO.Path.PathSeparator.ToString(), remainingfilters);
}
var hypervUtility = new HyperVUtility();
hypervUtility ??= new HyperVUtility();
if (paths == null || !ContainFilesForBackup(paths) || !hypervUtility.IsHyperVInstalled)
return changedOptions;
@@ -132,10 +127,10 @@ namespace Duplicati.Library.Modules.Builtin
{
var excludedWriters = commandlineOptions["vss-exclude-writers"].Split(';').Where(x => !string.IsNullOrWhiteSpace(x) && x.Trim().Length > 0).Select(x => new Guid(x)).ToArray();
if (excludedWriters.Contains(HyperVUtility.HyperVWriterGuid))
if (excludedWriters.Contains(hypervUtility.HyperVWriterGuid))
{
Logging.Log.WriteWarningMessage(LOGTAG, "CannotExcludeHyperVVSSWriter", null, "Excluded writers for VSS cannot contain Hyper-V writer when backuping Hyper-V virtual machines. Removing \"{0}\" to continue", HyperVUtility.HyperVWriterGuid.ToString());
changedOptions["vss-exclude-writers"] = string.Join(";", excludedWriters.Where(x => x != HyperVUtility.HyperVWriterGuid));
Logging.Log.WriteWarningMessage(LOGTAG, "CannotExcludeHyperVVSSWriter", null, "Excluded writers for VSS cannot contain Hyper-V writer when backuping Hyper-V virtual machines. Removing \"{0}\" to continue", hypervUtility.HyperVWriterGuid.ToString());
changedOptions["vss-exclude-writers"] = string.Join(";", excludedWriters.Where(x => x != hypervUtility.HyperVWriterGuid));
}
}
@@ -151,56 +146,89 @@ namespace Duplicati.Library.Modules.Builtin
Logging.Log.WriteInformationMessage(LOGTAG, "StartingHyperVQuery", "Starting to gather Hyper-V information");
var provider = Utility.Utility.ParseEnumOption(changedOptions.AsReadOnly(), "snapshot-provider", WindowsSnapshot.DEFAULT_WINDOWS_SNAPSHOT_QUERY_PROVIDER);
hypervUtility.QueryHyperVGuestsInfo(provider, true);
Logging.Log.WriteInformationMessage(LOGTAG, "HyperVMachineCount", "Found {0} virtual machines on Hyper-V", hypervUtility.Guests.Count);
if (hypervUtility.Guests == null || hypervUtility.Guests.Count == 0)
{
Logging.Log.WriteWarningMessage(LOGTAG, "NoHyperVMachinesFound", null, "No Hyper-V virtual machines found.");
return changedOptions;
}
Logging.Log.WriteInformationMessage(LOGTAG, "HyperVMachineCount", "Found {0} virtual machines on Hyper-V", hypervUtility.Guests?.Count);
foreach (var guest in hypervUtility.Guests)
Logging.Log.WriteProfilingMessage(LOGTAG, "FoundHyperVMachine", "Found VM name {0}, ID {1}, files {2}", guest.Name, guest.ID, string.Join(";", guest.DataPaths));
List<HyperVGuest> guestsForBackup = new List<HyperVGuest>();
var guestsForBackup = new List<HyperVGuest>();
var conditionalPathsForBackup = new List<(string ID, string Path)>();
if (paths.Contains(m_HyperVPathAllRegExp, StringComparer.OrdinalIgnoreCase))
if (paths.Contains(m_HyperVPathAllMarker, StringComparer.OrdinalIgnoreCase))
guestsForBackup = hypervUtility.Guests;
else
foreach (var guestID in paths.Where(x => Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
.Select(x => Regex.Match(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Groups[1].Value).ToArray())
{
var guestEntries = paths.Where(x => Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
.Select(x =>
{
var m = Regex.Match(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
return (ID: m.Groups["id"].Value, Path: m.Groups["path"].Value);
});
foreach ((var guestID, var path) in guestEntries)
{
var foundGuest = hypervUtility.Guests.Where(x => x.ID == new Guid(guestID));
if (foundGuest.Count() != 1)
throw new Duplicati.Library.Interface.UserInformationException(string.Format("Hyper-V guest specified in source with ID {0} cannot be found", guestID), "HyperVGuestNotFound");
guestsForBackup.Add(foundGuest.First());
if (string.IsNullOrWhiteSpace(path))
guestsForBackup.Add(foundGuest.First());
else
conditionalPathsForBackup.Add((guestID, path));
}
}
if (filtersInclude.Count > 0)
foreach (var guestID in filtersInclude)
var guestStatus = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
var pathFilters = new List<(string ID, string Filter)>();
foreach (var filterExp in hypervFilters)
{
var m = Regex.Match(filterExp, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
(var id, var path) = (m.Groups["id"].Value, m.Groups["path"].Value);
if (string.IsNullOrWhiteSpace(path))
{
var foundGuest = hypervUtility.Guests.Where(x => x.ID == new Guid(guestID));
if (foundGuest.Count() != 1)
throw new Duplicati.Library.Interface.UserInformationException(string.Format("Hyper-V guest specified in include filter with ID {0} cannot be found", guestID), "HyperVGuestNotFound");
guestsForBackup.Add(foundGuest.First());
Logging.Log.WriteInformationMessage(LOGTAG, "IncludeByFilter", "Including {0} based on including filters", guestID);
if (!guestStatus.ContainsKey(id))
guestStatus[id] = filterExp.StartsWith("-", StringComparison.Ordinal) ? false : true;
}
guestsForBackup = guestsForBackup.Distinct().ToList();
if (filtersExclude.Count > 0)
foreach (var guestID in filtersExclude)
else
{
var foundGuest = guestsForBackup.Where(x => x.ID == new Guid(guestID));
if (foundGuest.Count() != 1)
throw new Duplicati.Library.Interface.UserInformationException(string.Format("Hyper-V guest specified in exclude filter with ID {0} cannot be found", guestID), "HyperVGuestNotFound");
guestsForBackup.Remove(foundGuest.First());
Logging.Log.WriteInformationMessage(LOGTAG, "ExcludeByFilter", "Excluding {0} based on excluding filters", guestID);
pathFilters.Add((id, filterExp[0] + path));
}
}
var pathsForBackup = new List<string>(paths);
var filterhandler = new Utility.FilterExpression(
filter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries).Where(x => x.StartsWith("-", StringComparison.Ordinal)).Select(x => x.Substring(1)).ToList());
guestsForBackup = guestsForBackup
.Where(x => !guestStatus.ContainsKey(x.ID.ToString()) || guestStatus[x.ID.ToString()])
.DistinctBy(x => x.ID)
.ToList();
var includedGuestIds = guestsForBackup.Select(x => x.ID.ToString()).ToHashSet(StringComparer.OrdinalIgnoreCase);
pathFilters = pathFilters
.Where(x => includedGuestIds.Contains(x.ID))
.DistinctBy(x => x.ID)
.ToList();
conditionalPathsForBackup = conditionalPathsForBackup
.Where(x => includedGuestIds.Contains(x.ID))
.DistinctBy(x => x.ID)
.ToList();
var filterhandler = pathFilters.Select(x => x.Filter)
.Concat(filter.Split([System.IO.Path.PathSeparator], StringSplitOptions.RemoveEmptyEntries))
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => Utility.FilterExpression.StringToIFilter(x))
.Append(new Utility.FilterExpression())
.Aggregate((a, b) => Utility.FilterExpression.Combine(a, b));
var pathsForBackup = new List<string>(paths.Where(x => !x.Equals(m_HyperVPathAllMarker, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)));
pathsForBackup.AddRange(conditionalPathsForBackup.Select(x => x.Path).Where(x => !string.IsNullOrWhiteSpace(x)));
foreach (var guestForBackup in guestsForBackup)
foreach (var pathForBackup in guestForBackup.DataPaths)
@@ -214,7 +242,7 @@ namespace Duplicati.Library.Modules.Builtin
Logging.Log.WriteInformationMessage(LOGTAG, "ExcludeByFilter", "Excluding {0} based on excluding filters", pathForBackup);
}
paths = pathsForBackup.Where(x => !x.Equals(m_HyperVPathAllRegExp, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
paths = pathsForBackup
.Distinct(Utility.Utility.ClientFilenameStringComparer).OrderBy(a => a).ToArray();
return changedOptions;
@@ -225,7 +253,7 @@ namespace Duplicati.Library.Modules.Builtin
if (paths == null || !OperatingSystem.IsWindows())
return false;
return paths.Where(x => !string.IsNullOrWhiteSpace(x)).Any(x => x.Equals(m_HyperVPathAllRegExp, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
return paths.Where(x => !string.IsNullOrWhiteSpace(x)).Any(x => x.Equals(m_HyperVPathAllMarker, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(x, m_HyperVPathGuidRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
}
#endregion
@@ -36,8 +36,8 @@ namespace Duplicati.Library.Modules.Builtin
/// </summary>
private static readonly string LOGTAG = Logging.Log.LogTagFromType<MSSQLOptions>();
private const string m_MSSQLPathDBRegExp = @"\%MSSQL\%\\(.+)";
private const string m_MSSQLPathAllRegExp = @"%MSSQL%";
private const string m_MSSQLPathDBRegExp = @"\%MSSQL\%\\(?<id>^\\+)(?<path>\\.*)";
private const string m_MSSQLPathAllMarker = @"%MSSQL%";
#region IGenericModule Members
@@ -82,7 +82,7 @@ namespace Duplicati.Library.Modules.Builtin
Logging.Log.WriteWarningMessage(LOGTAG, "MSSqlWindowsOnly", null, "Microsoft SQL Server databases backup works only on Windows OS");
if (paths != null)
paths = paths.Where(x => !x.Equals(m_MSSQLPathAllRegExp, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray();
paths = paths.Where(x => !x.Equals(m_MSSQLPathAllMarker, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray();
if (!string.IsNullOrEmpty(filter))
{
@@ -102,26 +102,21 @@ namespace Duplicati.Library.Modules.Builtin
// referenced types from System.Management here
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
[SupportedOSPlatform("windows")]
private Dictionary<string, string> RealParseSourcePaths(ref string[] paths, ref string filter, Dictionary<string, string> commandlineOptions)
public Dictionary<string, string> RealParseSourcePaths(ref string[] paths, ref string filter, Dictionary<string, string> commandlineOptions, IMSSQLUtility mssqlUtility = null)
{
var changedOptions = new Dictionary<string, string>();
var filtersInclude = new List<string>();
var filtersExclude = new List<string>();
var mssqlFilters = new List<string>();
if (!string.IsNullOrEmpty(filter))
{
var filters = filter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries);
filtersInclude = filters.Where(x => x.StartsWith("+", StringComparison.Ordinal) && Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
.Select(x => Regex.Match(x.Substring(1), m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Groups[1].Value).ToList();
filtersExclude = filters.Where(x => x.StartsWith("-", StringComparison.Ordinal) && Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
.Select(x => Regex.Match(x.Substring(1), m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Groups[1].Value).ToList();
mssqlFilters = filters.Where(x => Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToList();
var remainingfilters = filters.Where(x => !Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray();
filter = string.Join(System.IO.Path.PathSeparator.ToString(), remainingfilters);
}
var mssqlUtility = new MSSQLUtility();
mssqlUtility ??= new MSSQLUtility();
if (paths == null || !ContainFilesForBackup(paths) || !mssqlUtility.IsMSSQLInstalled)
return changedOptions;
@@ -130,10 +125,10 @@ namespace Duplicati.Library.Modules.Builtin
{
var excludedWriters = commandlineOptions["vss-exclude-writers"].Split(';').Where(x => !string.IsNullOrWhiteSpace(x) && x.Trim().Length > 0).Select(x => new Guid(x)).ToArray();
if (excludedWriters.Contains(MSSQLUtility.MSSQLWriterGuid))
if (excludedWriters.Contains(mssqlUtility.MSSQLWriterGuid))
{
Logging.Log.WriteWarningMessage(LOGTAG, "CannotExcludeMsSqlVSSWriter", null, "Excluded writers for VSS cannot contain MS SQL writer when backuping Microsoft SQL Server databases. Removing \"{0}\" to continue", MSSQLUtility.MSSQLWriterGuid.ToString());
changedOptions["vss-exclude-writers"] = string.Join(";", excludedWriters.Where(x => x != MSSQLUtility.MSSQLWriterGuid));
Logging.Log.WriteWarningMessage(LOGTAG, "CannotExcludeMsSqlVSSWriter", null, "Excluded writers for VSS cannot contain MS SQL writer when backuping Microsoft SQL Server databases. Removing \"{0}\" to continue", mssqlUtility.MSSQLWriterGuid.ToString());
changedOptions["vss-exclude-writers"] = string.Join(";", excludedWriters.Where(x => x != mssqlUtility.MSSQLWriterGuid));
}
}
@@ -146,56 +141,90 @@ namespace Duplicati.Library.Modules.Builtin
Logging.Log.WriteInformationMessage(LOGTAG, "StartingMsSqlQuery", "Starting to gather Microsoft SQL Server information", Logging.LogMessageType.Information);
var provider = Utility.Utility.ParseEnumOption(changedOptions.AsReadOnly(), "snapshot-provider", WindowsSnapshot.DEFAULT_WINDOWS_SNAPSHOT_QUERY_PROVIDER);
mssqlUtility.QueryDBsInfo(provider);
if (mssqlUtility.DBs == null || mssqlUtility.DBs.Count == 0)
{
Logging.Log.WriteWarningMessage(LOGTAG, "NoMsSqlDatabasesFound", null, "No Microsoft SQL Server databases found.");
return changedOptions;
}
Logging.Log.WriteInformationMessage(LOGTAG, "MsSqlDatabaseCount", "Found {0} databases on Microsoft SQL Server", mssqlUtility.DBs.Count);
foreach (var db in mssqlUtility.DBs)
Logging.Log.WriteProfilingMessage(LOGTAG, "MsSqlDatabaseName", "Found DB name {0}, ID {1}, files {2}", db.Name, db.ID, string.Join(";", db.DataPaths));
List<MSSQLDB> dbsForBackup = new List<MSSQLDB>();
var conditionalPathsForBackup = new List<(string ID, string Path)>();
if (paths.Contains(m_MSSQLPathAllRegExp, StringComparer.OrdinalIgnoreCase))
if (paths.Contains(m_MSSQLPathAllMarker, StringComparer.OrdinalIgnoreCase))
dbsForBackup = mssqlUtility.DBs;
else
foreach (var dbID in paths.Where(x => Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
.Select(x => Regex.Match(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Groups[1].Value).ToArray())
{
var guestEntries = paths.Where(x => Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
.Select(x =>
{
var m = Regex.Match(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
return (ID: m.Groups["id"].Value, Path: m.Groups["path"].Value);
});
foreach ((var dbID, var path) in guestEntries)
{
var foundDB = mssqlUtility.DBs.Where(x => x.ID.Equals(dbID, StringComparison.OrdinalIgnoreCase));
if (foundDB.Count() != 1)
throw new Duplicati.Library.Interface.UserInformationException(string.Format("DB name specified in source with ID {0} cannot be found", dbID), "MsSqlDatabaseNotFound");
dbsForBackup.Add(foundDB.First());
if (string.IsNullOrWhiteSpace(path))
dbsForBackup.Add(foundDB.First());
else
conditionalPathsForBackup.Add((dbID, path));
}
}
if (filtersInclude.Count > 0)
foreach (var dbID in filtersInclude)
var dbStatus = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
var pathFilters = new List<(string ID, string Filter)>();
foreach (var filterExp in mssqlFilters)
{
var m = Regex.Match(filterExp, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
(var id, var path) = (m.Groups["id"].Value, m.Groups["path"].Value);
if (string.IsNullOrWhiteSpace(path))
{
var foundDB = mssqlUtility.DBs.Where(x => x.ID.Equals(dbID, StringComparison.OrdinalIgnoreCase));
if (foundDB.Count() != 1)
throw new Duplicati.Library.Interface.UserInformationException(string.Format("DB name specified in include filter with ID {0} cannot be found", dbID), "MsSqlDatabaseNotFound");
dbsForBackup.Add(foundDB.First());
Logging.Log.WriteInformationMessage(LOGTAG, "IncludeByFilter", "Including {0} based on including filters", dbID);
if (!dbStatus.ContainsKey(id))
dbStatus[id] = filterExp.StartsWith("-", StringComparison.Ordinal) ? false : true;
}
dbsForBackup = dbsForBackup.Distinct().ToList();
if (filtersExclude.Count > 0)
foreach (var dbID in filtersExclude)
else
{
var foundDB = dbsForBackup.Where(x => x.ID.Equals(dbID, StringComparison.OrdinalIgnoreCase));
if (foundDB.Count() != 1)
throw new Duplicati.Library.Interface.UserInformationException(string.Format("DB name specified in exclude filter with ID {0} cannot be found", dbID), "MsSqlDatabaseNotFound");
dbsForBackup.Remove(foundDB.First());
Logging.Log.WriteInformationMessage(LOGTAG, "ExcludeByFilter", "Excluding {0} based on excluding filters", dbID);
pathFilters.Add((id, filterExp[0] + path));
}
}
var pathsForBackup = new List<string>(paths);
var filterhandler = new Utility.FilterExpression(
filter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries).Where(x => x.StartsWith("-", StringComparison.Ordinal)).Select(x => x.Substring(1)).ToList());
dbsForBackup = dbsForBackup
.Where(x => !dbStatus.ContainsKey(x.ID.ToString()) || dbStatus[x.ID.ToString()])
.DistinctBy(x => x.ID)
.ToList();
var includedDbIds = dbsForBackup.Select(x => x.ID.ToString()).ToHashSet(StringComparer.OrdinalIgnoreCase);
pathFilters = pathFilters
.Where(x => includedDbIds.Contains(x.ID))
.DistinctBy(x => x.ID)
.ToList();
conditionalPathsForBackup = conditionalPathsForBackup
.Where(x => includedDbIds.Contains(x.ID))
.DistinctBy(x => x.ID)
.ToList();
var filterhandler = pathFilters.Select(x => x.Filter)
.Concat(filter.Split([System.IO.Path.PathSeparator], StringSplitOptions.RemoveEmptyEntries))
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => Utility.FilterExpression.StringToIFilter(x))
.Append(new Utility.FilterExpression())
.Aggregate((a, b) => Utility.FilterExpression.Combine(a, b));
var pathsForBackup = new List<string>(paths.Where(x => !x.Equals(m_MSSQLPathAllMarker, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)));
pathsForBackup.AddRange(conditionalPathsForBackup.Select(x => x.Path).Where(x => !string.IsNullOrWhiteSpace(x)));
foreach (var dbForBackup in dbsForBackup)
foreach (var pathForBackup in dbForBackup.DataPaths)
@@ -209,7 +238,7 @@ namespace Duplicati.Library.Modules.Builtin
Logging.Log.WriteInformationMessage(LOGTAG, "ExcludeByFilter", "Excluding {0} based on excluding filters", pathForBackup);
}
paths = pathsForBackup.Where(x => !x.Equals(m_MSSQLPathAllRegExp, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
paths = pathsForBackup
.Distinct(Utility.Utility.ClientFilenameStringComparer).OrderBy(a => a).ToArray();
return changedOptions;
@@ -220,7 +249,7 @@ namespace Duplicati.Library.Modules.Builtin
if (paths == null || !OperatingSystem.IsWindows())
return false;
return paths.Where(x => !string.IsNullOrWhiteSpace(x)).Any(x => x.Equals(m_MSSQLPathAllRegExp, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
return paths.Where(x => !string.IsNullOrWhiteSpace(x)).Any(x => x.Equals(m_MSSQLPathAllMarker, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(x, m_MSSQLPathDBRegExp, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant));
}
#endregion
@@ -82,8 +82,40 @@ namespace Duplicati.Library.Snapshots.Windows
}
}
/// <summary>
/// Interface for Hyper-V utility to query Hyper-V guests and their paths
/// </summary>
public interface IHyperVUtility
{
/// <summary>
/// The Hyper-V VSS Writer Guid
/// </summary>
Guid HyperVWriterGuid { get; }
/// <summary>
/// Hyper-V is supported only on Windows platform
/// </summary>
bool IsHyperVInstalled { get; }
/// <summary>
/// Hyper-V writer is supported only on Server version of Windows
/// </summary>
bool IsVSSWriterSupported { get; }
/// <summary>
/// Enumerated Hyper-V guests
/// </summary>
List<HyperVGuest> Guests { get; }
/// <summary>
/// Query Hyper-V for all Virtual Machines info
/// </summary>
/// <param name="bIncludePaths">Specify if returned data should contain VM paths</param>
/// <param name="provider">The provider to use for VSS</param>
/// <returns>List of Hyper-V Machines</returns>
void QueryHyperVGuestsInfo(WindowsSnapshotProvider provider, bool bIncludePaths = false);
}
[SupportedOSPlatform("windows")]
public class HyperVUtility
public class HyperVUtility : IHyperVUtility
{
/// <summary>
/// The tag used for logging
@@ -102,7 +134,11 @@ namespace Duplicati.Library.Snapshots.Windows
/// <summary>
/// The Hyper-V VSS Writer Guid
/// </summary>
public static readonly Guid HyperVWriterGuid = new Guid("66841cd4-6ded-4f4b-8f17-fd23f8ddc3de");
internal static readonly Guid _HyperVWriterGuid = new Guid("66841cd4-6ded-4f4b-8f17-fd23f8ddc3de");
/// <summary>
/// The Hyper-V VSS Writer Guid
/// </summary>
public Guid HyperVWriterGuid => _HyperVWriterGuid;
/// <summary>
/// Hyper-V is supported only on Windows platform
/// </summary>
@@ -224,7 +260,7 @@ namespace Duplicati.Library.Snapshots.Windows
{
using (var vssBackupComponents = new SnapshotManager(provider))
{
var writerGUIDS = new[] { HyperVWriterGuid };
var writerGUIDS = new[] { _HyperVWriterGuid };
try
{
@@ -81,8 +81,30 @@ namespace Duplicati.Library.Snapshots.Windows
}
}
public interface IMSSQLUtility
{
/// <summary>
/// The MS SQL VSS Writer Guid
/// </summary>
Guid MSSQLWriterGuid { get; }
/// <summary>
/// MS SQL is supported only on Windows platform
/// </summary>
bool IsMSSQLInstalled { get; }
/// <summary>
/// Enumerated MS SQL DBs
/// </summary>
List<MSSQLDB> DBs { get; }
/// <summary>
/// For all MS SQL databases it enumerate all associated paths using VSS data
/// </summary>
/// <returns>A collection of DBs and paths</returns>
void QueryDBsInfo(WindowsSnapshotProvider provider);
}
[SupportedOSPlatform("windows")]
public class MSSQLUtility
public class MSSQLUtility : IMSSQLUtility
{
/// <summary>
/// The tag used for logging
@@ -91,7 +113,11 @@ namespace Duplicati.Library.Snapshots.Windows
/// <summary>
/// The MS SQL VSS Writer Guid
/// </summary>
public static readonly Guid MSSQLWriterGuid = new Guid("a65faa63-5ea8-4ebc-9dbd-a0c4db26912a");
private static readonly Guid _MSSQLWriterGuid = new Guid("a65faa63-5ea8-4ebc-9dbd-a0c4db26912a");
/// <summary>
/// The MS SQL VSS Writer Guid
/// </summary>
public Guid MSSQLWriterGuid => _MSSQLWriterGuid;
/// <summary>
/// MS SQL is supported only on Windows platform
/// </summary>
@@ -160,7 +186,7 @@ namespace Duplicati.Library.Snapshots.Windows
using (var vssBackupComponents = new SnapshotManager(provider))
{
var writerGUIDS = new[] { MSSQLWriterGuid };
var writerGUIDS = new[] { _MSSQLWriterGuid };
try
{
vssBackupComponents.SetupWriters(writerGUIDS, null);