From 736b52c2fbef0054462cace3d397f4bcbbdc65d3 Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Thu, 19 Jun 2025 07:00:14 +0200 Subject: [PATCH] Made the LocalPurgeDatabase methods accept a cancellation token --- .../Main/Database/LocalPurgeDatabase.cs | 110 +++++++++++------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/Duplicati/Library/Main/Database/LocalPurgeDatabase.cs b/Duplicati/Library/Main/Database/LocalPurgeDatabase.cs index 50cdb25e3..8434c8842 100644 --- a/Duplicati/Library/Main/Database/LocalPurgeDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalPurgeDatabase.cs @@ -23,6 +23,8 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Duplicati.Library.Utility; using Microsoft.Data.Sqlite; @@ -40,13 +42,14 @@ namespace Duplicati.Library.Main.Database /// The path to the database file. /// The size of the page cache. /// The optional existing database instance to use. Used to mimic constructor chaining. + /// A cancellation token to cancel the operation. /// A task that when awaited contains a new instance of . - public static async Task CreateAsync(string path, long pagecachesize, LocalPurgeDatabase? dbnew = null) + public static async Task CreateAsync(string path, long pagecachesize, LocalPurgeDatabase? dbnew, CancellationToken token) { dbnew ??= new LocalPurgeDatabase(); dbnew = (LocalPurgeDatabase) - await LocalDeleteDatabase.CreateAsync(path, "Purge", pagecachesize, dbnew) + await LocalDeleteDatabase.CreateAsync(path, "Purge", pagecachesize, dbnew, token) .ConfigureAwait(false); dbnew.ShouldCloseConnection = true; @@ -59,13 +62,14 @@ namespace Duplicati.Library.Main.Database /// /// The parent database to use. /// An optional existing database instance to use. Used to mimic constructor chaining. + /// A cancellation token to cancel the operation. /// A task that when awaited contains a new instance of . - public static async Task CreateAsync(LocalDatabase dbparent, LocalPurgeDatabase? dbnew = null) + public static async Task CreateAsync(LocalDatabase dbparent, LocalPurgeDatabase? dbnew, CancellationToken token) { dbnew ??= new LocalPurgeDatabase(); dbnew = (LocalPurgeDatabase) - await LocalDeleteDatabase.CreateAsync(dbparent, dbnew) + await LocalDeleteDatabase.CreateAsync(dbparent, dbnew, token) .ConfigureAwait(false); return dbnew; @@ -75,10 +79,11 @@ namespace Duplicati.Library.Main.Database /// Creates a new instance of the class. /// /// The ID of the parent fileset. + /// A cancellation token to cancel the operation. /// A task that when awaited contains a new instance of . - public async Task CreateTemporaryFileset(long parentid) + public async Task CreateTemporaryFileset(long parentid, CancellationToken token) { - return await TemporaryFileset.CreateAsync(parentid, this) + return await TemporaryFileset.CreateAsync(parentid, this, token) .ConfigureAwait(false); } @@ -86,8 +91,9 @@ namespace Duplicati.Library.Main.Database /// Gets the remote volume name for a fileset with the specified ID. /// /// The ID of the fileset. + /// A cancellation token to cancel the operation. /// A task that when awaited contains the name of the remote volume. - public async Task GetRemoteVolumeNameForFileset(long id) + public async Task GetRemoteVolumeNameForFileset(long id, CancellationToken token) { await using var cmd = m_connection.CreateCommand(@" SELECT @@ -101,8 +107,8 @@ namespace Duplicati.Library.Main.Database ") .SetParameterValue("@FilesetId", id); - await using var rd = await cmd.ExecuteReaderAsync().ConfigureAwait(false); - if (!await rd.ReadAsync().ConfigureAwait(false)) + await using var rd = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false); + if (!await rd.ReadAsync(token).ConfigureAwait(false)) throw new Exception($"No remote volume found for fileset with id {id}"); else return rd.ConvertValueToString(0) ?? throw new Exception($"Remote volume name for fileset with id {id} is null"); @@ -111,8 +117,9 @@ namespace Duplicati.Library.Main.Database /// /// Counts the number of orphan files in the database. /// + /// A cancellation token to cancel the operation. /// A task that when awaited contains the count of orphan files. - internal async Task CountOrphanFiles() + internal async Task CountOrphanFiles(CancellationToken token) { await using var cmd = m_connection.CreateCommand(m_rtr); cmd.SetCommandAndParameters(@" @@ -123,8 +130,8 @@ namespace Duplicati.Library.Main.Database FROM ""FilesetEntry"" ) "); - await using var rd = await cmd.ExecuteReaderAsync().ConfigureAwait(false); - if (await rd.ReadAsync().ConfigureAwait(false)) + await using var rd = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false); + if (await rd.ReadAsync(token).ConfigureAwait(false)) return rd.ConvertValueToInt64(0, 0); else return 0; @@ -156,27 +163,31 @@ namespace Duplicati.Library.Main.Database /// Applies a filter to the temporary fileset. /// /// The filter to apply. + /// A cancellation token to cancel the operation. /// A task that completes when the filter has been applied. - Task ApplyFilter(Library.Utility.IFilter filter); + Task ApplyFilter(Library.Utility.IFilter filter, CancellationToken token); /// /// Applies a filter using a custom command. /// /// The command to execute for filtering. + /// A cancellation token to cancel the operation. /// A task that completes when the filter has been applied. - Task ApplyFilter(Func> filtercommand); + Task ApplyFilter(Func> filtercommand, CancellationToken token); /// /// Converts the temporary fileset to a permanent fileset. /// /// The name of the new fileset. /// The timestamp for the new fileset. /// Indicates if this is a full backup. + /// A cancellation token to cancel the operation. /// A task that when awaited contains a tuple with the remote volume ID and the new fileset ID. - Task> ConvertToPermanentFileset(string name, DateTime timestamp, bool isFullBackup); + Task> ConvertToPermanentFileset(string name, DateTime timestamp, bool isFullBackup, CancellationToken token); /// /// Lists all deleted files in the temporary fileset. /// + /// A cancellation token to cancel the operation. /// An asynchronous enumerable of key-value pairs where the key is the file path and the value is the file size. - IAsyncEnumerable> ListAllDeletedFiles(); + IAsyncEnumerable> ListAllDeletedFiles(CancellationToken token); } /// @@ -217,8 +228,9 @@ namespace Duplicati.Library.Main.Database /// /// The ID of the parent fileset. /// The database instance to use. + /// A cancellation token to cancel the operation. /// A task that when awaited contains a new instance of . - public static async Task CreateAsync(long parentid, LocalDatabase db) + public static async Task CreateAsync(long parentid, LocalDatabase db, CancellationToken token) { var tempf = new TemporaryFileset() { @@ -232,7 +244,7 @@ namespace Duplicati.Library.Main.Database CREATE TEMPORARY TABLE ""{tempf.m_tablename}"" ( ""FileID"" INTEGER PRIMARY KEY ) - ") + ", token) .ConfigureAwait(false); return tempf; @@ -242,23 +254,25 @@ namespace Duplicati.Library.Main.Database /// Applies a filter to the temporary fileset using a custom command. /// /// The command to execute for filtering. + /// A cancellation token to cancel the operation. /// A task that completes when the filter has been applied. - public async Task ApplyFilter(Func> filtercommand) + public async Task ApplyFilter(Func> filtercommand, CancellationToken token) { int updated; await using (var cmd = m_db.Connection.CreateCommand()) updated = await filtercommand(cmd, ParentID, m_tablename) .ConfigureAwait(false); - await PostFilterChecks(updated).ConfigureAwait(false); + await PostFilterChecks(updated, token).ConfigureAwait(false); } /// /// Applies a filter to the temporary fileset. /// /// The filter to apply. + /// A cancellation token to cancel the operation. /// A task that completes when the filter has been applied. - public async Task ApplyFilter(Library.Utility.IFilter filter) + public async Task ApplyFilter(Library.Utility.IFilter filter, CancellationToken token) { if (Library.Utility.Utility.IsFSCaseSensitive && filter is FilterExpression expression && expression.Type == Duplicati.Library.Utility.FilterType.Simple) { @@ -272,19 +286,19 @@ namespace Duplicati.Library.Main.Database CREATE TEMPORARY TABLE ""{filenamestable}"" ( ""Path"" TEXT NOT NULL ) - ") + ", token) .ConfigureAwait(false); await cmd.SetCommandAndParameters($@" INSERT INTO ""{filenamestable}"" (""Path"") VALUES (@Path) ") - .PrepareAsync() + .PrepareAsync(token) .ConfigureAwait(false); foreach (var s in p) await cmd.SetParameterValue("@Path", s) - .ExecuteNonQueryAsync() + .ExecuteNonQueryAsync(token) .ConfigureAwait(false); await cmd.SetCommandAndParameters($@" @@ -299,11 +313,11 @@ namespace Duplicati.Library.Main.Database AND ""B"".""Path"" IN ""{filenamestable}"" ") .SetParameterValue("@FilesetId", ParentID) - .ExecuteNonQueryAsync() + .ExecuteNonQueryAsync(token) .ConfigureAwait(false); await cmd - .ExecuteNonQueryAsync($@"DROP TABLE IF EXISTS ""{filenamestable}"" ") + .ExecuteNonQueryAsync($@"DROP TABLE IF EXISTS ""{filenamestable}"" ", token) .ConfigureAwait(false); } else @@ -316,7 +330,7 @@ namespace Duplicati.Library.Main.Database INSERT INTO ""{m_tablename}"" (""FileID"") VALUES (@FileId) ") - .PrepareAsync() + .PrepareAsync(token) .ConfigureAwait(false); cmd.SetCommandAndParameters(@" @@ -332,8 +346,8 @@ namespace Duplicati.Library.Main.Database ") .SetParameterValue("@FilesetId", ParentID); - await using var rd = await cmd.ExecuteReaderAsync().ConfigureAwait(false); - while (await rd.ReadAsync().ConfigureAwait(false)) + await using var rd = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false); + while (await rd.ReadAsync(token).ConfigureAwait(false)) { rd.GetValues(values); var path = values[0] as string; @@ -341,21 +355,22 @@ namespace Duplicati.Library.Main.Database { await cmd2 .SetParameterValue("@FileId", values[1]) - .ExecuteNonQueryAsync() + .ExecuteNonQueryAsync(token) .ConfigureAwait(false); } } } - await PostFilterChecks(0).ConfigureAwait(false); + await PostFilterChecks(0, token).ConfigureAwait(false); } /// /// Applies the filter checks after filtering. /// /// The number of files updated by the filter. + /// A cancellation token to cancel the operation. /// A task that completes when the checks have been applied. - private async Task PostFilterChecks(int updated) + private async Task PostFilterChecks(int updated, CancellationToken token) { await using var cmd = m_db.Connection.CreateCommand(); UpdatedFileCount = updated; @@ -363,7 +378,7 @@ namespace Duplicati.Library.Main.Database RemovedFileCount = await cmd.ExecuteScalarInt64Async($@" SELECT COUNT(*) FROM ""{m_tablename}"" - ", 0) + ", 0, token) .ConfigureAwait(false); RemovedFileSize = await cmd.ExecuteScalarInt64Async($@" @@ -382,7 +397,7 @@ namespace Duplicati.Library.Main.Database AND ""D"".""BlocksetID"" = ""C"".""ID"" ) ) - ", 0) + ", 0, token) .ConfigureAwait(false); var filesetcount = await cmd.SetCommandAndParameters($@" @@ -391,23 +406,27 @@ namespace Duplicati.Library.Main.Database WHERE ""FilesetID"" = @ParentID ") .SetParameterValue("@ParentID", ParentID) - .ExecuteScalarInt64Async(0) + .ExecuteScalarInt64Async(0, token) .ConfigureAwait(false); if (filesetcount == RemovedFileCount) throw new Interface.UserInformationException($"Refusing to purge {RemovedFileCount} files from fileset with ID {ParentID}, as that would remove the entire fileset.\nTo delete a fileset, use the \"delete\" command.", "PurgeWouldRemoveEntireFileset"); } - public async Task> ConvertToPermanentFileset(string name, DateTime timestamp, bool isFullBackup) + /// + public async Task> ConvertToPermanentFileset(string name, DateTime timestamp, bool isFullBackup, CancellationToken token) { var remotevolid = - await m_db.RegisterRemoteVolume(name, RemoteVolumeType.Files, RemoteVolumeState.Temporary) + await m_db + .RegisterRemoteVolume(name, RemoteVolumeType.Files, RemoteVolumeState.Temporary, token) .ConfigureAwait(false); - var filesetid = await m_db.CreateFileset(remotevolid, timestamp) + var filesetid = await m_db + .CreateFileset(remotevolid, timestamp, token) .ConfigureAwait(false); - await m_db.UpdateFullBackupStateInFileset(filesetid, isFullBackup) + await m_db + .UpdateFullBackupStateInFileset(filesetid, isFullBackup, token) .ConfigureAwait(false); await using (var cmd = m_db.Connection.CreateCommand(m_db.Transaction)) @@ -428,13 +447,14 @@ namespace Duplicati.Library.Main.Database ") .SetParameterValue("@TargetFilesetId", filesetid) .SetParameterValue("@SourceFilesetId", ParentID) - .ExecuteNonQueryAsync() + .ExecuteNonQueryAsync(token) .ConfigureAwait(false); return new Tuple(remotevolid, filesetid); } - public async IAsyncEnumerable> ListAllDeletedFiles() + /// + public async IAsyncEnumerable> ListAllDeletedFiles([EnumeratorCancellation] CancellationToken token) { await using var cmd = m_db.Connection.CreateCommand(); await using var rd = await cmd.ExecuteReaderAsync($@" @@ -448,8 +468,10 @@ namespace Duplicati.Library.Main.Database WHERE ""A"".""FileID"" = ""B"".""ID"" AND ""B"".""BlocksetID"" = ""C"".""ID"" - ").ConfigureAwait(false); - while (await rd.ReadAsync().ConfigureAwait(false)) + ", token) + .ConfigureAwait(false); + + while (await rd.ReadAsync(token).ConfigureAwait(false)) yield return new KeyValuePair( rd.ConvertValueToString(0) ?? "", rd.ConvertValueToInt64(1) @@ -467,7 +489,7 @@ namespace Duplicati.Library.Main.Database { await using var cmd = m_db.Connection.CreateCommand(); await cmd - .ExecuteNonQueryAsync($@"DROP TABLE IF EXISTS ""{m_tablename}""") + .ExecuteNonQueryAsync($@"DROP TABLE IF EXISTS ""{m_tablename}""", default) .ConfigureAwait(false); } catch { }