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
{
2025-06-18 08:26:20 +02:00
/// <summary>
/// Provides methods to load and manage SQLite connections, including handling encrypted databases.
/// </summary>
2014-04-07 11:59:33 +02:00
public static class SQLiteLoader
2018-06-28 10:50:17 +02:00
{
2025-04-22 14:26:46 +02:00
/// <summary>
2025-06-18 08:26:20 +02:00
/// The minimum value for the SQLite page cache size.
2025-04-22 14:26:46 +02:00
/// </summary>
public const long MINIMUM_SQLITE_PAGE_CACHE_SIZE = 2048000L ;
2018-06-14 21:43:20 +02:00
/// <summary>
2025-06-18 08:26:20 +02:00
/// The tag used for logging.
2018-05-15 11:29:08 +02:00
/// </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>
2025-06-18 08:26:20 +02:00
/// Helper method with logic to handle opening a database in possibly encrypted format.
2018-06-21 18:08:52 +02:00
/// </summary>
2025-06-18 08:26:20 +02:00
/// <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-06-18 08:26:20 +02:00
/// <returns>A task that completes when the database is opened.</returns>
/// <exception cref="UserInformationException">Thrown if the database cannot be opened or decrypted.</exception>
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-06-12 08:34:29 +02:00
await OpenSQLiteFileAsync ( con , databasePath )
. ConfigureAwait ( false );
await TestSQLiteFileAsync ( con ). ConfigureAwait ( false );
2018-06-21 18:08:52 +02:00
}
catch
{
2025-06-12 08:34:29 +02:00
try { await con . DisposeAsync (). ConfigureAwait ( false ); }
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-06-18 08:26:20 +02:00
/// <summary>
/// Loads an SQLite connection instance and opening the database.
/// </summary>
/// <returns>The SQLite connection instance.</returns>
/// <remarks>
/// This method is synchronous and should be used when you need to load the connection immediately. It calls the asynchronous version and waits for it to complete.
/// </remarks>
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>
2025-06-18 08:26:20 +02:00
/// Loads an SQLite connection instance and opening the database.
2017-01-15 23:09:47 +01:00
/// </summary>
2025-06-18 08:26:20 +02:00
/// <returns>A task that when awaited 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-06-12 08:34:29 +02:00
await DisposeConnectionAsync ( con ). ConfigureAwait ( false );
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>
2025-06-18 08:26:20 +02:00
/// Applies user-supplied custom pragmas to the SQLite connection.
2025-03-20 10:05:10 +01:00
/// </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-06-18 08:26:20 +02:00
/// <returns>A task that when awaited 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
{
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-06-12 08:34:29 +02:00
await cmd . ExecuteNonQueryAsync (). ConfigureAwait ( false );
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
}
2025-03-20 10:05:10 +01:00
return con ;
}
2025-06-18 08:26:20 +02:00
/// <summary>
/// Loads an SQLite connection instance and opening the database with a specified page cache size.
/// </summary>
/// <param name="targetpath">The optional path to the database.</param>
/// <param name="pagecachesize">The page cache size to set.</param>
/// <returns>The SQLite connection instance.</returns>
/// <remarks>
/// This method is synchronous and should be used when you need to load the connection immediately. It calls the asynchronous version and waits for it to complete.
/// </remarks>
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>
2025-06-18 08:26:20 +02:00
/// Loads an SQLite connection instance and opening the database.
2018-06-11 18:50:11 +02:00
/// </summary>
/// <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-06-18 08:26:20 +02:00
/// <returns>A task that when waited returns the SQLite connection instance.</returns>
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-06-12 08:34:29 +02:00
var con = await LoadConnectionAsync (). ConfigureAwait ( false );
2018-06-11 18:50:11 +02:00
try
{
2025-06-12 08:34:29 +02:00
await OpenSQLiteFileAsync ( con , targetpath ). ConfigureAwait ( false );
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-06-12 08:34:29 +02:00
await DisposeConnectionAsync ( con ). ConfigureAwait ( false );
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-06-12 08:34:29 +02:00
return await ApplyCustomPragmasAsync ( con , pagecachesize )
. ConfigureAwait ( false );
2017-01-15 23:09:47 +01:00
}
2014-04-07 11:59:33 +02:00
/// <summary>
2025-06-18 08:26:20 +02:00
/// Returns the SQLiteCommand type for the current architecture.
2014-04-07 11:59:33 +02:00
/// </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>
2025-06-18 08:26:20 +02:00
/// Returns the version string from the SQLite type.
2024-03-05 08:55:13 +01:00
/// </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>
2025-06-18 08:26:20 +02:00
/// Wrapper to dispose the SQLite connection.
2018-06-28 10:50:17 +02:00
/// </summary>
/// <param name="con">The connection to close.</param>
2025-06-18 08:26:20 +02:00
/// <returns>A task that completes when the connection is disposed.</returns>
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-06-12 08:34:29 +02:00
try { await con . DisposeAsync (). ConfigureAwait ( false ); }
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>
2025-06-18 08:26:20 +02:00
/// Opens the SQLite file in the given connection, creating the file if required.
2018-06-27 13:36:08 +02:00
/// </summary>
/// <param name="con">The connection to use.</param>
/// <param name="path">Path to the file to open, which may not exist.</param>
2025-06-18 08:26:20 +02:00
/// <returns>A task that completes when the file is opened.</returns>
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
{
2025-05-27 05:39:31 +02:00
con . ConnectionString = $"Data Source={path};Pooling=false" ;
2025-06-12 08:34:29 +02:00
await con . OpenAsync (). ConfigureAwait ( 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
}
2018-06-28 10:50:17 +02:00
/// <summary>
2025-06-18 08:26:20 +02:00
/// Tests the SQLite connection, throwing an exception if the connection does not work.
2018-06-28 10:50:17 +02:00
/// </summary>
/// <param name="con">The connection to test.</param>
2025-06-18 08:26:20 +02:00
/// <returns>A task that completes when the test query is executed.</returns>
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-06-12 08:34:29 +02:00
await cmd . ExecuteScalarAsync (). ConfigureAwait ( false );
2018-06-11 22:06:06 +02:00
}
2014-04-07 11:59:33 +02:00
}
}