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-06-19 11:48:05 +02:00
using System.Collections.Generic ;
2025-04-03 16:38:51 +02:00
using System.Globalization ;
2025-06-19 11:48:05 +02:00
using System.Threading ;
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
{
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-06-18 08:26:20 +02:00
/// <returns>A task that when awaited returns the connection with the pragmas applied.</returns>
2025-06-19 11:41:39 +02:00
public static async Task < Microsoft . Data . Sqlite . SqliteConnection > ApplyCustomPragmasAsync ( Microsoft . Data . Sqlite . SqliteConnection con )
2025-03-20 10:05:10 +01:00
{
2025-06-19 11:48:05 +02:00
Dictionary < string , string > customOptions = new Dictionary < string , string >( StringComparer . OrdinalIgnoreCase )
{
{ "synchronous" , "NORMAL" }, // NORMAL is more performant than FULL (the default), but less safe, as it no longer blocks until the disk sync syscall to the OS has completed.
{ "temp_store" , "MEMORY" }, // Use memory for temporary storage to improve performance.
{ "journal_mode" , "WAL" }, // Use Write-Ahead Logging for better concurrency and performance.
{ "cache_size" , "-65536" }, // Set cache size to 64 MB (negative value means in KB, so -64000 = 64 MB). Default is 2000 pages, which is 2 MB (2000 * 1024 bytes).
{ "mmap_size" , "67108864" }, // 64 MB.
{ "threads" , "8" }, // Use 8 threads for parallel processing where applicable.
{ "shared_cache" , "true" } //
};
// Override the default options with any custom options set in the environment variable.
2025-07-10 15:22:28 +02:00
var customOptionsEnv = Environment . GetEnvironmentVariable ( "CUSTOMSQLITEOPTIONS_DUPLICATI" ) ?? string . Empty ;
2025-06-19 11:48:05 +02:00
if (! string . IsNullOrWhiteSpace ( customOptionsEnv ))
{
2025-07-04 16:29:30 +02:00
foreach ( var opt in customOptionsEnv . Split ( new char [] { ';' }, StringSplitOptions . RemoveEmptyEntries ))
2025-06-19 11:48:05 +02:00
{
var parts = opt . Split ( new [] { '=' }, 2 );
if ( parts . Length == 2 )
customOptions [ parts [ 0 ]. Trim ()] = parts [ 1 ]. Trim ();
}
}
2025-03-20 10:05:10 +01:00
using ( var cmd = con . CreateCommand ())
2025-05-12 10:54:17 +02:00
{
2025-06-19 11:48:05 +02:00
foreach ( var ( key , value ) in customOptions )
2025-03-20 10:05:10 +01:00
{
2025-06-19 11:48:05 +02:00
var opt = $"{key}={value}" ;
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>
/// <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-06-19 11:41:39 +02:00
public static Microsoft . Data . Sqlite . SqliteConnection LoadConnection ( string targetpath )
2025-05-14 10:44:24 +02:00
{
2025-06-19 11:41:39 +02:00
return LoadConnectionAsync ( targetpath ). Await ();
2025-05-14 10:44:24 +02:00
}
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-06-18 08:26:20 +02:00
/// <returns>A task that when waited returns the SQLite connection instance.</returns>
2025-06-19 11:41:39 +02:00
public static async Task < Microsoft . Data . Sqlite . SqliteConnection > LoadConnectionAsync ( string targetpath )
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-19 11:41:39 +02:00
return await ApplyCustomPragmasAsync ( con )
2025-06-12 08:34:29 +02:00
. 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
}
}