Files
duplicati/Duplicati/Library/SQLiteHelper/SQLiteLoader.cs
T

279 lines
13 KiB
C#
Raw Normal View History

2025-04-03 16:38:51 +02:00
// Copyright (C) 2025, The Duplicati Team
// 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
// 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
// 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
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
2024-04-24 11:16:11 +02:00
#nullable enable
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;
using Duplicati.Library.Utility;
2020-02-17 09:34:39 -08:00
namespace Duplicati.Library.SQLiteHelper
{
public static class SQLiteLoader
{
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>
/// 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-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>
/// <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>
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");
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
await OpenSQLiteFileAsync(con, databasePath)
.ConfigureAwait(false);
await TestSQLiteFileAsync(con).ConfigureAwait(false);
2018-06-21 18:08:52 +02:00
}
catch
{
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
}
public static Microsoft.Data.Sqlite.SqliteConnection LoadConnection()
{
return LoadConnectionAsync().Await();
}
/// <summary>
2018-06-11 18:50:11 +02:00
/// Loads an SQLite connection instance and opening the database
/// </summary>
/// <returns>The SQLite connection instance.</returns>
public static async Task<Microsoft.Data.Sqlite.SqliteConnection> LoadConnectionAsync()
{
Microsoft.Data.Sqlite.SqliteConnection? con = null;
SetEnvironmentVariablesForSQLiteTempDir();
2018-06-14 22:00:59 +02:00
try
{
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.");
await DisposeConnectionAsync(con).ConfigureAwait(false);
2018-06-11 18:50:11 +02:00
throw;
}
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>
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
//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: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);
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;
}
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>
public static async Task<Microsoft.Data.Sqlite.SqliteConnection> LoadConnectionAsync(string targetpath, long pagecachesize)
2018-06-11 18:50:11 +02:00
{
if (string.IsNullOrWhiteSpace(targetpath))
throw new ArgumentNullException(nameof(targetpath));
var con = await LoadConnectionAsync().ConfigureAwait(false);
2018-06-11 18:50:11 +02:00
try
{
await OpenSQLiteFileAsync(con, targetpath).ConfigureAwait(false);
}
2018-06-11 19:10:14 +02:00
catch (Exception ex)
{
2018-06-11 19:10:14 +02:00
Logging.Log.WriteErrorMessage(LOGTAG, "FailedToLoadConnectionSQLite", ex, @"Failed to load connection with path '{0}'.", targetpath);
await DisposeConnectionAsync(con).ConfigureAwait(false);
throw;
}
2023-03-19 00:48:10 +01:00
// set custom Sqlite options
return await ApplyCustomPragmasAsync(con, pagecachesize)
.ConfigureAwait(false);
}
/// <summary>
/// Returns the SQLiteCommand type for the current architecture
/// </summary>
public static Type SQLiteConnectionType
{
get
{
return typeof(Microsoft.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
{
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;
}
}
/// <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);
}
/// <summary>
/// Wrapper to dispose the SQLite connection
/// </summary>
/// <param name="con">The connection to close.</param>
private static async Task DisposeConnectionAsync(Microsoft.Data.Sqlite.SqliteConnection? con)
{
if (con != null)
try { await con.DisposeAsync().ConfigureAwait(false); }
catch (Exception ex) { Logging.Log.WriteExplicitMessage(LOGTAG, "ConnectionDisposeError", ex, "Failed to dispose connection"); }
}
/// <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>
private static async Task OpenSQLiteFileAsync(Microsoft.Data.Sqlite.SqliteConnection con, string path)
{
con.ConnectionString = $"Data Source={path};Pooling=false";
await con.OpenAsync().ConfigureAwait(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); }
}
/// <summary>
/// Tests the SQLite connection, throwing an exception if the connection does not work
/// </summary>
/// <param name="con">The connection to test.</param>
private static async Task TestSQLiteFileAsync(Microsoft.Data.Sqlite.SqliteConnection con)
{
// 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";
await cmd.ExecuteScalarAsync().ConfigureAwait(false);
}
}
}