2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
2017-01-09 11:35:38 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text.RegularExpressions ;
2018-11-02 22:13:25 +01:00
using Duplicati.Library.Common ;
2017-01-09 11:35:38 +01:00
using Duplicati.Library.Snapshots ;
namespace Duplicati.Library.Modules.Builtin
{
public class MSSQLOptions : Interface . IGenericSourceModule
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < MSSQLOptions >();
2017-01-09 11:35:38 +01:00
private const string m_MSSQLPathDBRegExp = @"\%MSSQL\%\\(.+)" ;
private const string m_MSSQLPathAllRegExp = @"%MSSQL%" ;
#region IGenericModule Members
public string Key
{
get { return "mssql-options" ; }
}
public string DisplayName
{
get { return Strings . MSSQLOptions . DisplayName ; }
}
public string Description
{
get { return Strings . MSSQLOptions . Description ; }
}
public bool LoadAsDefault
{
2018-11-02 22:13:25 +01:00
get { return Platform . IsClientWindows ; }
2017-01-09 11:35:38 +01:00
}
public IList < Interface . ICommandLineArgument > SupportedCommands
{
get { return null ; }
}
public void Configure ( IDictionary < string , string > commandlineOptions )
{
2018-10-10 21:28:00 -07:00
// Do nothing. Implementation needed for IGenericModule interface.
2017-01-09 11:35:38 +01:00
}
#endregion
#region Implementation of IGenericSourceModule
public Dictionary < string , string > ParseSourcePaths ( ref string [] paths , ref string filter , Dictionary < string , string > commandlineOptions )
{
// Early exit in case we are non-windows to prevent attempting to load Windows-only components
2018-11-02 22:13:25 +01:00
if (! Platform . IsClientWindows )
2017-01-09 11:35:38 +01:00
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteWarningMessage ( LOGTAG , "MSSqlWindowsOnly" , null , "Microsoft SQL Server databases backup works only on Windows OS" );
2017-01-09 11:35:38 +01:00
if ( paths != null )
paths = paths . Where ( x => ! x . Equals ( m_MSSQLPathAllRegExp , StringComparison . OrdinalIgnoreCase ) && ! Regex . IsMatch ( x , m_MSSQLPathDBRegExp , RegexOptions . IgnoreCase | RegexOptions . CultureInvariant )). ToArray ();
if (! string . IsNullOrEmpty ( filter ))
{
var filters = filter . Split ( new string [] { System . IO . Path . PathSeparator . ToString () }, StringSplitOptions . RemoveEmptyEntries );
var remainingfilters = filters . Where ( x => ! Regex . IsMatch ( x , m_MSSQLPathDBRegExp , RegexOptions . IgnoreCase | RegexOptions . CultureInvariant )). ToArray ();
filter = string . Join ( System . IO . Path . PathSeparator . ToString (), remainingfilters );
}
return new Dictionary < string , string >();
}
// Windows, do the real stuff!
return RealParseSourcePaths ( ref paths , ref filter , commandlineOptions );
}
// Make sure the JIT does not attempt to inline this call and thus load
// referenced types from System.Management here
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private Dictionary < string , string > RealParseSourcePaths ( ref string [] paths , ref string filter , Dictionary < string , string > commandlineOptions )
{
var changedOptions = new Dictionary < string , string >();
var filtersInclude = new List < string >();
var filtersExclude = new List < string >();
if (! string . IsNullOrEmpty ( filter ))
{
var filters = filter . Split ( new string [] { System . IO . Path . PathSeparator . ToString () }, StringSplitOptions . RemoveEmptyEntries );
2017-11-26 10:53:14 -08:00
filtersInclude = filters . Where ( x => x . StartsWith ( "+" , StringComparison . Ordinal ) && Regex . IsMatch ( x , m_MSSQLPathDBRegExp , RegexOptions . IgnoreCase | RegexOptions . CultureInvariant ))
2017-01-09 11:35:38 +01:00
. Select ( x => Regex . Match ( x . Substring ( 1 ), m_MSSQLPathDBRegExp , RegexOptions . IgnoreCase | RegexOptions . CultureInvariant ). Groups [ 1 ]. Value ). ToList ();
2017-11-26 10:53:14 -08:00
filtersExclude = filters . Where ( x => x . StartsWith ( "-" , StringComparison . Ordinal ) && Regex . IsMatch ( x , m_MSSQLPathDBRegExp , RegexOptions . IgnoreCase | RegexOptions . CultureInvariant ))
2017-01-09 11:35:38 +01:00
. Select ( x => Regex . Match ( x . Substring ( 1 ), m_MSSQLPathDBRegExp , RegexOptions . IgnoreCase | RegexOptions . CultureInvariant ). Groups [ 1 ]. Value ). 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 ();
if ( paths == null || ! ContainFilesForBackup ( paths ) || ! mssqlUtility . IsMSSQLInstalled )
return changedOptions ;
if ( commandlineOptions . Keys . Contains ( "vss-exclude-writers" ))
{
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 ))
{
2018-03-12 14:07:11 +01:00
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 ());
2017-01-09 11:35:38 +01:00
changedOptions [ "vss-exclude-writers" ] = string . Join ( ";" , excludedWriters . Where ( x => x != MSSQLUtility . MSSQLWriterGuid ));
}
}
if (! commandlineOptions . Keys . Contains ( "snapshot-policy" ) || ! commandlineOptions [ "snapshot-policy" ]. Equals ( "required" , StringComparison . OrdinalIgnoreCase ))
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteWarningMessage ( LOGTAG , "MustSetSnapshotPolicy" , null , "Snapshot policy have to be set to \"required\" when backuping Microsoft SQL Server databases. Changing to \"required\" to continue" );
2017-01-09 11:35:38 +01:00
changedOptions [ "snapshot-policy" ] = "required" ;
}
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "StartingMsSqlQuery" , "Starting to gather Microsoft SQL Server information" , Logging . LogMessageType . Information );
2017-01-09 11:35:38 +01:00
mssqlUtility . QueryDBsInfo ();
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "MsSqlDatabaseCount" , "Found {0} databases on Microsoft SQL Server" , mssqlUtility . DBs . Count );
2017-01-09 11:35:38 +01:00
foreach ( var db in mssqlUtility . DBs )
2018-03-12 14:07:11 +01:00
Logging . Log . WriteProfilingMessage ( LOGTAG , "MsSqlDatabaseName" , "Found DB name {0}, ID {1}, files {2}" , db . Name , db . ID , string . Join ( ";" , db . DataPaths ));
2017-01-09 11:35:38 +01:00
List < MSSQLDB > dbsForBackup = new List < MSSQLDB >();
if ( paths . Contains ( m_MSSQLPathAllRegExp , 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 foundDB = mssqlUtility . DBs . Where ( x => x . ID . Equals ( dbID , StringComparison . OrdinalIgnoreCase ));
if ( foundDB . Count () != 1 )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( string . Format ( "DB name specified in source with ID {0} cannot be found" , dbID ), "MsSqlDatabaseNotFound" );
2017-01-09 11:35:38 +01:00
dbsForBackup . Add ( foundDB . First ());
}
if ( filtersInclude . Count > 0 )
foreach ( var dbID in filtersInclude )
{
var foundDB = mssqlUtility . DBs . Where ( x => x . ID . Equals ( dbID , StringComparison . OrdinalIgnoreCase ));
if ( foundDB . Count () != 1 )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( string . Format ( "DB name specified in include filter with ID {0} cannot be found" , dbID ), "MsSqlDatabaseNotFound" );
2017-01-09 11:35:38 +01:00
dbsForBackup . Add ( foundDB . First ());
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "IncludeByFilter" , "Including {0} based on including filters" , dbID );
2017-01-09 11:35:38 +01:00
}
dbsForBackup = dbsForBackup . Distinct (). ToList ();
if ( filtersExclude . Count > 0 )
foreach ( var dbID in filtersExclude )
{
var foundDB = dbsForBackup . Where ( x => x . ID . Equals ( dbID , StringComparison . OrdinalIgnoreCase ));
if ( foundDB . Count () != 1 )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( string . Format ( "DB name specified in exclude filter with ID {0} cannot be found" , dbID ), "MsSqlDatabaseNotFound" );
2017-01-09 11:35:38 +01:00
dbsForBackup . Remove ( foundDB . First ());
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "ExcludeByFilter" , "Excluding {0} based on excluding filters" , dbID );
2017-01-09 11:35:38 +01:00
}
var pathsForBackup = new List < string >( paths );
var filterhandler = new Utility . FilterExpression (
2017-11-26 10:53:14 -08:00
filter . Split ( new string [] { System . IO . Path . PathSeparator . ToString () }, StringSplitOptions . RemoveEmptyEntries ). Where ( x => x . StartsWith ( "-" , StringComparison . Ordinal )). Select ( x => x . Substring ( 1 )). ToList ());
2017-01-09 11:35:38 +01:00
foreach ( var dbForBackup in dbsForBackup )
foreach ( var pathForBackup in dbForBackup . DataPaths )
{
2019-04-16 21:35:02 -07:00
if (! filterhandler . Matches ( pathForBackup , out _ , out _ ))
2017-01-09 11:35:38 +01:00
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "IncludeDatabase" , "For DB {0} - adding {1}" , dbForBackup . Name , pathForBackup );
2017-01-09 11:35:38 +01:00
pathsForBackup . Add ( pathForBackup );
}
else
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "ExcludeByFilter" , "Excluding {0} based on excluding filters" , pathForBackup );
2017-01-09 11:35:38 +01:00
}
paths = pathsForBackup . Where ( x => ! x . Equals ( m_MSSQLPathAllRegExp , StringComparison . OrdinalIgnoreCase ) && ! Regex . IsMatch ( x , m_MSSQLPathDBRegExp , RegexOptions . IgnoreCase | RegexOptions . CultureInvariant ))
. Distinct ( Utility . Utility . ClientFilenameStringComparer ). OrderBy ( a => a ). ToArray ();
return changedOptions ;
}
public bool ContainFilesForBackup ( string [] paths )
{
2018-11-02 22:13:25 +01:00
if ( paths == null || ! Platform . IsClientWindows )
2017-01-09 11:35:38 +01:00
return false ;
2018-10-06 15:50:05 -07:00
return paths . Where ( x => ! string . IsNullOrWhiteSpace ( x )). Any ( x => x . Equals ( m_MSSQLPathAllRegExp , StringComparison . OrdinalIgnoreCase ) || Regex . IsMatch ( x , m_MSSQLPathDBRegExp , RegexOptions . IgnoreCase | RegexOptions . CultureInvariant ));
2017-01-09 11:35:38 +01:00
}
#endregion
}
}