2025-01-07 09:40:39 +01:00
// Copyright (C) 2025, The Duplicati Team
2024-02-28 15:45:30 +01:00
// https://duplicati.com, hello@duplicati.com
2025-05-19 13:19:24 +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
2024-02-28 15:45:30 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-19 13:19:24 +02:00
//
// The above copyright notice and this permission notice shall be included in
2024-02-28 15:45:30 +01:00
// all copies or substantial portions of the Software.
2025-05-19 13:19:24 +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-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2025-05-19 21:59:44 +02:00
2025-04-03 11:31:36 +02:00
#nullable enable
2019-01-25 23:37:57 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2025-06-18 16:00:20 +02:00
using System.Runtime.CompilerServices ;
using System.Threading ;
2025-05-19 13:19:24 +02:00
using System.Threading.Tasks ;
2025-05-19 15:43:55 +02:00
using Duplicati.Library.Utility ;
2025-05-19 13:19:24 +02:00
using Microsoft.Data.Sqlite ;
2019-01-25 23:37:57 +01:00
namespace Duplicati.Library.Main.Database
{
2025-06-17 09:59:22 +02:00
/// <summary>
/// A local backup database that stores blocks, files, and metadata for backup operations.
/// This database is used to track the state of backups and to allow for efficient retrieval of blocks and files.
/// It supports operations such as finding blocks by hash and size, inserting new blocks, files, and blocksets,
/// and managing metadata datasets.
/// </summary>
2019-01-25 23:37:57 +01:00
internal class LocalBackupDatabase : LocalDatabase
{
/// <summary>
2025-06-17 09:59:22 +02:00
/// The tag used for logging.
2019-01-25 23:37:57 +01:00
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < LocalBackupDatabase >();
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to find a block by its hash and size.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_findblockCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to find a blockset by its full hash and length.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_findblocksetCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to find a metadataset by its block hash and size.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_findfilesetCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to find a metadataset by its block hash and size.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_findmetadatasetCommand = null !;
2019-01-25 23:37:57 +01:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to insert a block into the database.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_insertblockCommand = null !;
2019-01-25 23:37:57 +01:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to insert a file into the database.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_insertfileCommand = null !;
2019-01-25 23:37:57 +01:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to insert a blockset into the database.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_insertblocksetCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to insert a blockset entry into the database.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_insertblocksetentryCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to insert a blocklist hash into the database.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_insertblocklistHashesCommand = null !;
2019-01-25 23:37:57 +01:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to insert a metadataset into the database.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_insertmetadatasetCommand = null !;
2019-01-25 23:37:57 +01:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to find a file in the database.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_findfileCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to select the last modified time of a file.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_selectfilelastmodifiedCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to select the last modified time and size of a file.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_selectfilelastmodifiedWithSizeCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to select the hash and size of a file's metadata.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_selectfileHashCommand = null !;
2019-01-25 23:37:57 +01:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to insert a file operation into the database.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_insertfileOperationCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to select the metadata hash and size of a file.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_selectfilemetadatahashandsizeCommand = null !;
2025-06-17 09:59:22 +02:00
/// <summary>
/// The command used to find the first fileset with a block in a blockset.
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand m_getfirstfilesetwithblockinblockset = null !;
2019-01-25 23:37:57 +01:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// HashSet of blocklist hashes to track whether a blocklist hash has been seen before.
/// </summary>
2025-05-19 15:43:55 +02:00
private HashSet < string > m_blocklistHashes = [];
2019-08-05 20:14:05 -04:00
2025-04-06 08:41:59 +02:00
/// <summary>
/// The temporary table with deleted blocks that can be re-used; null if not table is used
/// </summary>
private string? m_tempDeletedBlockTable ;
/// <summary>
/// The in-mmeory lookup for deleted blocks; null if in-memory lookup is not used
/// </summary>
private Dictionary < string , Dictionary < long , long >>? m_deletedBlockLookup ;
/// <summary>
/// The command used to move deleted blocks to the main block table; null if not used
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand ? m_moveblockfromdeletedCommand ;
2025-04-06 08:41:59 +02:00
/// <summary>
/// The command used to find blocks in the deleted blocks table; null if not used
/// </summary>
2025-05-19 13:19:24 +02:00
private SqliteCommand ? m_findindeletedCommand ;
2025-04-06 08:41:59 +02:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// The ID of the fileset currently being processed.
/// </summary>
2019-01-25 23:37:57 +01:00
private long m_filesetId ;
2025-06-17 09:59:22 +02:00
/// <summary>
/// Indicates whether the database should log queries for profiling purposes.
/// </summary>
2025-05-19 13:19:24 +02:00
private bool m_logQueries ;
2019-01-25 23:37:57 +01:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// Initializes a new instance of the <see cref="LocalBackupDatabase"/> class.
/// This constructor is private to enforce the use of the static and asynchronous CreateAsync methods for instantiation.
/// </summary>
/// <param name="path">The path to the database file.</param>
/// <param name="options">The Duplicati options used by the current operation.</param>
/// <param name="dbnew">An optional existing instance of <see cref="LocalBackupDatabase"/> to reuse. Used when derived classes need to call the base constructor.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to monitor for cancellation requests.</param>
2025-06-17 09:59:22 +02:00
/// <returns>A task that when awaited contains a new instance of <see cref="LocalBackupDatabase"/>.</returns>
2025-06-18 16:00:20 +02:00
public static async Task < LocalBackupDatabase > CreateAsync ( string path , Options options , CancellationToken token , LocalBackupDatabase ? dbnew = null )
2019-01-25 23:37:57 +01:00
{
2025-05-21 07:24:36 +02:00
dbnew ??= new LocalBackupDatabase ();
2025-05-19 13:19:24 +02:00
2025-06-12 08:34:29 +02:00
dbnew = ( LocalBackupDatabase )
2025-06-19 11:41:39 +02:00
await CreateLocalDatabaseAsync ( path , "Backup" , false , dbnew , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-06-18 16:00:20 +02:00
dbnew = await CreateAsync ( dbnew , options , null , token ). ConfigureAwait ( false );
2025-05-21 07:24:36 +02:00
dbnew . ShouldCloseConnection = true ;
2025-05-19 13:19:24 +02:00
2025-05-26 09:06:42 +02:00
return dbnew ;
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
2025-06-17 09:59:22 +02:00
/// <summary>
/// Creates a new instance of <see cref="LocalBackupDatabase"/> using an existing parent database.
/// This method is used to create a new backup database based on an existing local database.
/// </summary>
/// <param name="dbparent">The parent local database from which to create the new backup database.</param>
/// <param name="options">The Duplicati options used by the current operation.</param>
/// <param name="dbnew">An optional existing instance of <see cref="LocalBackupDatabase"/> to reuse. Used when derived classes need to call the base constructor.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to monitor for cancellation requests.</param>
2025-06-17 09:59:22 +02:00
/// <returns>A task that when awaited contains a new instance of <see cref="LocalBackupDatabase"/>.</returns>
2025-06-18 16:00:20 +02:00
public static async Task < LocalBackupDatabase > CreateAsync ( LocalDatabase dbparent , Options options , LocalBackupDatabase ? dbnew , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-21 07:24:36 +02:00
dbnew ??= new LocalBackupDatabase ();
2025-05-19 13:19:24 +02:00
2025-06-18 16:00:20 +02:00
dbnew = ( LocalBackupDatabase ) await CreateLocalDatabaseAsync ( dbparent , dbnew , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 13:19:24 +02:00
dbnew . m_logQueries = options . ProfileAllDatabaseQueries ;
2025-05-19 21:59:44 +02:00
dbnew . m_findblockCommand = await dbnew . Connection . CreateCommandAsync ( @"
SELECT ""ID""
FROM ""Block""
WHERE
2025-05-22 13:34:29 +02:00
""Hash"" = @Hash
2025-05-19 21:59:44 +02:00
AND ""Size"" = @Size
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_findblocksetCommand = await dbnew . Connection . CreateCommandAsync ( @"
SELECT ""ID""
FROM ""Blockset""
WHERE
""Fullhash"" = @Fullhash
AND ""Length"" = @Length
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_findmetadatasetCommand = await dbnew . Connection . CreateCommandAsync ( @"
SELECT ""A"".""ID""
FROM
2025-06-18 10:42:08 +02:00
""Metadataset"" ""A"",
""BlocksetEntry"" ""B"",
""Block"" ""C""
2025-05-19 21:59:44 +02:00
WHERE
""A"".""BlocksetID"" = ""B"".""BlocksetID""
AND ""B"".""BlockID"" = ""C"".""ID""
AND ""C"".""Hash"" = @Hash
AND ""C"".""Size"" = @Size
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_findfilesetCommand = await dbnew . Connection . CreateCommandAsync ( @"
SELECT ""ID""
FROM ""FileLookup""
WHERE
""BlocksetID"" = @BlocksetId
AND ""MetadataID"" = @MetadataId
AND ""Path"" = @Path
AND ""PrefixID"" = @PrefixId
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_insertblockCommand = await dbnew . Connection . CreateCommandAsync ( @"
INSERT INTO ""Block"" (
""Hash"",
""VolumeID"",
""Size""
)
VALUES (
@Hash,
@VolumeId,
@Size
);
SELECT last_insert_rowid();
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_insertfileOperationCommand = await dbnew . Connection . CreateCommandAsync ( @"
INSERT INTO ""FilesetEntry"" (
""FilesetID"",
""FileID"",
""Lastmodified""
)
VALUES (
@FilesetId,
@FileId,
@LastModified
)
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_insertfileCommand = await dbnew . Connection . CreateCommandAsync ( @"
INSERT INTO ""FileLookup"" (
""PrefixID"",
""Path"",
""BlocksetID"",
""MetadataID""
)
VALUES (
@PrefixId,
@Path,
@BlocksetId,
@MetadataId
);
SELECT last_insert_rowid();
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_insertblocksetCommand = await dbnew . Connection . CreateCommandAsync ( @"
INSERT INTO ""Blockset"" (
""Length"",
""FullHash""
)
VALUES (
@Length,
@Fullhash
);
2025-06-18 16:00:20 +02:00
SELECT last_insert_rowid();
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_insertblocksetentryCommand = await dbnew . Connection . CreateCommandAsync ( @"
INSERT INTO ""BlocksetEntry"" (
""BlocksetID"",
""Index"",
""BlockID""
)
SELECT
2025-06-18 10:42:08 +02:00
@BlocksetId AS ""A"",
@Index AS ""B"",
2025-05-19 21:59:44 +02:00
""ID""
FROM ""Block""
WHERE ""Hash"" = @Hash
AND ""Size"" = @Size
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_insertblocklistHashesCommand = await dbnew . Connection . CreateCommandAsync ( @"
INSERT INTO ""BlocklistHash"" (
""BlocksetID"",
""Index"",
""Hash""
)
VALUES (
@BlocksetId,
@Index,
@Hash
)
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_insertmetadatasetCommand = await dbnew . Connection . CreateCommandAsync ( @"
INSERT INTO ""Metadataset"" (""BlocksetID"")
VALUES (@BlocksetId);
SELECT last_insert_rowid();
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_selectfilelastmodifiedCommand = await dbnew . Connection . CreateCommandAsync ( @"
SELECT
""A"".""ID"",
""B"".""LastModified""
FROM (
SELECT ""ID""
FROM ""FileLookup""
WHERE ""PrefixID"" = @PrefixId
AND ""Path"" = @Path
) ""A""
CROSS JOIN ""FilesetEntry"" ""B""
WHERE
""A"".""ID"" = ""B"".""FileID""
AND ""B"".""FilesetID"" = @FilesetId
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_selectfilelastmodifiedWithSizeCommand = await dbnew . Connection . CreateCommandAsync ( @"
SELECT
""C"".""ID"",
""C"".""LastModified"",
""D"".""Length""
FROM
(
SELECT
""A"".""ID"",
""B"".""LastModified"",
""A"".""BlocksetID""
FROM (
SELECT
""ID"",
""BlocksetID""
FROM ""FileLookup""
WHERE
""PrefixID"" = @PrefixId
AND ""Path"" = @Path
) ""A""
CROSS JOIN ""FilesetEntry"" ""B""
WHERE
""A"".""ID"" = ""B"".""FileID""
AND ""B"".""FilesetID"" = @FilesetId
) AS ""C"",
""Blockset"" AS ""D""
WHERE ""C"".""BlocksetID"" == ""D"".""ID""
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 21:59:44 +02:00
dbnew . m_selectfilemetadatahashandsizeCommand = await dbnew . Connection . CreateCommandAsync ( @"
SELECT
""Blockset"".""Length"",
""Blockset"".""FullHash""
FROM
""Blockset"",
""Metadataset"",
""File""
WHERE
""File"".""ID"" = @FileId
AND ""Blockset"".""ID"" = ""Metadataset"".""BlocksetID""
AND ""Metadataset"".""ID"" = ""File"".""MetadataID""
2025-06-18 16:00:20 +02:00
" , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-04-06 08:41:59 +02:00
// Experimental toggling of the deleted block cache
// If the value is less than zero, the lookup is disabled
// meaning that deleted blocks are never reused (same as 2.1.0.5 and earlier)
// A value of zero disables the in-memory cache, always using a temporary table
// Any other value is the size of the in-memory cache
// If the number of deleted blocks exceed the cache size, a temporary table is used
var deletedBlockCacheSize = Environment . GetEnvironmentVariable ( "DUPLICATI_DELETEDBLOCKCACHESIZE" );
if (! long . TryParse ( deletedBlockCacheSize , out var deletedBlockCacheSizeLong ))
deletedBlockCacheSizeLong = 10000 ;
if ( deletedBlockCacheSizeLong >= 0 )
{
2025-06-18 11:53:11 +02:00
await using var cmd = dbnew . Connection . CreateCommand ();
2025-09-05 14:06:55 +02:00
dbnew . m_tempDeletedBlockTable = $"DeletedBlock-{Library.Utility.Utility.GetHexGuid()}" ;
2025-06-18 11:18:25 +02:00
await cmd . SetCommandAndParameters ( $@"
2025-06-18 16:00:20 +02:00
CREATE TEMPORARY TABLE ""{dbnew.m_tempDeletedBlockTable}"" AS
SELECT
MAX(""ID"") AS ""ID"",
""Hash"",
""Size""
FROM ""DeletedBlock""
WHERE ""VolumeID"" IN (
SELECT ""ID""
FROM ""RemoteVolume""
WHERE ""State"" NOT IN (@States)
)
GROUP BY
""Hash"",
""Size""
" )
2025-06-18 11:18:25 +02:00
. ExpandInClauseParameterMssqlite ( "@States" , [
RemoteVolumeState . Deleted ,
2025-09-05 14:07:35 +02:00
RemoteVolumeState . Deleting
2025-06-18 11:18:25 +02:00
])
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-04-06 08:41:59 +02:00
2025-06-18 11:18:25 +02:00
var deletedBlocks = await cmd . ExecuteScalarInt64Async ( @ $"
2025-05-20 06:07:45 +02:00
SELECT COUNT(*)
FROM ""{dbnew.m_tempDeletedBlockTable}""
2025-06-18 16:00:20 +02:00
" , 0 , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-04-06 08:41:59 +02:00
2025-06-18 11:18:25 +02:00
// There are no deleted blocks, so we can drop the table
if ( deletedBlocks == 0 )
{
2025-06-18 16:00:20 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE ""{dbnew.m_tempDeletedBlockTable}""" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
dbnew . m_tempDeletedBlockTable = null ;
2025-04-06 08:41:59 +02:00
2025-06-18 11:18:25 +02:00
}
// The deleted blocks are small enough to fit in memory
else if ( deletedBlocks <= deletedBlockCacheSizeLong )
{
dbnew . m_deletedBlockLookup = new Dictionary < string , Dictionary < long , long >>();
cmd . SetCommandAndParameters ( @ $"
2025-05-20 06:07:45 +02:00
SELECT
""ID"",
""Hash"",
""Size""
FROM ""{dbnew.m_tempDeletedBlockTable}""
2025-06-12 08:34:29 +02:00
" )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 06:07:45 +02:00
2025-06-18 16:00:20 +02:00
await using ( var reader = await cmd . ExecuteReaderAsync ( token ). ConfigureAwait ( false ))
while ( await reader . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
{
var id = reader . ConvertValueToInt64 ( 0 );
var hash = reader . ConvertValueToString ( 1 ) ?? throw new Exception ( "Hash is null" );
var size = reader . ConvertValueToInt64 ( 2 );
2025-04-06 08:41:59 +02:00
2025-06-18 11:18:25 +02:00
if (! dbnew . m_deletedBlockLookup . TryGetValue ( hash , out var sizes ))
dbnew . m_deletedBlockLookup [ hash ] = sizes = new Dictionary < long , long >();
sizes [ size ] = id ;
}
2025-04-06 08:41:59 +02:00
2025-06-18 16:00:20 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE ""{dbnew.m_tempDeletedBlockTable}""" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
dbnew . m_tempDeletedBlockTable = null ;
}
// The deleted blocks are too large to fit in memory, so we use a temporary table
else
{
await cmd . ExecuteNonQueryAsync ( $@"
2025-06-18 16:00:20 +02:00
CREATE UNIQUE INDEX ""unique_{dbnew.m_tempDeletedBlockTable}""
ON ""{dbnew.m_tempDeletedBlockTable}"" (
""Hash"",
""Size""
)
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 06:07:45 +02:00
2025-06-18 11:18:25 +02:00
dbnew . m_findindeletedCommand = await dbnew . Connection . CreateCommandAsync ( $@"
2025-06-18 16:00:20 +02:00
SELECT ""ID""
FROM ""{dbnew.m_tempDeletedBlockTable}""
WHERE
""Hash"" = @Hash
AND ""Size"" = @Size
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-20 06:07:45 +02:00
2025-06-18 11:18:25 +02:00
dbnew . m_moveblockfromdeletedCommand = await dbnew . Connection . CreateCommandAsync ( @ $"
2025-06-18 16:00:20 +02:00
INSERT INTO ""Block"" (
""Hash"",
""Size"",
""VolumeID""
)
SELECT
""Hash"",
""Size"",
""VolumeID""
FROM ""DeletedBlock""
WHERE ""ID"" = @DeletedBlockId LIMIT 1;
2025-05-20 06:07:45 +02:00
2025-06-18 16:00:20 +02:00
DELETE FROM ""DeletedBlock""
WHERE ""ID"" = @DeletedBlockId;
2025-05-20 06:07:45 +02:00
2025-06-18 16:00:20 +02:00
DELETE FROM ""{dbnew.m_tempDeletedBlockTable}""
WHERE ""ID"" = @DeletedBlockId;
2025-05-20 06:07:45 +02:00
2025-06-18 16:00:20 +02:00
SELECT last_insert_rowid()
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-04-06 08:41:59 +02:00
2025-06-18 11:18:25 +02:00
}
2025-04-06 08:41:59 +02:00
2025-06-18 11:18:25 +02:00
if ( deletedBlocks > 0 )
{
dbnew . m_moveblockfromdeletedCommand = await dbnew . m_connection . CreateCommandAsync ( @ $"
2025-06-18 16:00:20 +02:00
INSERT INTO ""Block"" (
""Hash"",
""Size"",
""VolumeID""
)
SELECT
""Hash"",
""Size"",
""VolumeID""
FROM ""DeletedBlock""
WHERE ""ID"" = @DeletedBlockId LIMIT 1;
2025-05-20 06:07:45 +02:00
2025-06-18 16:00:20 +02:00
DELETE FROM ""DeletedBlock""
WHERE ""ID"" = @DeletedBlockId;
2025-05-20 06:07:45 +02:00
2025-06-18 16:00:20 +02:00
SELECT last_insert_rowid()
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-04-06 08:41:59 +02:00
}
}
2019-01-25 23:37:57 +01:00
// Allow users to test on real-world data
// to get feedback on potential performance
int . TryParse ( Environment . GetEnvironmentVariable ( "TEST_QUERY_VERSION" ), out var testqueryversion );
if ( testqueryversion != 0 )
Logging . Log . WriteWarningMessage ( LOGTAG , "TestFileQuery" , null , "Using performance test query version {0} as the TEST_QUERY_VERSION environment variable is set" , testqueryversion );
2025-05-19 13:19:24 +02:00
// The original query (v==1) finds the most recent entry of the file in question,
2019-01-25 23:37:57 +01:00
// but it requires some large joins to extract the required information.
// To speed it up, we use a slightly simpler approach that only looks at the
// previous fileset, and uses information here.
// If there is a case where a file is sometimes there and sometimes not
// (i.e. filter file, remove filter) we will not find the file.
2025-05-19 13:19:24 +02:00
// We currently use this faster version,
2019-01-25 23:37:57 +01:00
// but allow users to switch back via an environment variable
// such that we can get performance feedback
2025-03-18 20:30:27 +01:00
string findQuery ;
2019-01-25 23:37:57 +01:00
switch ( testqueryversion )
{
// The query used in Duplicati until 2.0.3.9
case 1 :
2025-05-20 06:07:45 +02:00
findQuery = @"
SELECT
""FileLookup"".""ID"" AS ""FileID"",
""FilesetEntry"".""Lastmodified"",
""FileBlockset"".""Length"",
""MetaBlockset"".""Fullhash"" AS ""Metahash"",
""MetaBlockset"".""Length"" AS ""Metasize""
FROM
""FileLookup"",
""FilesetEntry"",
""Fileset"",
""Blockset"" ""FileBlockset"",
""Metadataset"",
""Blockset"" ""MetaBlockset""
WHERE
""FileLookup"".""PrefixID"" = @PrefixId
AND ""FileLookup"".""Path"" = @Path
AND ""FilesetEntry"".""FileID"" = ""FileLookup"".""ID""
AND ""Fileset"".""ID"" = ""FilesetEntry"".""FilesetID""
AND ""FileBlockset"".""ID"" = ""FileLookup"".""BlocksetID""
AND ""Metadataset"".""ID"" = ""FileLookup"".""MetadataID""
AND ""MetaBlockset"".""ID"" = ""Metadataset"".""BlocksetID""
AND @FilesetId IS NOT NULL
ORDER BY ""Fileset"".""Timestamp"" DESC
LIMIT 1
" ;
2019-01-25 23:37:57 +01:00
break ;
// The fastest reported query in Duplicati 2.0.3.10, but with "LIMIT 1" added
default :
case 2 :
2025-05-20 06:07:45 +02:00
var getLastFileEntryForPath = @"
SELECT
""A"".""ID"",
""B"".""LastModified"",
""A"".""BlocksetID"",
""A"".""MetadataID""
FROM (
SELECT
""ID"",
""BlocksetID"",
""MetadataID""
FROM ""FileLookup""
WHERE
""PrefixID"" = @PrefixId
AND ""Path"" = @Path
) ""A""
CROSS JOIN ""FilesetEntry"" ""B""
WHERE
""A"".""ID"" = ""B"".""FileID""
AND ""B"".""FilesetID"" = @FilesetId
" ;
findQuery = $@"
SELECT
""C"".""ID"" AS ""FileID"",
""C"".""LastModified"",
""D"".""Length"",
""E"".""FullHash"" as ""Metahash"",
""E"".""Length"" AS ""Metasize""
FROM
({getLastFileEntryForPath}) AS ""C"",
""Blockset"" AS ""D"",
""Blockset"" AS ""E"",
""Metadataset"" ""F""
WHERE
""C"".""BlocksetID"" == ""D"".""ID""
AND ""C"".""MetadataID"" == ""F"".""ID""
AND ""F"".""BlocksetID"" = ""E"".""ID""
LIMIT 1
" ;
2019-01-25 23:37:57 +01:00
break ;
// Potentially faster query: https://forum.duplicati.com/t/release-2-0-3-10-canary-2018-08-30/4497/25
case 3 :
2025-05-20 06:07:45 +02:00
findQuery = @"
SELECT
2025-06-18 10:42:08 +02:00
""FileLookup"".""ID"" as ""FileID"",
""FilesetEntry"".""Lastmodified"",
""FileBlockset"".""Length"",
""MetaBlockset"".""FullHash"" AS ""Metahash"",
""MetaBlockset"".""Length"" as ""Metasize""
FROM ""FilesetEntry""
INNER JOIN ""Fileset""
ON (""FileSet"".""ID"" = ""FilesetEntry"".FilesetID)
INNER JOIN ""FileLookup""
ON (""FileLookup"".""ID"" = ""FilesetEntry"".""FileID"")
INNER JOIN ""Metadataset""
ON (""Metadataset"".""ID"" = ""FileLookup"".""MetadataID"")
INNER JOIN Blockset AS ""MetaBlockset""
ON (""MetaBlockset"".""ID"" = ""Metadataset"".""BlocksetID"")
LEFT JOIN Blockset AS ""FileBlockset""
ON (""FileBlockset"".""ID"" = ""FileLookup"".""BlocksetID"")
2025-05-20 06:07:45 +02:00
WHERE
2025-06-18 10:42:08 +02:00
""FileLookup"".""PrefixID"" = @PrefixId
AND ""FileLookup"".""Path"" = @Path
2025-05-20 06:07:45 +02:00
AND FilesetID = @FilesetId
LIMIT 1
" ;
2019-01-25 23:37:57 +01:00
break ;
// The slow query used in Duplicati 2.0.3.10, but with "LIMIT 1" added
case 4 :
2025-05-20 06:07:45 +02:00
findQuery = @"
SELECT
""FileLookup"".""ID"" AS ""FileID"",
""FilesetEntry"".""Lastmodified"",
""FileBlockset"".""Length"",
""MetaBlockset"".""Fullhash"" AS ""Metahash"",
""MetaBlockset"".""Length"" AS ""Metasize""
FROM
""FileLookup"",
""FilesetEntry"",
""Fileset"",
""Blockset"" ""FileBlockset"",
""Metadataset"",
""Blockset"" ""MetaBlockset""
WHERE
""FileLookup"".""PrefixID"" = @PrefixId
AND ""FileLookup"".""Path"" = @Path
AND ""Fileset"".""ID"" = @FilesetId
AND ""FilesetEntry"".""FileID"" = ""FileLookup"".""ID""
AND ""Fileset"".""ID"" = ""FilesetEntry"".""FilesetID""
AND ""FileBlockset"".""ID"" = ""FileLookup"".""BlocksetID""
AND ""Metadataset"".""ID"" = ""FileLookup"".""MetadataID""
AND ""MetaBlockset"".""ID"" = ""Metadataset"".""BlocksetID""
LIMIT 1
" ;
2019-01-25 23:37:57 +01:00
break ;
}
2025-05-19 13:19:24 +02:00
dbnew . m_findfileCommand = dbnew . m_connection . CreateCommand ( findQuery );
2019-01-25 23:37:57 +01:00
2025-05-20 06:07:45 +02:00
dbnew . m_selectfileHashCommand = dbnew . m_connection . CreateCommand ( @"
SELECT ""Blockset"".""Fullhash""
2025-06-11 11:58:14 +02:00
FROM
""Blockset"",
""FileLookup""
2025-05-20 06:07:45 +02:00
WHERE
""Blockset"".""ID"" = ""FileLookup"".""BlocksetID""
AND ""FileLookup"".""ID"" = @FileId
" );
dbnew . m_getfirstfilesetwithblockinblockset = dbnew . m_connection . CreateCommand ( @"
SELECT MIN(""FilesetEntry"".""FilesetID"")
FROM ""FilesetEntry""
WHERE ""FilesetEntry"".""FileID"" IN (
SELECT ""File"".""ID""
FROM ""File""
WHERE ""File"".""BlocksetID"" IN(
SELECT ""BlocklistHash"".""BlocksetID""
FROM ""BlocklistHash""
WHERE ""BlocklistHash"".""Hash"" = @Hash
)
)
" );
2019-01-25 23:37:57 +01:00
2025-05-19 13:19:24 +02:00
dbnew . m_blocklistHashes = new HashSet < string >();
return dbnew ;
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
2019-01-25 23:37:57 +01:00
/// <summary>
2025-06-17 09:59:22 +02:00
/// Probes to see if a block already exists.
2019-01-25 23:37:57 +01:00
/// </summary>
2025-06-17 09:59:22 +02:00
/// <param name="key">The block key.</param>
/// <param name="size">The size of the block.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to monitor for cancellation requests.</param>
2025-06-17 09:59:22 +02:00
/// <returns>A task that when awaited contains true if the block should be added to the current output.</returns>
2025-06-18 16:00:20 +02:00
public async Task < long > FindBlockID ( string key , long size , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
return await m_findblockCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@Hash" , key )
. SetParameterValue ( "@Size" , size )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( m_logQueries , - 1 , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
/// <summary>
2025-06-17 09:59:22 +02:00
/// Adds a block to the local database, returning a value indicating if the value presents a new block.
2019-08-05 20:14:05 -04:00
/// </summary>
2025-06-17 09:59:22 +02:00
/// <param name="key">The block key.</param>
/// <param name="size">The size of the block.</param>
2025-06-17 15:13:33 +02:00
/// <param name="volumeid">The ID of the volume to which the block belongs.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to monitor for cancellation requests.</param>
2025-06-17 09:59:22 +02:00
/// <returns>A taskt that when awaited contains true if the block should be added to the current output.</returns>
2025-06-18 16:00:20 +02:00
public async Task < bool > AddBlock ( string key , long size , long volumeid , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
var r = await FindBlockID ( key , size , token ). ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
if ( r == - 1L )
{
2025-04-06 08:41:59 +02:00
if ( m_moveblockfromdeletedCommand != null )
2024-11-01 16:27:06 +01:00
{
2025-04-06 08:41:59 +02:00
if ( m_deletedBlockLookup != null )
{
if ( m_deletedBlockLookup . TryGetValue ( key , out var sizes ))
if ( sizes . TryGetValue ( size , out var id ))
{
2025-05-19 15:43:55 +02:00
await m_moveblockfromdeletedCommand
. SetTransaction ( m_rtr )
2025-04-06 08:41:59 +02:00
. SetParameterValue ( "@DeletedBlockId" , id )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-04-06 08:41:59 +02:00
sizes . Remove ( size );
if ( sizes . Count == 0 )
m_deletedBlockLookup . Remove ( key );
return false ;
}
}
else if ( m_findindeletedCommand != null )
{
// No transaction on the temporary table
2025-05-19 15:43:55 +02:00
var id = await m_findindeletedCommand
2025-05-28 05:53:44 +02:00
. SetTransaction ( m_rtr )
2025-04-06 08:41:59 +02:00
. SetParameterValue ( "@Hash" , key )
. SetParameterValue ( "@Size" , size )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( m_logQueries , - 1 , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2024-11-01 16:27:06 +01:00
2025-04-06 08:41:59 +02:00
if ( id != - 1 )
{
2025-05-19 15:43:55 +02:00
var c = await m_moveblockfromdeletedCommand
. SetTransaction ( m_rtr )
2025-04-06 08:41:59 +02:00
. SetParameterValue ( "@DeletedBlockId" , id )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2024-11-01 16:27:06 +01:00
2025-04-06 08:41:59 +02:00
if ( c != 2 )
throw new Exception ( $"Failed to move block {key} with size {size}, result count: {c}" );
// We do not clean up the temporary table, as the regular block lookup should now find it
return false ;
}
}
2024-11-01 16:27:06 +01:00
}
2025-05-19 15:43:55 +02:00
var ins = await m_insertblockCommand
. SetTransaction ( m_rtr )
2025-04-06 08:41:59 +02:00
. SetParameterValue ( "@Hash" , key )
. SetParameterValue ( "@VolumeId" , volumeid )
. SetParameterValue ( "@Size" , size )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-04-06 08:41:59 +02:00
if ( ins != 1 )
throw new Exception ( $"Failed to insert block {key} with size {size}, result count: {ins}" );
2019-01-25 23:37:57 +01:00
return true ;
}
else
{
//Update lookup cache if required
return false ;
}
}
/// <summary>
2025-06-17 09:59:22 +02:00
/// Adds a blockset to the database, returns a value indicating if the blockset is new.
2019-01-25 23:37:57 +01:00
/// </summary>
2025-06-17 09:59:22 +02:00
/// <param name="filehash">The hash of the blockset.</param>
/// <param name="size">The size of the blockset.</param>
/// <param name="blocksize">The size of the blocks in the blockset.</param>
/// <param name="hashes">The list of hashes.</param>
2025-06-17 15:13:33 +02:00
/// <param name="blocklistHashes">The list of hashes for the blocklist, or null if no blocklist is used.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token"> The cancellation token to monitor for cancellation requests.</param>
2025-06-17 09:59:22 +02:00
/// <returns>A task that when awaited contains a tuple with the first value indicating whether the blockset was created, and the second value being the blockset ID.</returns>
2025-06-18 16:00:20 +02:00
public async Task <( bool , long )> AddBlockset ( string filehash , long size , int blocksize , IEnumerable < string > hashes , IEnumerable < string > blocklistHashes , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
long blocksetid = await m_findblocksetCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@Fullhash" , filehash )
. SetParameterValue ( "@Length" , size )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( m_logQueries , - 1 , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2019-01-25 23:37:57 +01:00
if ( blocksetid != - 1 )
2025-05-19 15:43:55 +02:00
return ( false , blocksetid ); //Found it
2019-01-25 23:37:57 +01:00
2025-05-19 15:43:55 +02:00
blocksetid = await m_insertblocksetCommand
. SetTransaction ( m_rtr )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@Length" , size )
. SetParameterValue ( "@Fullhash" , filehash )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 14:40:00 +02:00
long ix = 0 ;
if ( blocklistHashes != null )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
m_insertblocklistHashesCommand
. SetTransaction ( m_rtr )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@BlocksetId" , blocksetid );
2019-01-25 23:37:57 +01:00
2025-05-19 14:40:00 +02:00
foreach ( var bh in blocklistHashes )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
var c = await m_insertblocklistHashesCommand
. SetParameterValue ( "@Index" , ix )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@Hash" , bh )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 14:40:00 +02:00
if ( c != 1 )
throw new Exception ( $"Failed to insert blocklist hash {bh} for blockset {blocksetid}, result count: {c}" );
2025-06-12 08:34:29 +02:00
2025-05-19 14:40:00 +02:00
ix ++;
2019-01-25 23:37:57 +01:00
}
2025-05-19 14:40:00 +02:00
}
2019-01-25 23:37:57 +01:00
2025-05-19 15:43:55 +02:00
m_insertblocksetentryCommand
. SetTransaction ( m_rtr )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@BlocksetId" , blocksetid );
2019-01-25 23:37:57 +01:00
2025-05-19 14:40:00 +02:00
ix = 0 ;
long remainsize = size ;
foreach ( var h in hashes )
{
var exsize = remainsize < blocksize ? remainsize : blocksize ;
2025-05-19 15:43:55 +02:00
var c = await m_insertblocksetentryCommand
. SetParameterValue ( "@Index" , ix )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@Hash" , h )
. SetParameterValue ( "@Size" , exsize )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 14:40:00 +02:00
if ( c != 1 )
2019-01-25 23:37:57 +01:00
{
2025-05-19 14:40:00 +02:00
Logging . Log . WriteErrorMessage ( LOGTAG , "CheckingErrorsForIssue1400" , null , "Checking errors, related to #1400. Unexpected result count: {0}, expected {1}, hash: {2}, size: {3}, blocksetid: {4}, ix: {5}, fullhash: {6}, fullsize: {7}" , c , 1 , h , exsize , blocksetid , ix , filehash , size );
2025-06-18 11:53:11 +02:00
await using ( var cmd = m_connection . CreateCommand ( m_rtr ))
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
var bid = await cmd . SetCommandAndParameters ( @"
SELECT ""ID""
FROM ""Block""
WHERE ""Hash"" = @Hash
" )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@Hash" , h )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async (- 1 , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2025-05-19 14:40:00 +02:00
if ( bid == - 1 )
throw new Exception ( $"Could not find any blocks with the given hash: {h}" );
2025-05-19 15:43:55 +02:00
cmd . SetCommandAndParameters ( @"
SELECT ""Size""
FROM ""Block""
WHERE ""Hash"" = @Hash
" )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@Hash" , h );
2025-05-19 15:43:55 +02:00
2025-06-18 16:00:20 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ( token ). ConfigureAwait ( false ))
2025-05-19 14:40:00 +02:00
Logging . Log . WriteErrorMessage ( LOGTAG , "FoundIssue1400Error" , null , "Found block with ID {0} and hash {1} and size {2}" , bid , h , rd . ConvertValueToInt64 ( 0 , - 1 ));
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
2025-05-19 14:40:00 +02:00
throw new Exception ( $"Unexpected result count: {c}, expected {1}, check log for more messages" );
2019-01-25 23:37:57 +01:00
}
2025-05-19 14:40:00 +02:00
ix ++;
remainsize -= blocksize ;
2019-01-25 23:37:57 +01:00
}
2025-06-18 16:00:20 +02:00
await m_rtr . CommitAsync ( token : token ). ConfigureAwait ( false );
2025-05-19 14:40:00 +02:00
2025-05-19 15:43:55 +02:00
return ( true , blocksetid );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-06-17 09:59:22 +02:00
/// Gets the metadataset ID from the filehash.
2019-01-25 23:37:57 +01:00
/// </summary>
/// <param name="filehash">The metadata hash.</param>
/// <param name="size">The size of the metadata.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token"> The cancellation token to cancel the operation.</param>
2025-06-17 09:59:22 +02:00
/// <returns>A task that when awaited contains a tuple with the first value indicating if the metadataset was found, and the second value being the metadataset ID.</returns>
2025-06-18 16:00:20 +02:00
public async Task <( bool , long )> GetMetadatasetID ( string filehash , long size , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
long metadataid ;
2019-01-25 23:37:57 +01:00
if ( size > 0 )
{
2025-05-19 15:43:55 +02:00
metadataid = await m_findmetadatasetCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@Hash" , filehash )
. SetParameterValue ( "@Size" , size )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( m_logQueries , - 1 , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
return ( metadataid != - 1 , metadataid );
2019-01-25 23:37:57 +01:00
}
metadataid = - 2 ;
2025-05-19 15:43:55 +02:00
return ( false , metadataid );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-06-17 15:13:33 +02:00
/// Adds a metadata set to the database, and returns a tuple indicating if the record was new and the ID of the metadata set.
2019-01-25 23:37:57 +01:00
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="filehash">The metadata hash.</param>
/// <param name="size">The size of the metadata.</param>
/// <param name="blocksetid">The id of the blockset to add.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token"> The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains a tuple with the first value indicating if the metadata set was added, and the second value being the metadata ID.</returns>
2025-06-18 16:00:20 +02:00
public async Task <( bool , long )> AddMetadataset ( string filehash , long size , long blocksetid , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
var ( metadatafound , metadataid ) = await GetMetadatasetID ( filehash , size , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
if ( metadatafound )
return ( false , metadataid );
2019-01-25 23:37:57 +01:00
2025-05-19 15:43:55 +02:00
metadataid = await m_insertmetadatasetCommand
. SetTransaction ( m_rtr )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@BlocksetId" , blocksetid )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 14:40:00 +02:00
2025-06-18 16:00:20 +02:00
await m_rtr . CommitAsync ( token : token ). ConfigureAwait ( false );
2025-05-19 14:40:00 +02:00
2025-05-19 15:43:55 +02:00
return ( true , metadataid );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-06-17 15:13:33 +02:00
/// Adds a file record to the database.
2019-01-25 23:37:57 +01:00
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="pathprefixid">The path prefix ID.</param>
/// <param name="filename">The path to the file.</param>
/// <param name="lastmodified">The time the file was modified.</param>
/// <param name="blocksetID">The ID of the hashkey for the file.</param>
/// <param name="metadataID">The ID for the metadata.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the file is added.</returns>
2025-06-18 16:00:20 +02:00
public async Task AddFile ( long pathprefixid , string filename , DateTime lastmodified , long blocksetID , long metadataID , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
var fileidobj = await m_findfilesetCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@BlocksetId" , blocksetID )
. SetParameterValue ( "@MetadataId" , metadataID )
. SetParameterValue ( "@Path" , filename )
. SetParameterValue ( "@PrefixId" , pathprefixid )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
if ( fileidobj == - 1 )
{
2025-05-19 15:43:55 +02:00
fileidobj = await m_insertfileCommand . SetTransaction ( m_rtr )
2025-05-19 14:40:00 +02:00
. SetParameterValue ( "@PrefixId" , pathprefixid )
. SetParameterValue ( "@Path" , filename )
. SetParameterValue ( "@BlocksetId" , blocksetID )
. SetParameterValue ( "@MetadataId" , metadataID )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2025-06-18 16:00:20 +02:00
await m_rtr . CommitAsync ( token ). ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-06-18 16:00:20 +02:00
await AddKnownFile ( fileidobj , lastmodified , token ). ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-06-17 15:13:33 +02:00
/// Adds a file record to the database.
2019-01-25 23:37:57 +01:00
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="filename">The path to the file.</param>
/// <param name="lastmodified">The time the file was modified.</param>
/// <param name="blocksetID">The ID of the hashkey for the file.</param>
/// <param name="metadataID">The ID for the metadata.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the file is added.</returns>
2025-06-18 16:00:20 +02:00
public async Task AddFile ( string filename , DateTime lastmodified , long blocksetID , long metadataID , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
var split = SplitIntoPrefixAndName ( filename );
2025-05-19 15:43:55 +02:00
await AddFile (
2025-06-18 16:00:20 +02:00
await GetOrCreatePathPrefix ( split . Key , token ). ConfigureAwait ( false ),
2025-05-19 15:43:55 +02:00
split . Value ,
lastmodified ,
blocksetID ,
2025-06-18 16:00:20 +02:00
metadataID ,
token
2025-06-12 08:34:29 +02:00
)
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-04-03 11:31:36 +02:00
/// <summary>
2025-06-17 15:13:33 +02:00
/// Adds a known file to the fileset.
2025-04-03 11:31:36 +02:00
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="fileid">Id of the file.</param>
/// <param name="lastmodified">The time the file was modified.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the file is added.</returns>
2025-06-18 16:00:20 +02:00
public async Task AddKnownFile ( long fileid , DateTime lastmodified , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
await m_insertfileOperationCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@FilesetId" , m_filesetId )
. SetParameterValue ( "@FileId" , fileid )
. SetParameterValue ( "@LastModified" , lastmodified . ToUniversalTime (). Ticks )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Adds a directory entry to the fileset.
/// </summary>
/// <param name="path">The path to the directory.</param>
/// <param name="metadataID">The ID for the metadata.</param>
/// <param name="lastmodified">The time the directory was modified.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the directory entry is added.</returns>
2025-06-18 16:00:20 +02:00
public async Task AddDirectoryEntry ( string path , long metadataID , DateTime lastmodified , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
await AddFile ( path , lastmodified , FOLDER_BLOCKSET_ID , metadataID , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
2025-06-17 15:13:33 +02:00
/// <summary>
/// Adds a symlink entry to the fileset.
/// </summary>
/// <param name="path">The path to the symlink.</param>
/// <param name="metadataID">The ID for the metadata.</param>
/// <param name="lastmodified">The time the symlink was modified.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the symlink entry is added.</returns>
2025-06-18 16:00:20 +02:00
public async Task AddSymlinkEntry ( string path , long metadataID , DateTime lastmodified , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
await AddFile ( path , lastmodified , SYMLINK_BLOCKSET_ID , metadataID , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Gets the ID, last modified time and size of a file in the fileset.
/// </summary>
/// <param name="prefixid">The ID of the path prefix.</param>
/// <param name="path">The path to the file.</param>
/// <param name="filesetid">The ID of the fileset.</param>
/// <param name="includeLength">Whether to include the file length in the result.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to monitor for cancellation requests.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains a tuple with the file ID, last modified time, and file length.</returns>
2025-06-18 16:00:20 +02:00
public async Task <( long , DateTime , long )> GetFileLastModified ( long prefixid , string path , long filesetid , bool includeLength , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
DateTime oldModified ;
long length ;
2019-01-25 23:37:57 +01:00
if ( includeLength )
{
2025-05-19 15:43:55 +02:00
m_selectfilelastmodifiedWithSizeCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@PrefixId" , prefixid )
. SetParameterValue ( "@Path" , path )
. SetParameterValue ( "@FilesetId" , filesetid );
2025-05-19 15:43:55 +02:00
2025-06-18 16:00:20 +02:00
await using var rd = await m_selectfilelastmodifiedWithSizeCommand . ExecuteReaderAsync ( m_logQueries , token ). ConfigureAwait ( false );
if ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
{
oldModified = new DateTime ( rd . ConvertValueToInt64 ( 1 ), DateTimeKind . Utc );
length = rd . ConvertValueToInt64 ( 2 );
return ( rd . ConvertValueToInt64 ( 0 ), oldModified , length );
}
2019-01-25 23:37:57 +01:00
}
else
{
2025-05-19 14:40:00 +02:00
m_selectfilelastmodifiedCommand . SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@PrefixId" , prefixid )
. SetParameterValue ( "@Path" , path )
. SetParameterValue ( "@FilesetId" , filesetid );
2025-05-19 15:43:55 +02:00
2025-06-18 16:00:20 +02:00
await using var rd = await m_selectfilelastmodifiedCommand . ExecuteReaderAsync ( m_logQueries , token ). ConfigureAwait ( false );
if ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
{
length = - 1 ;
oldModified = new DateTime ( rd . ConvertValueToInt64 ( 1 ), DateTimeKind . Utc );
return ( rd . ConvertValueToInt64 ( 0 ), oldModified , length );
}
2019-01-25 23:37:57 +01:00
}
2025-05-19 15:43:55 +02:00
2019-01-25 23:37:57 +01:00
oldModified = new DateTime ( 0 , DateTimeKind . Utc );
length = - 1 ;
2025-05-19 15:43:55 +02:00
return (- 1 , oldModified , length );
2019-01-25 23:37:57 +01:00
}
2025-06-19 11:01:12 +02:00
/// <summary>
/// Gets the file entry for a given path in the fileset.
/// </summary>
/// <param name="prefixid">The ID of the path prefix.</param>
/// <param name="path">The path to the file.</param>
/// <param name="filesetid">The ID of the fileset.</param>
/// <param name="token">The cancellation token to monitor for cancellation requests.</param>
/// <returns>A task that when awaited contains a tuple with the file ID, last modified time, file size, metadata hash, and metadata size.</returns>
/// <remarks>
public async Task <( long , DateTime , long , string? , long )> GetFileEntry ( long prefixid , string path , long filesetid , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
DateTime oldModified ;
long lastFileSize ;
string? oldMetahash ;
long oldMetasize ;
m_findfileCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@PrefixId" , prefixid )
. SetParameterValue ( "@Path" , path )
. SetParameterValue ( "@FilesetId" , filesetid );
2019-01-25 23:37:57 +01:00
2025-06-19 11:01:12 +02:00
await using var rd = await m_findfileCommand . ExecuteReaderAsync ( token ). ConfigureAwait ( false );
if ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
{
oldModified = new DateTime ( rd . ConvertValueToInt64 ( 1 ), DateTimeKind . Utc );
lastFileSize = rd . ConvertValueToInt64 ( 2 );
oldMetahash = rd . ConvertValueToString ( 3 );
oldMetasize = rd . ConvertValueToInt64 ( 4 );
return (
rd . ConvertValueToInt64 ( 0 ),
oldModified ,
lastFileSize ,
oldMetahash ,
oldMetasize
);
}
else
{
oldModified = new DateTime ( 0 , DateTimeKind . Utc );
lastFileSize = - 1 ;
oldMetahash = null ;
oldMetasize = - 1 ;
return (
- 1 ,
oldModified ,
lastFileSize ,
oldMetahash ,
oldMetasize
);
}
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Gets the metadata hash and size for a file.
/// </summary>
/// <param name="fileid">The ID of the file.</param>
2025-06-19 11:01:12 +02:00
/// <param name="token">A cancellation token to monitor for cancellation requests.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains a tuple with the metadata hash and size, or null if the file does not exist.</returns>
2025-06-19 11:01:12 +02:00
public async Task <( string MetadataHash , long Size )?> GetMetadataHashAndSizeForFile ( long fileid , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
m_selectfilemetadatahashandsizeCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@FileId" , fileid );
2019-01-25 23:37:57 +01:00
2025-06-19 11:01:12 +02:00
await using var rd = await m_selectfilemetadatahashandsizeCommand . ExecuteReaderAsync ( token ). ConfigureAwait ( false );
if ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
return (
rd . ConvertValueToString ( 1 ) ?? throw new InvalidOperationException ( "Metadata hash is null" ),
rd . ConvertValueToInt64 ( 0 )
);
2019-01-25 23:37:57 +01:00
return null ;
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Gets the hash of a file.
/// </summary>
/// <param name="fileid">The ID of the file.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to monitor for cancellation requests.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains the hash of the file, or null if the file does not exist.</returns>
2025-06-18 16:00:20 +02:00
public async Task < string? > GetFileHash ( long fileid , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
var r = await m_selectfileHashCommand
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@FileId" , fileid )
2025-06-18 16:00:20 +02:00
. ExecuteScalarAsync ( m_logQueries , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2019-01-25 23:37:57 +01:00
if ( r == null || r == DBNull . Value )
return null ;
2019-08-05 20:14:05 -04:00
2019-01-25 23:37:57 +01:00
return r . ToString ();
}
2019-08-05 20:14:05 -04:00
public override void Dispose ()
2025-05-19 15:43:55 +02:00
{
2025-06-17 15:11:25 +02:00
this . DisposeAsync (). AsTask (). Await ();
2025-05-19 15:43:55 +02:00
}
2025-06-17 15:11:25 +02:00
public override async ValueTask DisposeAsync ()
2019-01-25 23:37:57 +01:00
{
2025-04-06 08:41:59 +02:00
if (! string . IsNullOrWhiteSpace ( m_tempDeletedBlockTable ))
try
{
2025-06-18 11:53:11 +02:00
await using ( var cmd = m_connection . CreateCommand ( m_rtr ))
2025-06-18 16:00:20 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE ""{m_tempDeletedBlockTable}""" , default )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
await m_rtr . CommitAsync (). ConfigureAwait ( false );
2025-04-06 08:41:59 +02:00
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( LOGTAG , "DropTempTableFailed" , ex , "Failed to drop temporary table {0}: {1}" , m_tempDeletedBlockTable , ex . Message );
}
2025-06-12 08:34:29 +02:00
await base . DisposeAsync (). ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Gets the size of the last written DBlock volume.
/// </summary>
2025-06-18 16:00:20 +02:00
/// <param name="token"> The cancellation token to monitor for cancellation requests.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains the size of the last written DBlock volume, or -1 if no such volume exists.</returns>
2025-06-18 16:00:20 +02:00
public async Task < long > GetLastWrittenDBlockVolumeSize ( CancellationToken token )
2025-04-23 15:39:32 +02:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
return await cmd . SetCommandAndParameters ( @"
2025-05-19 15:43:55 +02:00
SELECT ""Size""
FROM ""RemoteVolume""
WHERE
""State"" = @State
AND ""Type"" = @Type
ORDER BY ""ID"" DESC
LIMIT 1
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@State" , RemoteVolumeState . Uploaded . ToString ())
. SetParameterValue ( "@Type" , RemoteVolumeType . Blocks . ToString ())
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async (- 1 , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-04-23 15:39:32 +02:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Gets the ID of the previous fileset based on the operation timestamp and current fileset ID.
/// </summary>
/// <param name="cmd">The command to use for the query.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains the ID of the previous fileset, or -1 if no such fileset exists.</returns>
2025-06-18 16:00:20 +02:00
private async Task < long > GetPreviousFilesetID ( SqliteCommand cmd , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
return await GetPreviousFilesetID ( cmd , OperationTimestamp , m_filesetId , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
2025-06-17 15:13:33 +02:00
/// <summary>
/// Gets the ID of the previous fileset based on the operation timestamp and current fileset ID.
/// </summary>
/// <param name="cmd">The command to use for the query.</param>
/// <param name="timestamp">The timestamp to use for the query.</param>
/// <param name="filesetid">The current fileset ID.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains the ID of the previous fileset, or -1 if no such fileset exists.</returns>
2025-06-18 16:00:20 +02:00
private async Task < long > GetPreviousFilesetID ( SqliteCommand cmd , DateTime timestamp , long filesetid , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 15:43:55 +02:00
return await cmd
. SetTransaction ( m_rtr )
. SetCommandAndParameters ( @"
SELECT ""ID""
FROM ""Fileset""
WHERE
""Timestamp"" < @Timestamp
AND ""ID"" != @FilesetId
ORDER BY ""Timestamp"" DESC
" )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@Timestamp" , Library . Utility . Utility . NormalizeDateTimeToEpochSeconds ( timestamp ))
. SetParameterValue ( "@FilesetId" , filesetid )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async (- 1 , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Gets the count and size of files in the last backup fileset.
/// </summary>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains a tuple with the count of files and the total size of files in the last backup fileset.</returns>
2025-06-18 16:00:20 +02:00
internal async Task < Tuple < long , long >> GetLastBackupFileCountAndSize ( CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
var lastFilesetId = await cmd . ExecuteScalarInt64Async ( @"
2025-06-18 16:00:20 +02:00
SELECT ""ID""
FROM ""Fileset""
ORDER BY ""Timestamp"" DESC
LIMIT 1
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2025-06-18 11:18:25 +02:00
var count = await cmd . SetCommandAndParameters ( @"
2025-06-18 16:00:20 +02:00
SELECT COUNT(*)
FROM ""FileLookup""
INNER JOIN ""FilesetEntry""
ON ""FileLookup"".""ID"" = ""FilesetEntry"".""FileID""
WHERE
""FilesetEntry"".""FilesetID"" = @FilesetId
AND ""FileLookup"".""BlocksetID"" NOT IN (
@FolderBlocksetId,
@SymlinkBlocksetId
)
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@FilesetId" , lastFilesetId )
. SetParameterValue ( "@FolderBlocksetId" , FOLDER_BLOCKSET_ID )
. SetParameterValue ( "@SymlinkBlocksetId" , SYMLINK_BLOCKSET_ID )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async (- 1 , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2025-06-18 11:18:25 +02:00
var size = await cmd . SetCommandAndParameters ( @"
2025-05-19 15:43:55 +02:00
SELECT SUM(""Blockset"".""Length"")
FROM
""FileLookup"",
""FilesetEntry"",
""Blockset""
WHERE
""FileLookup"".""ID"" = ""FilesetEntry"".""FileID"" AND
""FileLookup"".""BlocksetID"" = ""Blockset"".""ID"" AND
""FilesetEntry"".""FilesetID"" = @FilesetId AND
""FileLookup"".""BlocksetID"" NOT IN (
@FolderBlocksetId,
@SymlinkBlocksetId
)
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@FilesetId" , lastFilesetId )
. SetParameterValue ( "@FolderBlocksetId" , FOLDER_BLOCKSET_ID )
. SetParameterValue ( "@SymlinkBlocksetId" , SYMLINK_BLOCKSET_ID )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async (- 1 , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
2025-06-18 11:18:25 +02:00
return new Tuple < long , long >( count , size );
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Updates the change statistics for the current fileset based on the results of a backup operation.
/// </summary>
/// <param name="results">The results of the backup operation.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the change statistics are updated.</returns>
2025-06-18 16:00:20 +02:00
internal async Task UpdateChangeStatistics ( BackupResults results , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 16:00:20 +02:00
var prevFileSetId = await GetPreviousFilesetID ( cmd , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-06-18 16:00:20 +02:00
await ChangeStatistics . UpdateChangeStatistics ( cmd , results , m_filesetId , prevFileSetId , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-05-19 13:19:24 +02:00
/// Populates FilesetEntry table with files from previous fileset, which aren't
2019-01-25 23:37:57 +01:00
/// yet part of the new fileset, and which aren't on the (optional) list of <c>deleted</c> paths.
/// </summary>
2025-06-18 16:00:20 +02:00
/// <param name="deleted">List of deleted paths, or null.</param>"
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the files are appended.</returns>
2025-06-18 16:00:20 +02:00
public async Task AppendFilesFromPreviousSet ( IEnumerable < string >? deleted , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
await AppendFilesFromPreviousSet ( deleted , m_filesetId , - 1 , OperationTimestamp , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-05-19 13:19:24 +02:00
/// Populates FilesetEntry table with files from previous fileset, which aren't
2019-01-25 23:37:57 +01:00
/// yet part of the new fileset, and which aren't on the (optional) list of <c>deleted</c> paths.
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="deleted">List of deleted paths, or null.</param>
/// <param name="filesetid">Current file-set ID.</param>
/// <param name="prevId">Source file-set ID.</param>
/// <param name="timestamp">If <c>filesetid</c> == -1, used to locate previous file-set.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the files are appended.</returns>
2025-06-18 16:00:20 +02:00
public async Task AppendFilesFromPreviousSet ( IEnumerable < string >? deleted , long filesetid , long prevId , DateTime timestamp , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ();
await using var cmdDelete = m_connection . CreateCommand ();
2025-06-18 11:18:25 +02:00
long lastFilesetId = prevId < 0 ?
2025-06-18 16:00:20 +02:00
await GetPreviousFilesetID ( cmd , timestamp , filesetid , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false )
:
prevId ;
await cmd . SetTransaction ( m_rtr )
. SetCommandAndParameters ( @"
2025-05-19 15:43:55 +02:00
INSERT INTO ""FilesetEntry"" (
""FilesetID"",
""FileID"",
""Lastmodified""
)
SELECT
@CurrentFilesetId AS ""FilesetID"",
""FileID"",
""Lastmodified""
FROM (
SELECT DISTINCT
""FilesetID"",
""FileID"",
""Lastmodified""
FROM ""FilesetEntry""
WHERE
""FilesetID"" = @PreviousFilesetId
AND ""FileID"" NOT IN (
SELECT ""FileID""
FROM ""FilesetEntry""
WHERE ""FilesetID"" = @CurrentFilesetId
)
)
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@CurrentFilesetId" , filesetid )
. SetParameterValue ( "@PreviousFilesetId" , lastFilesetId )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( m_logQueries , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
2025-06-18 11:18:25 +02:00
if ( deleted != null )
{
2025-06-18 11:53:11 +02:00
await using var tmplist = await TemporaryDbValueList
2025-06-18 16:00:20 +02:00
. CreateAsync ( this , deleted , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-06-12 08:34:29 +02:00
2025-06-18 11:18:25 +02:00
await (
await cmdDelete . SetTransaction ( m_rtr )
. SetCommandAndParameters ( @"
2025-06-12 08:34:29 +02:00
DELETE FROM ""FilesetEntry""
WHERE
""FilesetID"" = @FilesetId
AND ""FileID"" IN (
SELECT ""ID""
FROM ""File""
WHERE ""Path"" IN (@Paths)
)
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@FilesetId" , filesetid )
2025-06-18 16:00:20 +02:00
. ExpandInClauseParameterMssqliteAsync ( "@Paths" , tmplist , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false )
)
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( m_logQueries , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-06-18 11:18:25 +02:00
2025-06-18 16:00:20 +02:00
await m_rtr . CommitAsync ( token ). ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-05-19 13:19:24 +02:00
/// Populates FilesetEntry table with files from previous fileset, which aren't
/// yet part of the new fileset, and which aren't excluded by the (optional) exclusion
2019-01-25 23:37:57 +01:00
/// predicate.
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="exclusionPredicate">Optional exclusion predicate (true = exclude file).</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the files are appended.</returns>
2025-06-18 16:00:20 +02:00
public async Task AppendFilesFromPreviousSetWithPredicate ( Func < string , long , bool > exclusionPredicate , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
await AppendFilesFromPreviousSetWithPredicate ( exclusionPredicate , m_filesetId , - 1 , OperationTimestamp , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-05-19 13:19:24 +02:00
/// Populates FilesetEntry table with files from previous fileset, which aren't
/// yet part of the new fileset, and which aren't excluded by the (optional) exclusion
2019-01-25 23:37:57 +01:00
/// predicate.
/// </summary>
/// <param name="exclusionPredicate">Optional exclusion predicate (true = exclude file)</param>
/// <param name="fileSetId">Current fileset ID</param>
/// <param name="prevFileSetId">Source fileset ID</param>
/// <param name="timestamp">If <c>prevFileSetId</c> == -1, used to locate previous fileset</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the files are appended.</returns>
2025-06-18 16:00:20 +02:00
public async Task AppendFilesFromPreviousSetWithPredicate ( Func < string , long , bool > exclusionPredicate , long fileSetId , long prevFileSetId , DateTime timestamp , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2022-02-26 20:24:14 -07:00
if ( exclusionPredicate == null )
{
2025-06-18 16:00:20 +02:00
await AppendFilesFromPreviousSet ( null , fileSetId , prevFileSetId , timestamp , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2020-08-18 11:43:23 +02:00
return ;
}
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ();
await using var cmdDelete = m_connection . CreateCommand ();
2025-06-18 11:18:25 +02:00
long lastFilesetId = prevFileSetId < 0 ?
2025-06-18 16:00:20 +02:00
await GetPreviousFilesetID ( cmd , timestamp , fileSetId , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false )
:
prevFileSetId ;
// copy entries from previous file set into a temporary table, except those file IDs already added by the current backup
2025-09-05 14:06:55 +02:00
var tempFileSetTable = $"FilesetEntry-{Library.Utility.Utility.GetHexGuid()}" ;
2025-06-18 11:18:25 +02:00
await cmd
. SetTransaction ( m_rtr )
. SetCommandAndParameters ( $@"
2025-05-19 15:43:55 +02:00
CREATE TEMPORARY TABLE ""{tempFileSetTable}"" AS
SELECT
""FileID"",
""Lastmodified""
FROM (
SELECT DISTINCT
""FilesetID"",
""FileID"",
""Lastmodified""
FROM ""FilesetEntry""
WHERE
""FilesetID"" = @PreviousFilesetId
AND ""FileID"" NOT IN (
SELECT ""FileID""
FROM ""FilesetEntry""
WHERE ""FilesetID"" = @CurrentFilesetId
)
)
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@PreviousFilesetId" , lastFilesetId )
. SetParameterValue ( "@CurrentFilesetId" , fileSetId )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2020-08-17 18:06:38 +02:00
2025-06-18 11:18:25 +02:00
// now we need to remove, from the above, any entries that were enumerated by the
// UNC-driven backup
cmdDelete . SetTransaction ( m_rtr )
. SetCommandAndParameters ( $@"
2025-05-19 15:43:55 +02:00
DELETE FROM ""{tempFileSetTable}""
WHERE ""FileID"" = @FileId
" );
2019-01-25 23:37:57 +01:00
2025-06-18 11:18:25 +02:00
// enumerate files from new temporary file set, and remove any entries handled by UNC
cmd . SetCommandAndParameters ( $@"
2025-05-19 15:43:55 +02:00
SELECT
2025-06-18 10:42:08 +02:00
""f"".""Path"",
""fs"".""FileID"",
""fs"".""Lastmodified"",
COALESCE(""bs"".""Length"", -1)
2025-05-19 15:43:55 +02:00
FROM (
SELECT DISTINCT
""FileID"",
""Lastmodified""
FROM ""{tempFileSetTable}""
2025-06-18 10:42:08 +02:00
) AS ""fs""
LEFT JOIN ""File"" AS ""f""
ON ""fs"".""FileID"" = ""f"".""ID""
LEFT JOIN ""Blockset"" AS ""bs""
ON ""f"".""BlocksetID"" = ""bs"".""ID"";
2025-05-19 15:43:55 +02:00
" );
2025-06-18 16:00:20 +02:00
await foreach ( var row in cmd . ExecuteReaderEnumerableAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
{
var path = row . ConvertValueToString ( 0 ) ?? throw new Exception ( "Unexpected null value for path" );
var size = row . ConvertValueToInt64 ( 3 );
2020-08-17 18:06:38 +02:00
2025-06-18 11:18:25 +02:00
if ( exclusionPredicate ( path , size ))
await cmdDelete . SetParameterValue ( "@FileId" , row . ConvertValueToInt64 ( 1 ))
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
}
2019-01-25 23:37:57 +01:00
2025-06-18 11:18:25 +02:00
// now copy the temporary table into the FileSetEntry table
await cmd . SetCommandAndParameters ( $@"
2025-05-19 15:43:55 +02:00
INSERT INTO ""FilesetEntry"" (
""FilesetID"",
""FileID"",
""Lastmodified""
)
SELECT
@FilesetId,
""FileID"",
""Lastmodified""
FROM ""{tempFileSetTable}""
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@FilesetId" , fileSetId )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2020-08-18 11:43:23 +02:00
2025-06-18 16:00:20 +02:00
await m_rtr . CommitAsync ( token ). ConfigureAwait ( false );
2022-02-26 20:24:14 -07:00
}
2019-01-25 23:37:57 +01:00
/// <summary>
/// Creates a timestamped backup operation to correctly associate the fileset with the time it was created.
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="volumeid">The ID of the fileset volume to update.</param>
/// <param name="timestamp">The timestamp of the operation to create.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains the ID of the created fileset.</returns>
2025-06-18 16:00:20 +02:00
public override async Task < long > CreateFileset ( long volumeid , DateTime timestamp , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
return m_filesetId = await base . CreateFileset ( volumeid , timestamp , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
2025-06-17 15:13:33 +02:00
/// <summary>
/// Retrieves the names of temporary fileset volumes that are incomplete.
/// </summary>
/// <param name="latestOnly">If true, only the latest incomplete fileset volume will be returned.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains a list of volume names.</returns>
2025-06-18 16:00:20 +02:00
public async Task < IEnumerable < string >> GetTemporaryFilelistVolumeNames ( bool latestOnly , CancellationToken token )
2022-02-26 20:24:14 -07:00
{
2025-06-18 16:00:20 +02:00
var incompleteFilesetIDs = GetIncompleteFilesets ( token ). OrderBy ( x => x . Value ). Select ( x => x . Key );
2022-02-26 20:24:14 -07:00
2025-06-18 16:00:20 +02:00
if (! await incompleteFilesetIDs . AnyAsync ( token ). ConfigureAwait ( false ))
2025-05-19 15:43:55 +02:00
return [];
2022-02-26 20:24:14 -07:00
if ( latestOnly )
2025-06-12 08:34:29 +02:00
incompleteFilesetIDs = new long [] {
2025-06-18 16:00:20 +02:00
await incompleteFilesetIDs . LastAsync ( token ). ConfigureAwait ( false )
2025-06-12 08:34:29 +02:00
}
. ToAsyncEnumerable ();
2022-02-26 20:24:14 -07:00
var volumeNames = new List < string >();
2025-06-12 08:34:29 +02:00
await foreach ( var filesetID in incompleteFilesetIDs . ConfigureAwait ( false ))
volumeNames . Add ((
2025-06-18 16:00:20 +02:00
await GetRemoteVolumeFromFilesetID ( filesetID , token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false )
). Name );
2022-02-26 20:24:14 -07:00
return volumeNames ;
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Retrieves the names of remote volumes that are missing index files.
/// </summary>
2025-06-19 11:01:12 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>An asynchronous enumerable of volume names that are missing index files.</returns>
2025-06-19 11:01:12 +02:00
public async IAsyncEnumerable < string > GetMissingIndexFiles ([ EnumeratorCancellation ] CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr )
2025-05-19 15:43:55 +02:00
. SetCommandAndParameters ( @"
SELECT ""Name""
FROM ""RemoteVolume""
WHERE
""Type"" = @Type
AND NOT ""ID"" IN (
SELECT ""BlockVolumeID""
FROM ""IndexBlockLink""
)
AND ""State"" IN (@States)
" )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@Type" , RemoteVolumeType . Blocks . ToString ())
2025-06-11 10:47:40 +02:00
. ExpandInClauseParameterMssqlite ( "@States" , [
2025-05-19 15:43:55 +02:00
RemoteVolumeState . Uploaded . ToString (),
RemoteVolumeState . Verified . ToString ()
]);
2025-04-03 11:31:36 +02:00
2025-06-19 11:01:12 +02:00
await using var rd = await cmd . ExecuteReaderAsync ( token ). ConfigureAwait ( false );
while ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2025-06-18 11:18:25 +02:00
yield return rd . ConvertValueToString ( 0 ) ?? throw new Exception ( "Unexpected null value for volume name" );
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
2025-06-17 15:13:33 +02:00
/// <summary>
/// Moves a block from one volume to another.
/// </summary>
/// <param name="blockkey">The hash of the block to move.</param>
/// <param name="size">The size of the block to move.</param>
/// <param name="sourcevolumeid">The ID of the source volume.</param>
/// <param name="targetvolumeid">The ID of the target volume.</param>
2025-06-19 11:01:12 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the block is moved.</returns>
2025-06-19 11:01:12 +02:00
public async Task MoveBlockToVolume ( string blockkey , long size , long sourcevolumeid , long targetvolumeid , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
var c = await cmd . SetCommandAndParameters ( @"
2025-05-19 15:43:55 +02:00
UPDATE ""Block""
SET ""VolumeID"" = @NewVolumeId
WHERE
""Hash"" = @Hash
AND ""Size"" = @Size
AND ""VolumeID"" = @PreviousVolumeId
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@NewVolumeId" , targetvolumeid )
. SetParameterValue ( "@Hash" , blockkey )
. SetParameterValue ( "@Size" , size )
. SetParameterValue ( "@PreviousVolumeId" , sourcevolumeid )
2025-06-19 11:01:12 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2025-06-18 11:18:25 +02:00
if ( c != 1 )
throw new Exception ( $"Failed to move block {blockkey}:{size} from volume {sourcevolumeid}, count: {c}" );
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Safely deletes a remote volume by checking if it has any associated blocks.
/// If it does, an exception is thrown; otherwise, the volume is removed.
/// </summary>
/// <param name="name">The name of the remote volume to delete.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the remote volume is safely deleted.</returns>
/// <exception cref="Exception">Thrown if the volume has associated blocks.</exception>
2025-06-18 16:00:20 +02:00
public async Task SafeDeleteRemoteVolume ( string name , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
var volumeid = await GetRemoteVolumeID ( name , token ). ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
var c = await cmd . SetCommandAndParameters ( @"
2025-05-19 15:43:55 +02:00
SELECT COUNT(*)
FROM ""Block""
WHERE ""VolumeID"" = @VolumeId
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@VolumeId" , volumeid )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async (- 1 , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2025-06-18 11:18:25 +02:00
if ( c != 0 )
throw new Exception ( $"Failed to safe-delete volume {name}, blocks: {c}" );
2019-01-25 23:37:57 +01:00
2025-06-18 16:00:20 +02:00
await RemoveRemoteVolume ( name , token ). ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Retrieves the hashes of blocks that are on the blocklist for a given volume.
/// </summary>
/// <param name="name">The name of the volume to check.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token"> The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains an array of blocklist hashes.</returns>
2025-06-18 16:00:20 +02:00
public async Task < string []> GetBlocklistHashes ( string name , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 16:00:20 +02:00
var volumeid = GetRemoteVolumeID ( name , token );
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
// Grab the strings and return as array to avoid concurrent access to the IEnumerable
cmd . SetCommandAndParameters ( @"
2025-05-19 15:43:55 +02:00
SELECT DISTINCT ""Block"".""Hash""
FROM ""Block""
WHERE
""Block"".""VolumeID"" = @VolumeId
AND ""Block"".""Hash"" IN (
SELECT ""Hash""
FROM ""BlocklistHash""
)
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@VolumeId" , volumeid );
2025-05-19 15:43:55 +02:00
2025-06-18 16:00:20 +02:00
return await cmd . ExecuteReaderEnumerableAsync ( token )
2025-06-18 11:18:25 +02:00
. Select ( x => x . ConvertValueToString ( 0 ) ?? throw new Exception ( "Unexpected null value for blocklist hash" ))
2025-06-18 16:00:20 +02:00
. ToArrayAsync ( cancellationToken : token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
2025-06-17 15:13:33 +02:00
/// <summary>
/// Retrieves the first path in the database, ordered by length in descending order.
/// </summary>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited contains the first path, or null if no paths exist.</returns>
2025-06-18 16:00:20 +02:00
public async Task < string? > GetFirstPath ( CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
var v0 = await cmd . ExecuteScalarAsync ( @"
2025-05-19 15:43:55 +02:00
SELECT ""Path""
FROM ""File""
ORDER BY LENGTH(""Path"") DESC
LIMIT 1
2025-06-18 16:00:20 +02:00
" , token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2025-06-18 11:18:25 +02:00
if ( v0 == null || v0 == DBNull . Value )
return null ;
2019-01-25 23:37:57 +01:00
2025-06-18 11:18:25 +02:00
return v0 . ToString ();
2019-01-25 23:37:57 +01:00
}
2019-08-05 20:14:05 -04:00
2019-01-25 23:37:57 +01:00
/// <summary>
2025-06-17 15:13:33 +02:00
/// Retrieves the change journal data for file set.
2019-01-25 23:37:57 +01:00
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="fileSetId">The Fileset-ID.</param>
/// <returns>An asynchronous enumerable of USN journal data entries.</returns>
2025-06-18 16:00:20 +02:00
public async IAsyncEnumerable < Interface . USNJournalDataEntry > GetChangeJournalData ( long fileSetId , [ EnumeratorCancellation ] CancellationToken token )
2019-01-25 23:37:57 +01:00
{
var data = new List < Interface . USNJournalDataEntry >();
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( @"
2025-05-19 15:43:55 +02:00
SELECT
""VolumeName"",
""JournalID"",
""NextUSN"",
""ConfigHash""
FROM ""ChangeJournalData""
WHERE ""FilesetID"" = @FilesetId
" )
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@FilesetId" , fileSetId );
2025-05-19 15:43:55 +02:00
2025-06-18 16:00:20 +02:00
await using var rd = await cmd . ExecuteReaderAsync ( token ). ConfigureAwait ( false );
while ( await rd . ReadAsync ( token ). ConfigureAwait ( false ))
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:18:25 +02:00
yield return new Interface . USNJournalDataEntry
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:18:25 +02:00
Volume = rd . ConvertValueToString ( 0 ),
JournalId = rd . ConvertValueToInt64 ( 1 ),
NextUsn = rd . ConvertValueToInt64 ( 2 ),
ConfigHash = rd . ConvertValueToString ( 3 )
};
2019-01-25 23:37:57 +01:00
}
}
/// <summary>
2025-06-17 15:13:33 +02:00
/// Adds NTFS change journal data for file set and volume.
2019-01-25 23:37:57 +01:00
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="data">Data to add.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the data is added.</returns>
/// <exception cref="Exception">Thrown if unable to add change journal entry.</exception>
2025-06-18 16:00:20 +02:00
public async Task CreateChangeJournalData ( IEnumerable < Interface . USNJournalDataEntry > data , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 14:40:00 +02:00
foreach ( var entry in data )
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ( m_rtr );
2025-06-18 11:18:25 +02:00
var c = await cmd . SetCommandAndParameters ( @"
2025-05-19 15:43:55 +02:00
INSERT INTO ""ChangeJournalData"" (
""FilesetID"",
""VolumeName"",
""JournalID"",
""NextUSN"",
""ConfigHash""
)
VALUES (
@FilesetId,
@VolumeName,
@JournalId,
@NextUsn,
@ConfigHash
);
" )
2025-06-18 11:18:25 +02:00
. SetParameterValue ( "@FilesetId" , m_filesetId )
. SetParameterValue ( "@VolumeName" , entry . Volume )
. SetParameterValue ( "@JournalId" , entry . JournalId )
. SetParameterValue ( "@NextUsn" , entry . NextUsn )
. SetParameterValue ( "@ConfigHash" , entry . ConfigHash )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
2025-06-18 11:18:25 +02:00
if ( c != 1 )
throw new Exception ( "Unable to add change journal entry" );
2019-01-25 23:37:57 +01:00
}
2025-05-19 14:40:00 +02:00
2025-06-18 16:00:20 +02:00
await m_rtr . CommitAsync ( token : token ). ConfigureAwait ( false );
2019-01-25 23:37:57 +01:00
}
/// <summary>
2025-06-17 15:13:33 +02:00
/// Adds NTFS change journal data for file set and volume.
2019-01-25 23:37:57 +01:00
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="data">Data to add.</param>
/// <param name="fileSetId">Existing file set to update.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that completes when the data is added.</returns>
2025-06-18 16:00:20 +02:00
public async Task UpdateChangeJournalData ( IEnumerable < Interface . USNJournalDataEntry > data , long fileSetId , CancellationToken token )
2019-01-25 23:37:57 +01:00
{
2025-05-19 14:40:00 +02:00
foreach ( var entry in data )
2019-01-25 23:37:57 +01:00
{
2025-06-18 11:53:11 +02:00
await using var cmd = m_connection . CreateCommand ();
2025-06-18 11:18:25 +02:00
await cmd . SetCommandAndParameters ( @"
2025-05-19 15:43:55 +02:00
UPDATE ""ChangeJournalData""
SET ""NextUSN"" = @NextUsn
WHERE
""FilesetID"" = @FilesetId
AND ""VolumeName"" = @VolumeName
AND ""JournalID"" = @JournalId;
" )
2025-06-18 11:18:25 +02:00
. SetTransaction ( m_rtr )
. SetParameterValue ( "@NextUsn" , entry . NextUsn )
. SetParameterValue ( "@FilesetId" , fileSetId )
. SetParameterValue ( "@VolumeName" , entry . Volume )
. SetParameterValue ( "@JournalId" , entry . JournalId )
2025-06-18 16:00:20 +02:00
. ExecuteNonQueryAsync ( token )
2025-06-18 11:18:25 +02:00
. ConfigureAwait ( false );
2019-08-05 20:14:05 -04:00
}
2025-05-19 14:40:00 +02:00
2025-06-18 16:00:20 +02:00
await m_rtr . CommitAsync ( token : token ). ConfigureAwait ( false );
2019-08-05 20:14:05 -04:00
}
2024-10-31 15:30:25 +01:00
/// <summary>
2025-06-17 15:13:33 +02:00
/// Checks if a blocklist hash is known.
2024-10-31 15:30:25 +01:00
/// </summary>
2025-06-17 15:13:33 +02:00
/// <param name="hash">The hash to check.</param>
2025-06-18 16:00:20 +02:00
/// <param name="token">The cancellation token to cancel the operation.</param>
2025-06-17 15:13:33 +02:00
/// <returns>A task that when awaited returns true if the hash is known, false otherwise.</returns>
2025-06-18 16:00:20 +02:00
public async Task < bool > IsBlocklistHashKnown ( string hash , CancellationToken token )
2024-10-31 15:30:25 +01:00
{
2025-05-19 15:43:55 +02:00
var res = await m_getfirstfilesetwithblockinblockset
. SetTransaction ( m_rtr )
2025-04-03 11:31:36 +02:00
. SetParameterValue ( "@Hash" , hash )
2025-06-18 16:00:20 +02:00
. ExecuteScalarInt64Async ( token )
2025-06-12 08:34:29 +02:00
. ConfigureAwait ( false );
2025-05-19 15:43:55 +02:00
2024-11-04 21:46:12 +01:00
if ( res != - 1 && res != m_filesetId )
2024-10-31 15:30:25 +01:00
return true ;
else
return ! m_blocklistHashes . Add ( hash );
}
2019-01-25 23:37:57 +01:00
}
}