2025-04-03 16:38:51 +02:00
// Copyright (C) 2025, The Duplicati Team
2025-02-06 16:53:23 +01:00
// 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
2024-03-04 12:21:53 +01:00
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
2024-04-24 11:16:11 +02:00
#nullable enable
2014-04-07 11:59:33 +02:00
using System ;
2025-04-03 16:38:51 +02:00
using System.Globalization ;
2024-03-01 14:30:28 +01:00
using Duplicati.Library.Common.IO ;
2024-04-23 17:06:32 +02:00
using Duplicati.Library.Interface ;
2020-02-17 09:34:39 -08:00
2014-04-07 11:59:33 +02:00
namespace Duplicati.Library.SQLiteHelper
{
public static class SQLiteLoader
2018-06-28 10:50:17 +02:00
{
2025-04-22 14:26:46 +02:00
/// <summary>
/// The minimum value for the SQLite page cache size
/// </summary>
public const long MINIMUM_SQLITE_PAGE_CACHE_SIZE = 2048000L ;
2018-06-14 21:43:20 +02:00
/// <summary>
2018-05-15 11:29:08 +02:00
/// The tag used for logging
/// </summary>
2018-06-14 21:43:20 +02:00
private static readonly string LOGTAG = Logging . Log . LogTagFromType ( typeof ( SQLiteLoader ));
2018-05-15 11:29:08 +02:00
2018-06-21 18:08:52 +02:00
/// <summary>
/// Helper method with logic to handle opening a database in possibly encrypted format
/// </summary>
/// <param name="con">The SQLite connection object</param>
2018-06-28 10:50:17 +02:00
/// <param name="databasePath">The location of Duplicati's database.</param>
2024-04-23 17:06:32 +02:00
/// <param name="decryptionPassword">The password to use for decryption.</param>
2024-04-24 11:16:11 +02:00
public static void OpenDatabase ( System . Data . IDbConnection con , string databasePath , string? decryptionPassword )
2018-06-21 18:08:52 +02:00
{
2024-04-24 11:16:11 +02:00
if (! string . IsNullOrWhiteSpace ( decryptionPassword ) && SQLiteRC4Decrypter . IsDatabaseEncrypted ( databasePath ))
2024-04-23 17:06:32 +02:00
{
Logging . Log . WriteWarningMessage ( LOGTAG , "SQLiteRC4Decrypter" , null , "Database is encrypted, attempting to decrypt..." );
try
{
SQLiteRC4Decrypter . DecryptSQLiteFile ( databasePath , decryptionPassword );
Logging . Log . WriteInformationMessage ( LOGTAG , "SQLiteRC4Decrypter" , "Database decrypted successfully." );
}
catch ( Exception ex )
{
Logging . Log . WriteErrorMessage ( LOGTAG , "SQLiteRC4Decrypter" , ex , "Failed to decrypt database" );
2024-05-08 12:48:10 +02:00
throw new UserInformationException ( $"The database appears to be encrypted, but the decrypting failed. Please check the password. Error message: {ex.Message}" , "RC4DecryptionFailed" , ex );
2024-04-23 17:06:32 +02:00
}
}
2018-06-21 18:08:52 +02:00
try
{
//Attempt to open in preferred state
2018-06-28 10:50:17 +02:00
OpenSQLiteFile ( con , databasePath );
2018-06-21 18:58:55 +02:00
TestSQLiteFile ( con );
2018-06-21 18:08:52 +02:00
}
catch
{
2024-04-23 17:06:32 +02:00
try { con . Dispose (); }
catch { }
2018-06-21 18:08:52 +02:00
2024-04-23 17:06:32 +02:00
throw ;
2018-06-21 18:08:52 +02:00
}
2024-04-23 17:06:32 +02:00
if ( con . State != System . Data . ConnectionState . Open )
throw new UserInformationException ( "Failed to open database for unknown reason, check the logs to see error messages" , "DatabaseOpenFailed" );
2018-06-21 18:08:52 +02:00
}
2017-01-15 23:09:47 +01:00
/// <summary>
2018-06-11 18:50:11 +02:00
/// Loads an SQLite connection instance and opening the database
2017-01-15 23:09:47 +01:00
/// </summary>
/// <returns>The SQLite connection instance.</returns>
2018-06-11 18:50:11 +02:00
public static System . Data . IDbConnection LoadConnection ()
2017-01-15 23:09:47 +01:00
{
2024-04-24 11:16:11 +02:00
System . Data . IDbConnection ? con = null ;
2018-06-19 08:41:07 +02:00
SetEnvironmentVariablesForSQLiteTempDir ();
2018-06-14 22:00:59 +02:00
2017-01-15 23:09:47 +01:00
try
{
2024-04-24 11:16:11 +02:00
con = ( System . Data . IDbConnection ?) Activator . CreateInstance ( Duplicati . Library . SQLiteHelper . SQLiteLoader . SQLiteConnectionType );
2018-06-11 18:50:11 +02:00
}
2018-06-11 19:10:14 +02:00
catch ( Exception ex )
2018-06-11 18:50:11 +02:00
{
2018-06-11 19:10:14 +02:00
Logging . Log . WriteErrorMessage ( LOGTAG , "FailedToLoadConnectionSQLite" , ex , "Failed to load connection." );
2018-06-11 22:06:06 +02:00
DisposeConnection ( con );
2018-06-11 18:50:11 +02:00
throw ;
2018-06-28 10:50:17 +02:00
}
2018-06-11 18:50:11 +02:00
2024-04-24 11:16:11 +02:00
return con ?? throw new InvalidOperationException ( "Failed to load connection" );
2018-06-11 18:50:11 +02:00
}
2025-03-20 10:05:10 +01:00
/// <summary>
/// Applies user-supplied custom pragmas to the SQLite connection
/// </summary>
/// <param name="con">The connection to apply the pragmas to.</param>
2025-04-22 14:26:46 +02:00
/// <param name="pagecachesize"> The page cache size to set.</param>
2025-03-20 10:05:10 +01:00
/// <returns>The connection with the pragmas applied.</returns>
2025-04-22 14:26:46 +02:00
public static System . Data . IDbConnection ApplyCustomPragmas ( System . Data . IDbConnection con , long pagecachesize )
2025-03-20 10:05:10 +01:00
{
2025-04-22 14:26:46 +02:00
var opts = Environment . GetEnvironmentVariable ( "CUSTOMSQLITEOPTIONS_DUPLICATI" ) ?? "" ;
if ( pagecachesize > MINIMUM_SQLITE_PAGE_CACHE_SIZE )
opts = string . Format ( CultureInfo . InvariantCulture , "cache_size=-{0};{1}" , pagecachesize / 1024L , opts );
2025-03-20 12:05:16 +01:00
if ( string . IsNullOrWhiteSpace ( opts ))
2025-03-20 10:05:10 +01:00
return con ;
using ( var cmd = con . CreateCommand ())
2025-03-20 12:05:16 +01:00
foreach ( var opt in opts . Split ( new char [] { ';' }, StringSplitOptions . RemoveEmptyEntries ))
2025-03-20 10:05:10 +01:00
{
Logging . Log . WriteVerboseMessage ( LOGTAG , "CustomSQLiteOption" , @"Setting custom SQLite option '{0}'." , opt );
try
{
2025-04-03 16:38:51 +02:00
cmd . CommandText = string . Format ( CultureInfo . InvariantCulture , "PRAGMA {0}" , opt );
2025-03-20 10:05:10 +01:00
cmd . ExecuteNonQuery ();
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( LOGTAG , "CustomSQLiteOption" , ex , @"Error setting custom SQLite option '{0}'." , opt );
}
}
return con ;
}
2018-06-11 18:50:11 +02:00
/// <summary>
/// Loads an SQLite connection instance and opening the database
/// </summary>
/// <returns>The SQLite connection instance.</returns>
/// <param name="targetpath">The optional path to the database.</param>
2025-04-22 14:26:46 +02:00
/// <param name="pagecachesize"> The page cache size to set.</param>
public static System . Data . IDbConnection LoadConnection ( string targetpath , long pagecachesize )
2018-06-11 18:50:11 +02:00
{
2018-06-28 10:50:17 +02:00
if ( string . IsNullOrWhiteSpace ( targetpath ))
throw new ArgumentNullException ( nameof ( targetpath ));
2024-05-08 12:48:10 +02:00
2025-03-20 10:05:10 +01:00
var con = LoadConnection ();
2018-06-11 18:50:11 +02:00
try
{
2018-06-28 10:50:17 +02:00
OpenSQLiteFile ( con , targetpath );
2017-01-15 23:09:47 +01:00
}
2018-06-11 19:10:14 +02:00
catch ( Exception ex )
2017-01-15 23:09:47 +01:00
{
2018-06-11 19:10:14 +02:00
Logging . Log . WriteErrorMessage ( LOGTAG , "FailedToLoadConnectionSQLite" , ex , @"Failed to load connection with path '{0}'." , targetpath );
2018-06-11 22:06:06 +02:00
DisposeConnection ( con );
2017-01-15 23:09:47 +01:00
throw ;
}
2023-03-19 00:48:10 +01:00
2024-05-08 12:48:10 +02:00
// set custom Sqlite options
2025-04-22 14:26:46 +02:00
return ApplyCustomPragmas ( con , pagecachesize );
2017-01-15 23:09:47 +01:00
}
2014-04-07 11:59:33 +02:00
/// <summary>
/// Returns the SQLiteCommand type for the current architecture
/// </summary>
public static Type SQLiteConnectionType
{
get
{
2024-03-05 08:55:13 +01:00
return typeof ( System . Data . SQLite . SQLiteConnection );
}
}
/// <summary>
/// Returns the version string from the SQLite type
/// </summary>
2024-04-24 11:16:11 +02:00
public static string? SQLiteVersion
2024-03-05 08:55:13 +01:00
{
get
{
var versionString = SQLiteConnectionType . GetProperty ( "SQLiteVersion" )?. GetValue ( null , null ) as string ;
if ( string . IsNullOrWhiteSpace ( versionString ))
{
// Support for Microsoft.Data.SQLite
// NOTE: Has an issue with ? as position parameters
var inst = Activator . CreateInstance ( SQLiteConnectionType );
versionString = SQLiteConnectionType . GetProperty ( "ServerVersion" )?. GetValue ( inst , null ) as string ;
}
return versionString ;
2014-04-07 11:59:33 +02:00
}
}
2018-06-11 22:06:06 +02:00
2018-06-19 08:41:07 +02:00
/// <summary>
/// Set environment variables to be used by SQLite to determine which folder to use for temporary files.
/// From SQLite's documentation, SQLITE_TMPDIR is used for unix-like systems.
/// For Windows, TMP and TEMP environment variables are used.
/// </summary>
private static void SetEnvironmentVariablesForSQLiteTempDir ()
{
2025-03-20 10:05:10 +01:00
// Allow the user to override the temp folder for SQLite
if ( string . IsNullOrWhiteSpace ( Environment . GetEnvironmentVariable ( "SQLITE_TMPDIR" )))
Environment . SetEnvironmentVariable ( "SQLITE_TMPDIR" , Utility . TempFolder . SystemTempPath );
Environment . SetEnvironmentVariable ( "TMPDIR" , Utility . TempFolder . SystemTempPath );
Environment . SetEnvironmentVariable ( "TMP" , Utility . TempFolder . SystemTempPath );
Environment . SetEnvironmentVariable ( "TEMP" , Utility . TempFolder . SystemTempPath );
2018-06-19 08:41:07 +02:00
}
2018-06-11 22:06:06 +02:00
2018-06-28 10:50:17 +02:00
/// <summary>
/// Wrapper to dispose the SQLite connection
/// </summary>
/// <param name="con">The connection to close.</param>
2024-04-24 11:16:11 +02:00
private static void DisposeConnection ( System . Data . IDbConnection ? con )
2018-06-11 22:06:06 +02:00
{
if ( con != null )
try { con . Dispose (); }
2018-06-28 10:50:17 +02:00
catch ( Exception ex ) { Logging . Log . WriteExplicitMessage ( LOGTAG , "ConnectionDisposeError" , ex , "Failed to dispose connection" ); }
}
2018-06-27 13:36:08 +02:00
/// <summary>
/// Opens the SQLite file in the given connection, creating the file if required
/// </summary>
/// <param name="con">The connection to use.</param>
/// <param name="path">Path to the file to open, which may not exist.</param>
2018-06-21 18:58:55 +02:00
private static void OpenSQLiteFile ( System . Data . IDbConnection con , string path )
{
con . ConnectionString = "Data Source=" + path ;
2018-06-28 10:50:17 +02:00
con . Open ();
2024-06-04 19:35:06 +02:00
if ( con is System . Data . SQLite . SQLiteConnection sqlitecon && ! OperatingSystem . IsMacOS ())
2024-05-22 00:10:43 +02:00
{
2024-06-04 19:35:06 +02:00
// These configuration options crash on MacOS (arm64), but the other platforms should be enough to detect incorrect SQL
2024-05-22 00:10:43 +02:00
sqlitecon . SetConfigurationOption ( System . Data . SQLite . SQLiteConfigDbOpsEnum . SQLITE_DBCONFIG_DQS_DDL , false );
sqlitecon . SetConfigurationOption ( System . Data . SQLite . SQLiteConfigDbOpsEnum . SQLITE_DBCONFIG_DQS_DML , false );
}
2025-03-19 21:20:07 +01:00
// Make the file only accessible by the current user, unless opting out
if (! SystemIO . IO_OS . FileExists ( SystemIO . IO_OS . PathCombine ( SystemIO . IO_OS . PathGetDirectoryName ( path ), Util . InsecurePermissionsMarkerFile )))
2025-03-31 16:07:50 +02:00
try { SystemIO . IO_OS . FileSetPermissionUserRWOnly ( path ); }
catch ( Exception ex ) { Logging . Log . WriteWarningMessage ( LOGTAG , "SQLiteFilePermissionError" , ex , "Failed to set permissions on SQLite file '{0}'" , path ); }
2018-06-21 18:58:55 +02:00
}
2018-06-28 10:50:17 +02:00
/// <summary>
/// Tests the SQLite connection, throwing an exception if the connection does not work
/// </summary>
/// <param name="con">The connection to test.</param>
2018-06-21 18:58:55 +02:00
private static void TestSQLiteFile ( System . Data . IDbConnection con )
{
// Do a dummy query to make sure we have a working db
using ( var cmd = con . CreateCommand ())
{
cmd . CommandText = "SELECT COUNT(*) FROM SQLITE_MASTER" ;
cmd . ExecuteScalar ();
}
2018-06-11 22:06:06 +02:00
}
2014-04-07 11:59:33 +02:00
}
}