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
2025-05-12 10:52:18 +02:00
//
// 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
2025-02-06 16:53:23 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-12 10:52:18 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-02-06 16:53:23 +01:00
// all copies or substantial portions of the Software.
2025-05-12 10:52:18 +02:00
//
// 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 ;
2025-05-12 10:55:22 +02:00
using System.Threading.Tasks ;
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 ;
2025-05-14 10:44:24 +02:00
using Duplicati.Library.Utility ;
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
2025-05-14 10:44:24 +02:00
public static void OpenDatabase ( Microsoft . Data . Sqlite . SqliteConnection con , string databasePath , string? decryptionPassword )
{
OpenDatabaseAsync ( con , databasePath , decryptionPassword ). Await ();
}
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>
2025-05-14 10:44:24 +02:00
public static async Task OpenDatabaseAsync ( Microsoft . Data . Sqlite . SqliteConnection 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
2025-05-14 10:44:24 +02:00
await OpenSQLiteFileAsync ( con , databasePath );
await TestSQLiteFileAsync ( con );
2018-06-21 18:08:52 +02:00
}
catch
{
2025-05-12 10:55:22 +02:00
try { await con . DisposeAsync (); }
2024-04-23 17:06:32 +02:00
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
}
2025-05-14 10:44:24 +02:00
public static Microsoft . Data . Sqlite . SqliteConnection LoadConnection ()
{
return LoadConnectionAsync (). Await ();
}
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>
2025-05-14 10:44:24 +02:00
public static async Task < Microsoft . Data . Sqlite . SqliteConnection > LoadConnectionAsync ()
2017-01-15 23:09:47 +01:00
{
2025-05-12 10:54:17 +02:00
Microsoft . Data . Sqlite . SqliteConnection ? 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
{
2025-05-12 10:54:17 +02:00
con = new Microsoft . Data . Sqlite . SqliteConnection ();
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." );
2025-05-14 10:44:24 +02:00
await DisposeConnectionAsync ( 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-05-14 10:44:24 +02:00
public static Microsoft . Data . Sqlite . SqliteConnection ApplyCustomPragmas ( Microsoft . Data . Sqlite . SqliteConnection con , long pagecachesize )
{
return ApplyCustomPragmasAsync ( con , pagecachesize ). Await ();
}
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-05-14 10:44:24 +02:00
public static async Task < Microsoft . Data . Sqlite . SqliteConnection > ApplyCustomPragmasAsync ( Microsoft . Data . Sqlite . SqliteConnection con , long pagecachesize )
2025-03-20 10:05:10 +01:00
{
2025-05-12 10:56:22 +02:00
// TODO more default custom options
2025-05-14 10:44:24 +02:00
//await cmd.ExecuteNonQueryAsync("PRAGMA synchronous = NORMAL;");
//await cmd.ExecuteNonQueryAsync("PRAGMA temp_store = MEMORY;");
//await cmd.ExecuteNonQueryAsync("PRAGMA journal_mode = WAL;");
//await cmd.ExecuteNonQueryAsync("PRAGMA cache_size = -512000;");
//await cmd.ExecuteNonQueryAsync("PRAGMA threads = 8;");
//await cmd.ExecuteNonQueryAsync("PRAGMA read_uncommitted = 1;");
//await cmd.ExecuteNonQueryAsync("PRAGMA mmap_size = 41943040;");
//await cmd.ExecuteNonQueryAsync("PRAGMA shared_cache = true;");
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-05-12 10:54:17 +02:00
using ( var transaction = con . BeginTransaction ())
{
cmd . Transaction = transaction ;
2025-05-12 10:56:05 +02:00
foreach ( var opt in opts . Split ([ ';' ], 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-05-12 10:55:22 +02:00
await cmd . ExecuteNonQueryAsync ();
2025-03-20 10:05:10 +01:00
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( LOGTAG , "CustomSQLiteOption" , ex , @"Error setting custom SQLite option '{0}'." , opt );
}
}
2025-05-12 10:55:22 +02:00
await transaction . CommitAsync ();
}
2025-03-20 10:05:10 +01:00
return con ;
}
2025-05-14 10:44:24 +02:00
public static Microsoft . Data . Sqlite . SqliteConnection LoadConnection ( string targetpath , long pagecachesize )
{
return LoadConnectionAsync ( targetpath , pagecachesize ). Await ();
}
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>
2025-05-14 10:44:24 +02:00
public static async Task < Microsoft . Data . Sqlite . SqliteConnection > LoadConnectionAsync ( 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-05-14 10:44:24 +02:00
var con = await LoadConnectionAsync ();
2018-06-11 18:50:11 +02:00
try
{
2025-05-14 10:44:24 +02:00
await OpenSQLiteFileAsync ( 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 );
2025-05-14 10:44:24 +02:00
await DisposeConnectionAsync ( 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-05-14 10:44:24 +02:00
return await ApplyCustomPragmasAsync ( 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
{
2025-05-12 10:54:17 +02:00
return typeof ( Microsoft . Data . Sqlite . SqliteConnection );
2024-03-05 08:55:13 +01:00
}
}
/// <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
2025-05-14 10:44:24 +02:00
private static void DisposeConnection ( Microsoft . Data . Sqlite . SqliteConnection ? con )
{
DisposeConnectionAsync ( con ). Await ();
}
2018-06-28 10:50:17 +02:00
/// <summary>
/// Wrapper to dispose the SQLite connection
/// </summary>
/// <param name="con">The connection to close.</param>
2025-05-14 10:44:24 +02:00
private static async Task DisposeConnectionAsync ( Microsoft . Data . Sqlite . SqliteConnection ? con )
2018-06-11 22:06:06 +02:00
{
if ( con != null )
2025-05-12 10:55:22 +02:00
try { await con . DisposeAsync (); }
2018-06-28 10:50:17 +02:00
catch ( Exception ex ) { Logging . Log . WriteExplicitMessage ( LOGTAG , "ConnectionDisposeError" , ex , "Failed to dispose connection" ); }
}
2025-05-14 10:44:24 +02:00
private static void OpenSQLiteFile ( Microsoft . Data . Sqlite . SqliteConnection con , string path )
{
OpenSQLiteFileAsync ( con , path ). Await ();
}
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>
2025-05-14 10:44:24 +02:00
private static async Task OpenSQLiteFileAsync ( Microsoft . Data . Sqlite . SqliteConnection con , string path )
2018-06-21 18:58:55 +02:00
{
con . ConnectionString = "Data Source=" + path ;
2025-05-12 10:55:22 +02:00
await con . OpenAsync ();
2025-05-12 10:56:22 +02:00
// TODO legacy configuration?
//if (con is Microsoft.Data.Sqlite.SqliteConnection sqlitecon && !OperatingSystem.IsMacOS())
//{
// // These configuration options crash on MacOS (arm64), but the other platforms should be enough to detect incorrect SQL
// sqlitecon.SetConfigurationOption(System.Data.SQLite.SQLiteConfigDbOpsEnum.SQLITE_DBCONFIG_DQS_DDL, false);
// sqlitecon.SetConfigurationOption(System.Data.SQLite.SQLiteConfigDbOpsEnum.SQLITE_DBCONFIG_DQS_DML, false);
//}
2024-05-22 00:10:43 +02:00
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
}
2025-05-14 10:44:24 +02:00
private static void TestSQLiteFile ( Microsoft . Data . Sqlite . SqliteConnection con )
{
TestSQLiteFileAsync ( con ). Await ();
}
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>
2025-05-14 10:44:24 +02:00
private static async Task TestSQLiteFileAsync ( Microsoft . Data . Sqlite . SqliteConnection con )
2018-06-21 18:58:55 +02:00
{
// Do a dummy query to make sure we have a working db
2025-05-12 10:56:05 +02:00
using var cmd = con . CreateCommand ();
cmd . CommandText = "SELECT COUNT(*) FROM SQLITE_MASTER" ;
2025-05-12 10:55:22 +02:00
await cmd . ExecuteScalarAsync ();
2018-06-11 22:06:06 +02:00
}
2014-04-07 11:59:33 +02:00
}
}