2025-03-10 14:41:12 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-05-13 11:25:17 +02:00
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
2025-03-10 14:41:12 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-13 11:25:17 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-03-10 14:41:12 +01:00
// all copies or substantial portions of the Software.
2025-05-13 11:25:17 +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.
2013-03-30 15:23:09 +01:00
2025-04-03 15:46:20 +02:00
#nullable enable
2013-03-30 15:23:09 +01:00
using System ;
using System.Collections.Generic ;
2025-05-13 11:25:17 +02:00
using System.Threading.Tasks ;
2021-04-04 11:17:13 -07:00
using Duplicati.Library.Utility ;
2025-05-13 11:25:17 +02:00
using Microsoft.Data.Sqlite ;
2013-03-30 15:23:09 +01:00
2013-05-08 20:17:07 +02:00
namespace Duplicati.Library.Main.Database
2013-03-30 15:23:09 +01:00
{
2016-09-15 11:39:27 +02:00
internal class LocalRepairDatabase : LocalDatabase
{
2018-03-12 14:07:11 +01:00
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType ( typeof ( LocalRepairDatabase ));
2025-05-08 10:31:38 +02:00
/// <summary>
/// Creates a new local repair database
/// </summary>
/// <param name="path">The path to the database</param>
/// <param name="pagecachesize">The page cache size</param>
2025-04-22 14:26:46 +02:00
public LocalRepairDatabase ( string path , long pagecachesize )
: base ( path , "Repair" , true , pagecachesize )
2016-09-15 11:39:27 +02:00
{
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
}
2025-03-10 14:41:12 +01:00
2025-05-08 10:31:38 +02:00
/// <summary>
/// Gets the fileset ID from the remote name
/// </summary>
/// <param name="filelist">The remote name of the fileset</param>
/// <param name="transaction">An optional transaction</param>
/// <returns>>The fileset ID</returns>
2025-05-13 11:25:17 +02:00
public async Task <( long FilesetId , DateTime Time , bool IsFullBackup )> GetFilesetFromRemotename ( string filelist , SqliteTransaction ? transaction )
2016-09-15 11:39:27 +02:00
{
2025-05-13 11:27:47 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( @"SELECT ""Fileset"".""ID"", ""Fileset"".""Timestamp"", ""Fileset"".""IsFullBackup"" FROM ""Fileset"",""RemoteVolume"" WHERE ""Fileset"".""VolumeID"" = ""RemoteVolume"".""ID"" AND ""RemoteVolume"".""Name"" = @Name" );
cmd . SetParameterValue ( "@Name" , filelist );
var rd = await cmd . ExecuteReaderAsync ();
2025-04-02 23:34:30 +02:00
2025-05-13 11:27:47 +02:00
if (! await rd . ReadAsync ())
throw new Exception ( $"No such remote file: {filelist}" );
2025-03-10 14:41:12 +01:00
2025-05-13 11:27:47 +02:00
return ( rd . ConvertValueToInt64 ( 0 , - 1 ), ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 )). ToLocalTime (), rd . GetInt32 ( 2 ) == BackupType . FULL_BACKUP );
2016-09-15 11:39:27 +02:00
}
2025-05-08 10:31:38 +02:00
/// <summary>
/// Moves entries in the FilesetEntry table from previous fileset to current fileset
/// </summary>
/// <param name="filesetid">Current fileset ID</param>
/// <param name="prevFilesetId">Source fileset ID</param>
/// <param name="transaction">The transaction</param>
2025-05-13 11:25:17 +02:00
public async Task MoveFilesFromFileset ( long filesetid , long prevFilesetId , SqliteTransaction transaction )
2025-05-08 10:31:38 +02:00
{
if ( filesetid <= 0 )
throw new ArgumentException ( "filesetid must be > 0" );
if ( prevFilesetId <= 0 )
throw new ArgumentException ( "prevId must be > 0" );
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . SetCommandAndParameters ( @"UPDATE ""FilesetEntry"" SET ""FilesetID"" = @CurrentFilesetId WHERE ""FilesetID"" = @PreviousFilesetId " );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@CurrentFilesetId" , filesetid );
cmd . SetParameterValue ( "@PreviousFilesetId" , prevFilesetId );
await cmd . ExecuteNonQueryAsync ();
2025-05-10 09:12:11 +02:00
}
2025-05-08 10:31:38 +02:00
/// <summary>
/// Gets the list of index files that reference a given block file
/// </summary>
/// <param name="blockfileid">The block file ID</param>
/// <returns>>A list of index file IDs</returns>
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < string > GetIndexFilesReferencingBlockFile ( long blockfileid , SqliteTransaction transaction )
2025-05-08 10:31:38 +02:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT ""RemoteVolume"".""Name"" FROM ""RemoteVolume"", ""IndexBlockLink"" WHERE ""IndexBlockLink"".""BlockVolumeID"" = @BlockFileId AND ""RemoteVolume"".""ID"" = ""IndexBlockLink"".""IndexVolumeID"" AND ""RemoteVolume"".""Type"" = @Type" );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@BlockFileId" , blockfileid );
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Index . ToString ());
2025-05-08 10:31:38 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-05-08 10:31:38 +02:00
yield return rd . ConvertValueToString ( 0 ) ?? throw new Exception ( "RemoteVolume name was null" );
}
2025-03-20 15:59:14 +01:00
/// <summary>
/// Gets a list of filesets that are missing files
/// </summary>
/// <param name="transaction">An optional transaction</param>
/// <returns>A list of fileset IDs and timestamps</returns>
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < KeyValuePair < long , DateTime >> GetFilesetsWithMissingFiles ( SqliteTransaction transaction )
2025-03-20 15:59:14 +01:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
using var rd = await cmd . ExecuteReaderAsync ( @"SELECT ID, Timestamp FROM Fileset WHERE ID IN (SELECT FilesetID FROM ""FilesetEntry"" WHERE ""FileID"" NOT IN (SELECT ""ID"" FROM ""FileLookup""))" );
while ( await rd . ReadAsync ())
{
yield return new KeyValuePair < long , DateTime >(
rd . ConvertValueToInt64 ( 0 ),
ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 )). ToLocalTime ()
);
}
2025-03-20 15:59:14 +01:00
}
/// <summary>
/// Deletes all fileset entries for a given fileset
/// </summary>
/// <param name="filesetid">The fileset ID</param>
/// <param name="transaction">An optional transaction</param>
/// <returns>The number of deleted entries</returns>
2025-05-13 11:25:17 +02:00
public async Task < int > DeleteFilesetEntries ( long filesetid , SqliteTransaction transaction )
2025-03-20 15:59:14 +01:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
cmd . SetCommandAndParameters ( @"DELETE FROM ""FilesetEntry"" WHERE ""FilesetID"" = @FilesetId" );
cmd . SetParameterValue ( "@FilesetId" , filesetid );
return await cmd . ExecuteNonQueryAsync ();
2025-03-20 15:59:14 +01:00
}
2016-09-15 11:39:27 +02:00
private class RemoteVolume : IRemoteVolume
{
public string Name { get ; private set ; }
public string Hash { get ; private set ; }
public long Size { get ; private set ; }
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
public RemoteVolume ( string name , string hash , long size )
{
this . Name = name ;
this . Hash = hash ;
this . Size = size ;
}
}
2025-03-10 14:41:12 +01:00
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < IRemoteVolume > GetBlockVolumesFromIndexName ( string indexName , SqliteTransaction transaction )
2016-09-15 11:39:27 +02:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT ""Name"", ""Hash"", ""Size"" FROM ""RemoteVolume"" WHERE ""ID"" IN (SELECT ""BlockVolumeID"" FROM ""IndexBlockLink"" WHERE ""IndexVolumeID"" IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" = @Name)) " );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@Name" , indexName );
2025-04-02 23:34:30 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-04-03 15:46:20 +02:00
yield return new RemoteVolume ( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToString ( 1 ) ?? "" , rd . ConvertValueToInt64 ( 2 ));
2016-09-15 11:39:27 +02:00
}
2025-03-10 14:41:12 +01:00
2025-05-08 10:31:38 +02:00
/// <summary>
/// A single block with source data for repair
/// </summary>
/// <param name="Hash">The block hash</param>
/// <param name="Size">The block size</param>
/// <param name="File">The file that contains the block</param>
/// <param name="Offset">The offset of the block in the file</param>
public sealed record BlockWithSourceData ( string Hash , long Size , string File , long Offset );
/// <summary>
/// A single block with metadata source data for repair
/// </summary>
/// <param name="Hash">The block hash</param>
/// <param name="Size">The block size</param>
/// <param name="Path">The path of the file or directory that contains the block</param>
public sealed record BlockWithMetadataSourceData ( string Hash , long Size , string Path );
/// <summary>
/// A single blocklist hash entry
/// </summary>
/// <param name="BlocksetId">The blockset id</param>
/// <param name="BlocklistHash">The hash of the blocklist entry (when done)</param>
/// <param name="BlocklistHashLength">The total length of the blockset</param>
/// <param name="BlocklistHashIndex">The index of the blocklist hash</param>
/// <param name="Index">The index of the block in the blockset</param>
/// <param name="Hash">The hash of the entry</param>
public sealed record BlocklistHashesEntry (
long BlocksetId ,
string BlocklistHash ,
long BlocklistHashLength ,
long BlocklistHashIndex ,
int Index ,
string Hash );
/// <summary>
/// Helper interface for the missing block list
/// </summary>
2013-06-24 20:49:32 +02:00
public interface IMissingBlockList : IDisposable
{
2025-05-08 10:31:38 +02:00
/// <summary>
/// Registers a block as restored
/// </summary>
/// <param name="hash">The block hash</param>
/// <param name="size">The block size</param>
2025-05-09 11:12:29 +02:00
/// <param name="volumeId">The volume ID of the new target volume</param>
2025-05-08 10:31:38 +02:00
/// <returns>The restored blocks</returns>
2025-05-13 11:25:17 +02:00
Task < bool > SetBlockRestored ( string hash , long size , long volumeId );
2025-05-08 10:31:38 +02:00
/// <summary>
/// Gets the list of files that contains missing blocks
/// </summary>
/// <param name="blocksize">The blocksize setting</param>
/// <returns>A list of files with missing blocks</returns>
2025-05-13 11:25:17 +02:00
IAsyncEnumerable < BlockWithSourceData > GetSourceFilesWithBlocks ( long blocksize );
2025-05-08 10:31:38 +02:00
/// <summary>
/// Gets a list for filesystem entries that contain missing blocks in metadata
/// </summary>
/// <returns>A list of filesystem entries with missing blocks</returns>
2025-05-13 11:25:17 +02:00
IAsyncEnumerable < BlockWithMetadataSourceData > GetSourceItemsWithMetadataBlocks ();
2025-05-08 10:31:38 +02:00
/// <summary>
/// Gets missing blocklist hashes
/// </summary>
/// <param name="hashesPerBlock">The number of hashes for each block</param>
/// <returns>>A list of blocklist hashes</returns>
2025-05-13 11:25:17 +02:00
IAsyncEnumerable < BlocklistHashesEntry > GetBlocklistHashes ( long hashesPerBlock );
2025-05-08 10:31:38 +02:00
/// <summary>
/// Gets the number of missing blocks
/// </summary>
/// <returns>>The number of missing blocks</returns>
2025-05-13 11:25:17 +02:00
Task < long > GetMissingBlockCount ();
2025-05-08 10:31:38 +02:00
/// <summary>
/// Gets all the filesets that are affected by missing blocks
/// </summary>
/// <returns>>A list of filesets</returns>
2025-05-13 11:25:17 +02:00
IAsyncEnumerable < IRemoteVolume > GetFilesetsUsingMissingBlocks ();
2025-05-08 10:31:38 +02:00
/// <summary>
/// Gets a list of remote files that may contain missing blocks
/// </summary>
/// <returns>>A list of remote files</returns>
2025-05-13 11:25:17 +02:00
IAsyncEnumerable < IRemoteVolume > GetMissingBlockSources ();
2013-06-24 20:49:32 +02:00
}
2025-03-10 14:41:12 +01:00
2025-05-08 10:31:38 +02:00
/// <summary>
/// Implementation of the missing block list
/// </summary>
2013-06-24 20:49:32 +02:00
private class MissingBlockList : IMissingBlockList
{
2025-05-08 10:31:38 +02:00
/// <summary>
/// The connection to the database
/// </summary>
2025-05-13 11:25:17 +02:00
private readonly SqliteConnection m_connection ;
2025-05-08 10:31:38 +02:00
/// <summary>
/// The transaction to use
/// </summary>
private readonly ReusableTransaction m_transaction ;
/// <summary>
/// The insert command to use for restoring blocks
/// </summary>
2025-05-13 11:25:17 +02:00
private readonly SqliteCommand m_insertCommand ;
2025-05-08 10:31:38 +02:00
/// <summary>
2025-05-09 11:12:29 +02:00
/// The command to copy blocks into the duplicate block table
/// </summary>
2025-05-13 11:25:17 +02:00
private readonly SqliteCommand m_copyIntoDuplicatedBlocks ;
2025-05-09 11:12:29 +02:00
/// <summary>
/// The command to assign blocks to a new volume
/// </summary>
2025-05-13 11:25:17 +02:00
private readonly SqliteCommand m_assignBlocksToNewVolume ;
2025-05-09 11:12:29 +02:00
/// <summary>
2025-05-08 10:31:38 +02:00
/// The name of the temporary table
/// </summary>
private readonly string m_tablename ;
/// <summary>
/// The name of the volume where blocks are missing
/// </summary>
2018-05-23 21:18:01 -07:00
private readonly string m_volumename ;
2025-05-08 10:31:38 +02:00
/// <summary>
/// The query to use for getting missing blocks
/// </summary>
private readonly string m_missingBlocksQuery ;
/// <summary>
/// Whether the object has been disposed
/// </summary>
private bool m_isDisposed = false ;
/// <summary>
/// Creates a new missing block list
/// </summary>
/// <param name="volumename">The name of the volume with missing blocks</param>
/// <param name="connection">The connection to the database</param>
/// <param name="transaction">The transaction to use</param>
2025-05-13 11:25:17 +02:00
public MissingBlockList ( string volumename , SqliteConnection connection , ReusableTransaction transaction )
2013-06-24 20:49:32 +02:00
{
m_connection = connection ;
2025-05-08 10:31:38 +02:00
m_transaction = transaction ;
2013-06-24 20:49:32 +02:00
m_volumename = volumename ;
var tablename = "MissingBlocks-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2025-05-13 11:25:17 +02:00
using ( var cmd = m_connection . CreateCommand ())
2013-06-24 20:49:32 +02:00
{
2025-05-13 11:25:17 +02:00
cmd . Transaction = transaction . Transaction ;
cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{tablename}"" (""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL, ""Restored"" INTEGER NOT NULL) " )). Await ();
2013-06-24 20:49:32 +02:00
m_tablename = tablename ;
2025-03-10 14:41:12 +01:00
2025-05-13 11:25:17 +02:00
cmd . SetCommandAndParameters ( FormatInvariant ( $@"INSERT INTO ""{m_tablename}"" (""Hash"", ""Size"", ""Restored"") SELECT DISTINCT ""Block"".""Hash"", ""Block"".""Size"", 0 AS ""Restored"" FROM ""Block"",""Remotevolume"" WHERE ""Block"".""VolumeID"" = ""Remotevolume"".""ID"" AND ""Remotevolume"".""Name"" = @Name " ));
cmd . SetParameterValue ( "@Name" , volumename );
var blockCount = cmd . ExecuteNonQueryAsync (). Await ();
2025-04-02 23:34:30 +02:00
2013-06-24 20:49:32 +02:00
if ( blockCount == 0 )
2025-03-13 23:06:32 +01:00
throw new Exception ( $"Unexpected empty block volume: {0}" );
2014-11-23 22:07:55 +01:00
2025-05-13 11:25:17 +02:00
cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE UNIQUE INDEX ""{tablename}-Ix"" ON ""{tablename}"" (""Hash"", ""Size"", ""Restored"")" )). Await ();
2013-06-24 20:49:32 +02:00
}
2014-11-23 22:07:55 +01:00
2025-05-08 10:31:38 +02:00
m_insertCommand = m_connection . CreateCommand ( FormatInvariant ( $@"UPDATE ""{tablename}"" SET ""Restored"" = @NewRestoredValue WHERE ""Hash"" = @Hash AND ""Size"" = @Size AND ""Restored"" = @PreviousRestoredValue " ));
2025-05-09 11:12:29 +02:00
m_copyIntoDuplicatedBlocks = m_connection . CreateCommand ( @"INSERT OR IGNORE INTO ""DuplicateBlock"" (""BlockID"", ""VolumeID"") SELECT ""Block"".""ID"", ""Block"".""VolumeID"" FROM ""Block"" WHERE ""Block"".""Hash"" = @Hash AND ""Block"".""Size"" = @Size" );
m_assignBlocksToNewVolume = m_connection . CreateCommand ( @"UPDATE ""Block"" SET ""VolumeID"" = @TargetVolumeId WHERE ""Hash"" = @Hash AND ""Size"" = @Size" );
2025-05-08 10:31:38 +02:00
m_missingBlocksQuery = FormatInvariant ( $@"SELECT ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"" FROM ""{m_tablename}"" WHERE ""{m_tablename}"".""Restored"" = @Restored " );
2013-06-24 20:49:32 +02:00
}
2025-03-10 14:41:12 +01:00
2025-05-08 10:31:38 +02:00
/// <inheritdoc/>
2025-05-13 11:25:17 +02:00
public async Task < bool > SetBlockRestored ( string hash , long size , long targetVolumeId )
2013-06-24 20:49:32 +02:00
{
2025-05-13 11:25:17 +02:00
//m_insertCommand.SetTransaction(m_transaction.Transaction);
m_insertCommand . Transaction = m_transaction . Transaction ;
m_insertCommand . SetParameterValue ( "@NewRestoredValue" , 1 );
m_insertCommand . SetParameterValue ( "@Hash" , hash );
m_insertCommand . SetParameterValue ( "@Size" , size );
m_insertCommand . SetParameterValue ( "@PreviousRestoredValue" , 0 );
var restored = await m_insertCommand . ExecuteNonQueryAsync () == 1 ;
2025-05-09 11:12:29 +02:00
if ( restored )
{
2025-05-13 11:25:17 +02:00
m_copyIntoDuplicatedBlocks . Transaction = m_transaction . Transaction ;
m_copyIntoDuplicatedBlocks . SetParameterValue ( "@Hash" , hash );
m_copyIntoDuplicatedBlocks . SetParameterValue ( "@Size" , size );
await m_copyIntoDuplicatedBlocks . ExecuteNonQueryAsync ();
2025-05-09 11:12:29 +02:00
2025-05-13 11:25:17 +02:00
m_assignBlocksToNewVolume . Transaction = m_transaction . Transaction ;
m_assignBlocksToNewVolume . SetParameterValue ( "@TargetVolumeId" , targetVolumeId );
m_assignBlocksToNewVolume . SetParameterValue ( "@Hash" , hash );
m_assignBlocksToNewVolume . SetParameterValue ( "@Size" , size );
var c = await m_assignBlocksToNewVolume . ExecuteNonQueryAsync ();
2025-05-09 11:12:29 +02:00
if ( c != 1 )
throw new Exception ( $"Unexpected number of updated blocks: {c} != 1" );
}
return restored ;
2013-06-24 20:49:32 +02:00
}
2025-03-10 14:41:12 +01:00
2025-05-08 10:31:38 +02:00
/// <inheritdoc/>
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < BlockWithSourceData > GetSourceFilesWithBlocks ( long blocksize )
2013-06-24 20:49:32 +02:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( FormatInvariant ( $@"SELECT DISTINCT ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"", ""File"".""Path"", ""BlocksetEntry"".""Index"" * {blocksize} FROM ""{m_tablename}"", ""Block"", ""BlocksetEntry"", ""File"" WHERE ""File"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID"" AND ""Block"".""ID"" = ""BlocksetEntry"".""BlockID"" AND ""{m_tablename}"".""Hash"" = ""Block"".""Hash"" AND ""{m_tablename}"".""Size"" = ""Block"".""Size"" AND ""{m_tablename}"".""Restored"" = @Restored ORDER BY ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"", ""File"".""Path"", ""BlocksetEntry"".""Index"" * {blocksize}" ));
cmd . Transaction = m_transaction . Transaction ;
cmd . SetParameterValue ( "@Restored" , 0 );
2025-05-08 10:31:38 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-05-08 10:31:38 +02:00
{
var hash = rd . ConvertValueToString ( 0 ) ?? throw new Exception ( "Hash value was null" );
var size = rd . ConvertValueToInt64 ( 1 );
var file = rd . ConvertValueToString ( 2 ) ?? throw new Exception ( "File value was null" );
var offset = rd . ConvertValueToInt64 ( 3 );
yield return new BlockWithSourceData ( hash , size , file , offset );
}
2013-06-24 20:49:32 +02:00
}
2025-03-10 14:41:12 +01:00
2025-05-08 10:31:38 +02:00
/// <inheritdoc/>
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < BlockWithMetadataSourceData > GetSourceItemsWithMetadataBlocks ()
2013-06-24 20:49:32 +02:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( FormatInvariant ( $@"SELECT DISTINCT ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"", ""File"".""Path"" FROM ""{m_tablename}"", ""Block"", ""BlocksetEntry"", ""Metadataset"", ""File"" WHERE ""File"".""MetadataID"" == ""Metadataset"".""ID"" AND ""Metadataset"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID"" AND ""Block"".""ID"" = ""BlocksetEntry"".""BlockID"" AND ""{m_tablename}"".""Hash"" = ""Block"".""Hash"" AND ""{m_tablename}"".""Size"" = ""Block"".""Size"" AND ""{m_tablename}"".""Restored"" = @Restored ORDER BY ""{m_tablename}"".""Hash"", ""{m_tablename}"".""Size"", ""File"".""Path""" ));
cmd . Transaction = m_transaction . Transaction ;
cmd . SetParameterValue ( "@Restored" , 0 );
2025-04-02 23:34:30 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-05-08 10:31:38 +02:00
{
var hash = rd . ConvertValueToString ( 0 ) ?? throw new Exception ( "Hash value was null" );
var size = rd . ConvertValueToInt64 ( 1 );
var path = rd . ConvertValueToString ( 2 ) ?? throw new Exception ( "File value was null" );
yield return new BlockWithMetadataSourceData ( hash , size , path );
}
}
/// <inheritdoc/>
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < BlocklistHashesEntry > GetBlocklistHashes ( long hashesPerBlock )
2025-05-08 10:31:38 +02:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = m_transaction . Transaction ;
2025-05-09 11:12:29 +02:00
var blocklistTableName = $"BlocklistHashList-{Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray())}" ;
try
{
// We need to create a snapshot as we will be updating the m_tablename table during enumeration
cmd . SetCommandAndParameters ( FormatInvariant ( $@"
CREATE TEMPORARY TABLE ""{blocklistTableName}"" AS
2025-05-13 11:25:17 +02:00
SELECT
b.""Hash"" AS ""BlockHash"",
2025-05-09 11:12:29 +02:00
bs.""Id"" AS ""BlocksetId"",
2025-05-13 11:25:17 +02:00
bs.""Length"" AS ""BlocksetLength"",
2025-05-09 11:12:29 +02:00
bse.""Index"" / @HashesPerBlock AS ""BlocklistHashIndex"",
(
SELECT blh.""Hash""
FROM ""BlocklistHash"" blh
WHERE blh.""BlocksetID"" = bs.""ID""
AND blh.""Index"" == bse.""Index"" / @HashesPerBlock
LIMIT 1
) AS ""BlocklistHashHash"",
bse.""Index"" AS ""BlocksetEntryIndex""
2025-05-13 11:25:17 +02:00
FROM
2025-05-09 11:12:29 +02:00
""BlocksetEntry"" bse
JOIN ""Block"" b ON b.""ID"" = bse.""BlockID""
JOIN ""Blockset"" bs ON bs.""ID"" = bse.""BlocksetID""
2025-05-13 11:25:17 +02:00
WHERE
2025-05-09 11:12:29 +02:00
EXISTS (
SELECT 1
FROM ""BlocklistHash"" blh
JOIN ""{m_tablename}"" mt ON mt.""Hash"" = blh.""Hash""
WHERE blh.""BlocksetID"" = bs.""ID""
AND mt.""Restored"" = @Restored
)
2025-05-13 11:25:17 +02:00
" ));
cmd . SetParameterValue ( "@HashesPerBlock" , hashesPerBlock );
cmd . SetParameterValue ( "@Restored" , 0 );
await cmd . ExecuteNonQueryAsync ();
2025-05-08 10:31:38 +02:00
2025-05-09 11:12:29 +02:00
cmd . SetCommandAndParameters ( FormatInvariant ( $@"
2025-05-13 11:25:17 +02:00
SELECT
""BlockHash"",
""BlocksetId"",
""BlocksetLength"",
""BlocklistHashIndex"",
""BlocklistHashHash"",
""BlocksetEntryIndex""
FROM ""{blocklistTableName}""
2025-05-09 11:12:29 +02:00
ORDER BY ""BlocksetId"", ""BlocklistHashIndex"", ""BlocksetEntryIndex""" ));
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-05-09 11:12:29 +02:00
{
var hash = rd . ConvertValueToString ( 0 ) ?? throw new Exception ( "Block.Hash is null" );
var blocksetId = rd . ConvertValueToInt64 ( 1 );
var length = rd . ConvertValueToInt64 ( 2 );
var blocklistHashIndex = rd . ConvertValueToInt64 ( 3 );
var blocklistHash = rd . ConvertValueToString ( 4 ) ?? throw new Exception ( "BlocklistHash is null" );
var index = rd . ConvertValueToInt64 ( 5 );
yield return new BlocklistHashesEntry ( blocksetId , blocklistHash , length , blocklistHashIndex , ( int ) index , hash );
}
}
finally
2025-05-08 10:31:38 +02:00
{
2025-05-13 11:25:17 +02:00
try { await cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{blocklistTableName}"" " )); }
2025-05-09 11:12:29 +02:00
catch { }
2025-05-08 10:31:38 +02:00
}
}
/// <inheritdoc/>
2025-05-13 11:25:17 +02:00
public async Task < long > GetMissingBlockCount ()
2025-05-08 10:31:38 +02:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( FormatInvariant ( $@"SELECT COUNT(*) FROM ({m_missingBlocksQuery})" ));
cmd . Transaction = m_transaction . Transaction ;
cmd . SetParameterValue ( "@Restored" , 0 );
2025-05-08 10:31:38 +02:00
2025-05-13 11:25:17 +02:00
return await cmd . ExecuteScalarInt64Async ( 0 );
2025-05-08 10:31:38 +02:00
}
/// <summary>
/// Gets the list of missing blocks
/// </summary>
/// <returns>>A list of missing blocks</returns>
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable <( string Hash , long Size )> GetMissingBlocks ()
2025-05-08 10:31:38 +02:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( m_missingBlocksQuery );
cmd . Transaction = m_transaction . Transaction ;
cmd . SetParameterValue ( "@Restored" , 0 );
2025-05-08 10:31:38 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-05-08 10:31:38 +02:00
yield return ( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToInt64 ( 1 ));
}
/// <inheritdoc/>
2025-05-13 11:25:17 +02:00
public async Task < long > MoveBlocksToNewVolume ( long targetVolumeId , long sourceVolumeId )
2025-05-08 10:31:38 +02:00
{
if ( targetVolumeId <= 0 )
throw new ArgumentOutOfRangeException ( nameof ( targetVolumeId ), "Target volume ID must be greater than 0" );
// Move the source blocks into the DuplicateBlock table
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( FormatInvariant ( $@"
2025-05-08 10:31:38 +02:00
INSERT OR IGNORE INTO ""DuplicateBlock"" (""BlockID"", ""VolumeID"")
SELECT b.""ID"", b.""VolumeID""
FROM ""Block"" b
WHERE b.""VolumeID"" = @SourceVolumeId
AND (b.""Hash"", b.""Size"") IN (
SELECT ""Hash"", ""Size""
FROM ""{m_tablename}""
WHERE ""Restored"" = @Restored
)
2025-05-13 11:25:17 +02:00
" ));
cmd . SetParameterValue ( "@SourceVolumeId" , sourceVolumeId );
cmd . SetParameterValue ( "@Restored" , 1 );
2025-05-08 10:31:38 +02:00
2025-05-13 11:25:17 +02:00
var moved = await cmd . ExecuteNonQueryAsync ();
2025-05-08 10:31:38 +02:00
// Then update the blocks table to point to the new volume
cmd . SetCommandAndParameters ( FormatInvariant ( $@"
2025-05-13 11:25:17 +02:00
UPDATE ""Block""
SET ""VolumeID"" = @TargetVolumeId
WHERE ""VolumeID"" = @SourceVolumeId
2025-05-08 10:31:38 +02:00
AND (""Hash"", ""Size"") IN (
2025-05-13 11:25:17 +02:00
SELECT ""Hash"", ""Size""
2025-05-08 10:31:38 +02:00
FROM ""{m_tablename}""
WHERE ""Restored"" = @Restored
)
2025-05-13 11:25:17 +02:00
" ));
cmd . SetParameterValue ( "@TargetVolumeId" , targetVolumeId );
cmd . SetParameterValue ( "@SourceVolumeId" , sourceVolumeId );
cmd . SetParameterValue ( "@Restored" , 1 );
2025-05-08 10:31:38 +02:00
2025-05-13 11:25:17 +02:00
var updated = await cmd . ExecuteNonQueryAsync ();
2025-05-08 10:31:38 +02:00
if ( updated != moved )
throw new Exception ( $"Unexpected number of updated blocks: {updated} != {moved}" );
return updated ;
2013-06-24 20:49:32 +02:00
}
2025-03-10 14:41:12 +01:00
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < IRemoteVolume > GetFilesetsUsingMissingBlocks ()
2013-06-24 20:49:32 +02:00
{
2025-03-14 12:13:17 +01:00
var blocks = FormatInvariant ( $@"SELECT DISTINCT ""FileLookup"".""ID"" AS ID FROM ""{m_tablename}"", ""Block"", ""Blockset"", ""BlocksetEntry"", ""FileLookup"" WHERE ""Block"".""Hash"" = ""{m_tablename}"".""Hash"" AND ""Block"".""Size"" = ""{m_tablename}"".""Size"" AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""BlocksetEntry"".""BlocksetID"" = ""Blockset"".""ID"" AND ""FileLookup"".""BlocksetID"" = ""Blockset"".""ID"" " );
var blocklists = FormatInvariant ( $@"SELECT DISTINCT ""FileLookup"".""ID"" AS ID FROM ""{m_tablename}"", ""Block"", ""Blockset"", ""BlocklistHash"", ""FileLookup"" WHERE ""Block"".""Hash"" = ""{m_tablename}"".""Hash"" AND ""Block"".""Size"" = ""{m_tablename}"".""Size"" AND ""BlocklistHash"".""Hash"" = ""Block"".""Hash"" AND ""BlocklistHash"".""BlocksetID"" = ""Blockset"".""ID"" AND ""FileLookup"".""BlocksetID"" = ""Blockset"".""ID"" " );
2025-04-02 23:34:30 +02:00
var cmdtxt = FormatInvariant ( $@"SELECT DISTINCT ""RemoteVolume"".""Name"", ""RemoteVolume"".""Hash"", ""RemoteVolume"".""Size"" FROM ""RemoteVolume"", ""FilesetEntry"", ""Fileset"" WHERE ""RemoteVolume"".""ID"" = ""Fileset"".""VolumeID"" AND ""Fileset"".""ID"" = ""FilesetEntry"".""FilesetID"" AND ""RemoteVolume"".""Type"" = @Type AND ""FilesetEntry"".""FileID"" IN (SELECT DISTINCT ""ID"" FROM ( {blocks} UNION {blocklists} ))" );
2025-03-10 14:41:12 +01:00
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( cmdtxt );
cmd . Transaction = m_transaction . Transaction ;
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ());
2025-04-02 23:34:30 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-04-03 15:46:20 +02:00
yield return new RemoteVolume ( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToString ( 1 ) ?? "" , rd . ConvertValueToInt64 ( 2 ));
2013-06-24 20:49:32 +02:00
}
2025-03-10 14:41:12 +01:00
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < IRemoteVolume > GetMissingBlockSources ()
2013-06-24 20:49:32 +02:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( FormatInvariant ( $@"SELECT DISTINCT ""RemoteVolume"".""Name"", ""RemoteVolume"".""Hash"", ""RemoteVolume"".""Size"" FROM ""RemoteVolume"", ""Block"", ""{m_tablename}"" WHERE ""{m_tablename}"".""Restored"" = @Restored AND ""Block"".""Hash"" = ""{m_tablename}"".""Hash"" AND ""Block"".""Size"" = ""{m_tablename}"".""Size"" AND ""Block"".""VolumeID"" = ""RemoteVolume"".""ID"" AND ""Remotevolume"".""Name"" != @Name " ));
cmd . Transaction = m_transaction . Transaction ;
cmd . SetParameterValue ( "@Restored" , 0 );
cmd . SetParameterValue ( "@Name" , m_volumename );
2025-04-02 23:34:30 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-04-03 15:46:20 +02:00
yield return new RemoteVolume ( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToString ( 1 ) ?? "" , rd . ConvertValueToInt64 ( 2 ));
2013-06-24 20:49:32 +02:00
}
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
public void Dispose ()
{
2025-05-08 10:31:38 +02:00
if ( m_isDisposed )
return ;
m_isDisposed = true ;
try
2013-06-24 20:49:32 +02:00
{
2025-05-08 10:31:38 +02:00
if ( m_tablename != null )
2025-05-13 11:25:17 +02:00
{
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = m_transaction . Transaction ;
cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{m_tablename}"" " )). Await ();
}
2013-06-24 20:49:32 +02:00
}
2025-05-08 10:31:38 +02:00
catch { }
2025-03-10 14:41:12 +01:00
2025-05-08 10:31:38 +02:00
try { m_insertCommand ?. Dispose (); }
catch { }
2013-06-24 20:49:32 +02:00
}
}
2025-03-10 14:41:12 +01:00
2025-05-08 10:31:38 +02:00
public IMissingBlockList CreateBlockList ( string volumename , ReusableTransaction transaction )
2013-06-24 20:49:32 +02:00
{
return new MissingBlockList ( volumename , m_connection , transaction );
}
2013-07-24 21:25:41 +02:00
2025-05-13 11:25:17 +02:00
public async Task FixDuplicateMetahash ()
2014-10-30 15:44:32 +01:00
{
2025-05-13 11:27:47 +02:00
using var tr = m_connection . BeginTransaction ();
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = tr ;
var sql_count =
@"SELECT COUNT(*) FROM (" +
@" SELECT DISTINCT c1 FROM (" +
@"SELECT COUNT(*) AS ""C1"" FROM (SELECT DISTINCT ""BlocksetID"" FROM ""Metadataset"") UNION SELECT COUNT(*) AS ""C1"" FROM ""Metadataset"" " +
@")" +
@")" ;
var x = await cmd . ExecuteScalarInt64Async ( sql_count , 0 );
if ( x > 1 )
2014-10-30 15:44:32 +01:00
{
2025-05-13 11:27:47 +02:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateMetadataHashes" , "Found duplicate metadatahashes, repairing" );
2014-10-30 15:44:32 +01:00
2025-05-13 11:27:47 +02:00
var tablename = "TmpFile-" + Guid . NewGuid (). ToString ( "N" );
2014-11-23 21:54:38 +01:00
2025-05-13 11:27:47 +02:00
await cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{tablename}"" AS SELECT * FROM ""File""" ));
2014-11-23 21:54:38 +01:00
2025-05-13 11:27:47 +02:00
var sql = @"SELECT ""A"".""ID"", ""B"".""BlocksetID"" FROM (SELECT MIN(""ID"") AS ""ID"", COUNT(""ID"") AS ""Duplicates"" FROM ""Metadataset"" GROUP BY ""BlocksetID"") ""A"", ""Metadataset"" ""B"" WHERE ""A"".""Duplicates"" > 1 AND ""A"".""ID"" = ""B"".""ID""" ;
2014-10-30 15:44:32 +01:00
2025-05-13 11:27:47 +02:00
using ( var c2 = m_connection . CreateCommand ())
{
c2 . Transaction = tr ;
c2 . SetCommandAndParameters ( FormatInvariant ( $@"UPDATE ""{tablename}"" SET ""MetadataID"" = @MetadataId WHERE ""MetadataID"" IN (SELECT ""ID"" FROM ""Metadataset"" WHERE ""BlocksetID"" = @BlocksetId)" )
+ @"; DELETE FROM ""Metadataset"" WHERE ""BlocksetID"" = @BlocksetId AND ""ID"" != @MetadataId" );
using ( var rd = await cmd . ExecuteReaderAsync ( sql ))
while ( await rd . ReadAsync ())
{
c2 . SetParameterValue ( "@MetadataId" , rd . GetValue ( 0 ));
c2 . SetParameterValue ( "@BlocksetId" , rd . GetValue ( 1 ));
await c2 . ExecuteNonQueryAsync ();
}
}
2014-10-30 15:44:32 +01:00
2025-05-13 11:27:47 +02:00
sql = FormatInvariant ( $@"SELECT ""ID"", ""Path"", ""BlocksetID"", ""MetadataID"", ""Entries"" FROM (
2025-05-13 11:25:17 +02:00
SELECT MIN(""ID"") AS ""ID"", ""Path"", ""BlocksetID"", ""MetadataID"", COUNT(*) as ""Entries"" FROM ""{tablename}"" GROUP BY ""Path"", ""BlocksetID"", ""MetadataID"")
2025-03-14 12:13:17 +01:00
WHERE ""Entries"" > 1 ORDER BY ""ID""" );
2014-11-23 21:54:38 +01:00
2025-05-13 11:27:47 +02:00
using ( var c2 = m_connection . CreateCommand ())
{
c2 . Transaction = tr ;
c2 . SetCommandAndParameters ( FormatInvariant ( $@"UPDATE ""FilesetEntry"" SET ""FileID"" = @FileId WHERE ""FileID"" IN (SELECT ""ID"" FROM ""{tablename}"" WHERE ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetadataId)" )
+ FormatInvariant ( $@"; DELETE FROM ""{tablename}"" WHERE ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetadataId AND ""ID"" != @FileId" ));
2025-04-02 23:34:30 +02:00
2025-05-13 11:27:47 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ( sql ))
{
c2 . SetParameterValue ( "@FileId" , rd . GetValue ( 0 ));
c2 . SetParameterValue ( "@Path" , rd . GetValue ( 1 ));
c2 . SetParameterValue ( "@BlocksetId" , rd . GetValue ( 2 ));
c2 . SetParameterValue ( "@MetadataId" , rd . GetValue ( 3 ));
await c2 . ExecuteNonQueryAsync ();
2014-11-23 21:54:38 +01:00
}
2025-05-13 11:27:47 +02:00
}
2014-11-23 21:54:38 +01:00
2025-05-13 11:27:47 +02:00
await cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DELETE FROM ""FileLookup"" WHERE ""ID"" NOT IN (SELECT ""ID"" FROM ""{tablename}"") " ));
await cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE INDEX ""{tablename}-Ix"" ON ""{tablename}"" (""ID"", ""MetadataID"")" ));
await cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"UPDATE ""FileLookup"" SET ""MetadataID"" = (SELECT ""MetadataID"" FROM ""{tablename}"" A WHERE ""A"".""ID"" = ""FileLookup"".""ID"") " ));
await cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DROP TABLE ""{tablename}"" " ));
2014-11-23 21:54:38 +01:00
2025-05-13 11:27:47 +02:00
x = await cmd . ExecuteScalarInt64Async ( sql_count , 0 );
if ( x > 1 )
throw new Interface . UserInformationException ( "Repair failed, there are still duplicate metadatahashes!" , "DuplicateHashesRepairFailed" );
2014-10-30 15:44:32 +01:00
2025-05-13 11:27:47 +02:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateMetadataHashesFixed" , "Duplicate metadatahashes repaired succesfully" );
await tr . CommitAsync ();
2014-10-30 15:44:32 +01:00
}
}
2025-05-13 11:25:17 +02:00
public async Task FixDuplicateFileentries ()
2014-10-30 15:44:32 +01:00
{
2025-05-13 11:27:47 +02:00
using var tr = m_connection . BeginTransaction ();
using var cmd = m_connection . CreateCommand ();
2014-10-30 15:44:32 +01:00
2025-05-13 11:27:47 +02:00
cmd . Transaction = tr ;
var sql_count = @"SELECT COUNT(*) FROM (SELECT ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"", COUNT(*) as ""Duplicates"" FROM ""FileLookup"" GROUP BY ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"") WHERE ""Duplicates"" > 1" ;
var x = await cmd . ExecuteScalarInt64Async ( sql_count , 0 );
if ( x > 0 )
{
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateFileEntries" , "Found duplicate file entries, repairing" );
2014-10-30 15:44:32 +01:00
2025-05-13 11:27:47 +02:00
var sql = @"SELECT ""ID"", ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"", ""Entries"" FROM (
2025-05-13 11:25:17 +02:00
SELECT MIN(""ID"") AS ""ID"", ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"", COUNT(*) as ""Entries"" FROM ""FileLookup"" GROUP BY ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"")
2014-10-30 15:44:32 +01:00
WHERE ""Entries"" > 1 ORDER BY ""ID""" ;
2025-05-13 11:27:47 +02:00
using ( var c2 = m_connection . CreateCommand ())
{
c2 . Transaction = tr ;
c2 . SetCommandAndParameters ( @"UPDATE ""FilesetEntry"" SET ""FileID"" = @FileId WHERE ""FileID"" IN (SELECT ""ID"" FROM ""FileLookup"" WHERE ""PrefixID"" = @PrefixId AND ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetatadataId)"
+ @"; DELETE FROM ""FileLookup"" WHERE ""PrefixID"" = @PrefixId AND ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetadataId AND ""ID"" != @FileId" );
cmd . SetCommandAndParameters ( sql );
2025-04-02 23:34:30 +02:00
2025-05-13 11:27:47 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
{
c2 . SetParameterValue ( "@FileId" , rd . GetValue ( 0 ));
c2 . SetParameterValue ( "@PrefixId" , rd . GetValue ( 1 ));
c2 . SetParameterValue ( "@Path" , rd . GetValue ( 2 ));
c2 . SetParameterValue ( "@BlocksetId" , rd . GetValue ( 3 ));
c2 . SetParameterValue ( "@MetadataId" , rd . GetValue ( 4 ));
await c2 . ExecuteNonQueryAsync ();
2014-10-30 15:44:32 +01:00
}
2025-05-13 11:27:47 +02:00
}
2014-10-30 15:44:32 +01:00
2025-05-13 11:27:47 +02:00
x = await cmd . ExecuteScalarInt64Async ( sql_count , 0 );
if ( x > 1 )
throw new Interface . UserInformationException ( "Repair failed, there are still duplicate file entries!" , "DuplicateFilesRepairFailed" );
2014-10-30 15:44:32 +01:00
2025-05-13 11:27:47 +02:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateFileEntriesFixed" , "Duplicate file entries repaired succesfully" );
await tr . CommitAsync ();
2014-10-30 15:44:32 +01:00
}
}
2015-01-12 23:07:57 +01:00
2025-05-13 11:25:17 +02:00
public async Task FixMissingBlocklistHashes ( string blockhashalgorithm , long blocksize )
2015-02-03 23:49:46 +01:00
{
var blocklistbuffer = new byte [ blocksize ];
2025-05-13 11:27:47 +02:00
using var tr = m_connection . BeginTransaction ();
using var cmd = m_connection . CreateCommand ();
using var blockhasher = HashFactory . CreateHasher ( blockhashalgorithm );
2021-04-04 11:17:13 -07:00
2025-05-13 11:27:47 +02:00
cmd . Transaction = tr ;
var hashsize = blockhasher . HashSize / 8 ;
2021-04-04 11:17:13 -07:00
2025-05-13 11:27:47 +02:00
var sql = FormatInvariant ( $@"SELECT * FROM (SELECT ""N"".""BlocksetID"", ((""N"".""BlockCount"" + {blocksize / hashsize} - 1) / {blocksize / hashsize}) AS ""BlocklistHashCountExpected"", CASE WHEN ""G"".""BlocklistHashCount"" IS NULL THEN 0 ELSE ""G"".""BlocklistHashCount"" END AS ""BlocklistHashCountActual"" FROM (SELECT ""BlocksetID"", COUNT(*) AS ""BlockCount"" FROM ""BlocksetEntry"" GROUP BY ""BlocksetID"") ""N"" LEFT OUTER JOIN (SELECT ""BlocksetID"", COUNT(*) AS ""BlocklistHashCount"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"") ""G"" ON ""N"".""BlocksetID"" = ""G"".""BlocksetID"" WHERE ""N"".""BlockCount"" > 1) WHERE ""BlocklistHashCountExpected"" != ""BlocklistHashCountActual""" );
var countsql = @"SELECT COUNT(*) FROM (" + sql + @")" ;
var itemswithnoblocklisthash = await cmd . ExecuteScalarInt64Async ( countsql , 0 );
if ( itemswithnoblocklisthash != 0 )
{
Logging . Log . WriteInformationMessage ( LOGTAG , "MissingBlocklistHashes" , "Found {0} missing blocklisthash entries, repairing" , itemswithnoblocklisthash );
using ( var c2 = m_connection . CreateCommand ())
using ( var c3 = m_connection . CreateCommand ())
using ( var c4 = m_connection . CreateCommand ())
using ( var c5 = m_connection . CreateCommand ())
using ( var c6 = m_connection . CreateCommand ())
2015-02-03 23:49:46 +01:00
{
2025-05-13 11:27:47 +02:00
c2 . Transaction = tr ;
c3 . Transaction = tr ;
c4 . Transaction = tr ;
c5 . Transaction = tr ;
c6 . Transaction = tr ;
c3 . SetCommandAndParameters ( @"INSERT INTO ""BlocklistHash"" (""BlocksetID"", ""Index"", ""Hash"") VALUES (@BlocksetId, @Index, @Hash) " );
c4 . SetCommandAndParameters ( @"SELECT ""ID"" FROM ""Block"" WHERE ""Hash"" = @Hash AND ""Size"" = @Size" );
c5 . SetCommandAndParameters ( @"SELECT ""ID"" FROM ""DeletedBlock"" WHERE ""Hash"" = @Hash AND ""Size"" = @Size AND ""VolumeID"" IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND (""State"" IN (@State1, @State2)))" );
c6 . SetCommandAndParameters ( @"INSERT INTO ""Block"" (""Hash"", ""Size"", ""VolumeID"") SELECT ""Hash"", ""Size"", ""VolumeID"" FROM ""DeletedBlock"" WHERE ""ID"" = @DeletedBlockId LIMIT 1; DELETE FROM ""DeletedBlock"" WHERE ""ID"" = @DeletedBlockId;" );
await foreach ( var e in cmd . ExecuteReaderEnumerableAsync ( sql ))
2015-02-03 23:49:46 +01:00
{
2025-05-13 11:27:47 +02:00
var blocksetid = e . ConvertValueToInt64 ( 0 );
var ix = 0L ;
int blocklistoffset = 0 ;
2015-02-03 23:49:46 +01:00
2025-05-13 11:27:47 +02:00
c2 . SetCommandAndParameters ( @"DELETE FROM ""BlocklistHash"" WHERE ""BlocksetID"" = @BlocksetId" );
c2 . SetParameterValue ( "@BlocksetId" , blocksetid );
await c2 . ExecuteNonQueryAsync ();
2025-04-02 23:34:30 +02:00
2025-05-13 11:27:47 +02:00
c2 . SetCommandAndParameters ( @"SELECT ""A"".""Hash"" FROM ""Block"" ""A"", ""BlocksetEntry"" ""B"" WHERE ""A"".""ID"" = ""B"".""BlockID"" AND ""B"".""BlocksetID"" = @BlocksetId ORDER BY ""B"".""Index""" );
c2 . SetParameterValue ( "@BlocksetId" , blocksetid );
2015-02-03 23:49:46 +01:00
2025-05-13 11:27:47 +02:00
await foreach ( var h in c2 . ExecuteReaderEnumerableAsync ())
{
var tmp = Convert . FromBase64String ( h . ConvertValueToString ( 0 ) ?? throw new Exception ( "Hash value was null" ));
if ( blocklistbuffer . Length - blocklistoffset < tmp . Length )
2015-02-03 23:49:46 +01:00
{
2025-05-13 11:27:47 +02:00
var blkey = Convert . ToBase64String ( blockhasher . ComputeHash ( blocklistbuffer , 0 , blocklistoffset ));
2015-02-03 23:49:46 +01:00
2025-05-13 11:27:47 +02:00
// Check if the block exists in "blocks"
c4 . SetParameterValue ( "@Hash" , blkey );
2025-05-13 11:25:17 +02:00
c4 . SetParameterValue ( "@Size" , blocklistoffset );
var existingBlockId = await c4 . ExecuteScalarInt64Async (- 1 );
2025-04-03 16:38:51 +02:00
if ( existingBlockId <= 0 )
2015-02-04 21:52:49 +01:00
{
2025-05-13 11:27:47 +02:00
c5 . SetParameterValue ( "@Hash" , blkey );
2025-05-13 11:25:17 +02:00
c5 . SetParameterValue ( "@Size" , blocklistoffset );
c5 . SetParameterValue ( "@Type" , RemoteVolumeType . Blocks . ToString ());
c5 . SetParameterValue ( "@State1" , RemoteVolumeState . Uploaded . ToString ());
c5 . SetParameterValue ( "@State2" , RemoteVolumeState . Verified . ToString ());
var deletedBlockId = await c5 . ExecuteScalarInt64Async (- 1 );
2025-04-03 13:58:06 +02:00
2025-05-13 11:27:47 +02:00
if ( deletedBlockId <= 0 )
throw new Exception ( $"Missing block for blocklisthash: {blkey}" );
2015-02-04 21:52:49 +01:00
else
{
2025-05-13 11:25:17 +02:00
c6 . SetParameterValue ( "@DeletedBlockId" , deletedBlockId );
var rc = await c6 . ExecuteNonQueryAsync ();
2025-05-13 11:27:47 +02:00
2015-02-04 21:52:49 +01:00
if ( rc != 2 )
2025-03-13 23:06:32 +01:00
throw new Exception ( $"Unexpected update count: {rc}" );
2015-02-04 21:52:49 +01:00
}
}
2015-02-03 23:49:46 +01:00
// Add to table
2025-05-13 11:25:17 +02:00
c3 . SetParameterValue ( "@BlocksetId" , blocksetid );
c3 . SetParameterValue ( "@Index" , ix );
2025-05-13 11:27:47 +02:00
c3 . SetParameterValue ( "@Hash" , blkey );
2025-05-13 11:25:17 +02:00
await c3 . ExecuteNonQueryAsync ();
2025-05-13 11:27:47 +02:00
ix ++;
blocklistoffset = 0 ;
2015-02-03 23:49:46 +01:00
}
2025-05-13 11:27:47 +02:00
Array . Copy ( tmp , 0 , blocklistbuffer , blocklistoffset , tmp . Length );
blocklistoffset += tmp . Length ;
2015-02-03 23:49:46 +01:00
}
2025-05-13 11:27:47 +02:00
if ( blocklistoffset != 0 )
{
var blkeyfinal = Convert . ToBase64String ( blockhasher . ComputeHash ( blocklistbuffer , 0 , blocklistoffset ));
2015-02-03 23:49:46 +01:00
2025-05-13 11:27:47 +02:00
// Ensure that the block exists in "blocks"
c4 . SetParameterValue ( "@Hash" , blkeyfinal );
c4 . SetParameterValue ( "@Size" , blocklistoffset );
var existingBlockId = await c4 . ExecuteScalarInt64Async (- 1 );
2015-02-03 23:49:46 +01:00
2025-05-13 11:27:47 +02:00
if ( existingBlockId <= 0 )
{
c5 . SetParameterValue ( "@Hash" , blkeyfinal );
c5 . SetParameterValue ( "@Size" , blocklistoffset );
c5 . SetParameterValue ( "@Type" , RemoteVolumeType . Blocks . ToString ());
c5 . SetParameterValue ( "@State1" , RemoteVolumeState . Uploaded . ToString ());
c5 . SetParameterValue ( "@State2" , RemoteVolumeState . Verified . ToString ());
var deletedBlockId = await c5 . ExecuteScalarInt64Async (- 1 );
if ( deletedBlockId == 0 )
throw new Exception ( $"Missing block for blocklisthash: {blkeyfinal}" );
else
{
c6 . SetParameterValue ( "@DeletedBlockId" , deletedBlockId );
var rc = await c6 . ExecuteNonQueryAsync ();
if ( rc != 2 )
throw new Exception ( $"Unexpected update count: {rc}" );
}
}
// Add to table
c3 . SetParameterValue ( "@BlocksetId" , blocksetid );
c3 . SetParameterValue ( "@Index" , ix );
c3 . SetParameterValue ( "@Hash" , blkeyfinal );
await c3 . ExecuteNonQueryAsync ();
}
}
2015-02-03 23:49:46 +01:00
}
2025-05-13 11:27:47 +02:00
itemswithnoblocklisthash = cmd . ExecuteScalarInt64 ( countsql , 0 );
if ( itemswithnoblocklisthash != 0 )
throw new Interface . UserInformationException ( $"Failed to repair, after repair {itemswithnoblocklisthash} blocklisthashes were missing" , "MissingBlocklistHashesRepairFailed" );
Logging . Log . WriteInformationMessage ( LOGTAG , "MissingBlocklisthashesRepaired" , "Missing blocklisthashes repaired succesfully" );
tr . Commit ();
2015-02-03 23:49:46 +01:00
}
}
2025-05-13 11:25:17 +02:00
public async Task FixDuplicateBlocklistHashes ( long blocksize , long hashsize )
2015-01-12 23:07:57 +01:00
{
2025-05-13 11:27:47 +02:00
using var tr = m_connection . BeginTransaction ();
using var cmd = m_connection . CreateCommand ();
2015-01-12 23:07:57 +01:00
2025-05-13 11:27:47 +02:00
cmd . Transaction = tr ;
var dup_sql = @"SELECT * FROM (SELECT ""BlocksetID"", ""Index"", COUNT(*) AS ""EC"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"", ""Index"") WHERE ""EC"" > 1" ;
2015-01-12 23:07:57 +01:00
2025-05-13 11:27:47 +02:00
var sql_count = @"SELECT COUNT(*) FROM (" + dup_sql + ")" ;
2015-01-12 23:07:57 +01:00
2025-05-13 11:27:47 +02:00
var x = await cmd . ExecuteScalarInt64Async ( sql_count , 0 );
if ( x > 0 )
{
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateBlocklistHashes" , "Found duplicate blocklisthash entries, repairing" );
var unique_count = await cmd . ExecuteScalarInt64Async ( @"SELECT COUNT(*) FROM (SELECT DISTINCT ""BlocksetID"", ""Index"" FROM ""BlocklistHash"")" , 0 );
2015-01-12 23:07:57 +01:00
2025-05-13 11:27:47 +02:00
using ( var c2 = m_connection . CreateCommand ())
{
c2 . Transaction = tr ;
c2 . SetCommandAndParameters ( @"DELETE FROM ""BlocklistHash"" WHERE rowid IN (SELECT rowid FROM ""BlocklistHash"" WHERE ""BlocksetID"" = @BlocksetId AND ""Index"" = @Index LIMIT @Limit)" );
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ( dup_sql ))
2015-01-12 23:07:57 +01:00
{
2025-05-13 11:27:47 +02:00
var expected = rd . GetInt32 ( 2 ) - 1 ;
c2 . SetParameterValue ( "@BlocksetId" , rd . GetValue ( 0 ));
c2 . SetParameterValue ( "@Index" , rd . GetValue ( 1 ));
c2 . SetParameterValue ( "@Limit" , expected );
var actual = await c2 . ExecuteNonQueryAsync ();
if ( actual != expected )
throw new Exception ( $"Unexpected number of results after fix, got: {actual}, expected: {expected}" );
2015-01-12 23:07:57 +01:00
}
2025-05-13 11:27:47 +02:00
}
2015-01-12 23:07:57 +01:00
2025-05-13 11:27:47 +02:00
x = await cmd . ExecuteScalarInt64Async ( sql_count );
if ( x > 1 )
throw new Exception ( "Repair failed, there are still duplicate file entries!" );
2015-01-12 23:07:57 +01:00
2025-05-13 11:27:47 +02:00
var real_count = await cmd . ExecuteScalarInt64Async ( @"SELECT Count(*) FROM ""BlocklistHash""" , 0 );
2015-01-12 23:07:57 +01:00
2025-05-13 11:27:47 +02:00
if ( real_count != unique_count )
throw new Interface . UserInformationException ( $"Failed to repair, result should have been {unique_count} blocklist hashes, but result was {real_count} blocklist hashes" , "DuplicateBlocklistHashesRepairFailed" );
2015-11-28 21:53:46 +01:00
2025-05-13 11:27:47 +02:00
try
{
await VerifyConsistency ( blocksize , hashsize , true , tr );
2015-01-12 23:07:57 +01:00
}
2025-05-13 11:27:47 +02:00
catch ( Exception ex )
{
throw new Interface . UserInformationException ( "Repaired blocklisthashes, but the database was broken afterwards, rolled back changes" , "DuplicateBlocklistHashesRepairFailed" , ex );
}
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateBlocklistHashesRepaired" , "Duplicate blocklisthashes repaired succesfully" );
await tr . CommitAsync ();
2015-01-12 23:07:57 +01:00
}
}
2016-03-17 21:51:42 +01:00
2025-05-13 11:25:17 +02:00
public async Task CheckAllBlocksAreInVolume ( string filename , IEnumerable < KeyValuePair < string , long >> blocks , SqliteTransaction transaction )
2016-03-17 21:51:42 +01:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
2025-05-10 09:55:13 +02:00
var tablename = "ProbeBlocks-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
try
2016-03-17 21:51:42 +01:00
{
2025-05-13 11:25:17 +02:00
await cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"CREATE TEMPORARY TABLE ""{tablename}"" (""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL)" ));
2025-04-03 16:38:51 +02:00
cmd . SetCommandAndParameters ( FormatInvariant ( $@"INSERT INTO ""{tablename}"" (""Hash"", ""Size"") VALUES (@Hash, @Size)" ));
2016-03-17 21:51:42 +01:00
2025-03-10 14:41:12 +01:00
foreach ( var kp in blocks )
2016-03-17 21:51:42 +01:00
{
2025-04-02 23:34:30 +02:00
cmd . SetParameterValue ( "@Hash" , kp . Key );
cmd . SetParameterValue ( "@Size" , kp . Value );
2025-05-13 11:25:17 +02:00
await cmd . ExecuteNonQueryAsync ();
2016-03-17 21:51:42 +01:00
}
2025-05-13 11:25:17 +02:00
cmd . SetCommandAndParameters ( @"SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" = @Name" );
cmd . SetParameterValue ( "@Name" , filename );
var id = await cmd . ExecuteScalarInt64Async (- 1 );
2025-04-02 23:34:30 +02:00
2025-05-13 11:25:17 +02:00
cmd . SetCommandAndParameters ( FormatInvariant ( $@"SELECT COUNT(*) FROM (SELECT ""A"".""VolumeID"" FROM ""{tablename}"" B LEFT OUTER JOIN ""Block"" A ON ""A"".""Hash"" = ""B"".""Hash"" AND ""A"".""Size"" = ""B"".""Size"") WHERE ""VolumeID"" != @VolumeId " ));
cmd . SetParameterValue ( "@VolumeId" , id );
var aliens = await cmd . ExecuteScalarInt64Async ( 0 );
2016-03-17 21:51:42 +01:00
if ( aliens != 0 )
2025-03-13 23:06:32 +01:00
throw new Exception ( $"Not all blocks were found in {filename}" );
2016-03-17 21:51:42 +01:00
}
2025-05-10 09:55:13 +02:00
finally
{
2025-05-13 11:25:17 +02:00
await cmd . ExecuteNonQueryAsync ( FormatInvariant ( $@"DROP TABLE IF EXISTS ""{tablename}"" " ));
2025-05-10 09:55:13 +02:00
}
2016-03-17 21:51:42 +01:00
}
2025-05-13 11:25:17 +02:00
public async Task CheckBlocklistCorrect ( string hash , long length , IEnumerable < string > blocklist , long blocksize , long blockhashlength , SqliteTransaction transaction )
2016-03-17 21:51:42 +01:00
{
2025-05-13 11:27:47 +02:00
using var cmd = m_connection . CreateCommand ();
cmd . Transaction = transaction ;
var query = FormatInvariant ( $@"
2025-05-13 11:25:17 +02:00
SELECT
""C"".""Hash"",
2016-03-17 21:51:42 +01:00
""C"".""Size""
2025-05-13 11:25:17 +02:00
FROM
""BlocksetEntry"" A,
2016-03-17 21:51:42 +01:00
(
2025-05-13 11:25:17 +02:00
SELECT
2016-03-17 21:51:42 +01:00
""Y"".""BlocksetID"",
""Y"".""Hash"" AS ""BlocklistHash"",
""Y"".""Index"" AS ""BlocklistHashIndex"",
""Z"".""Size"" AS ""BlocklistSize"",
2025-05-13 11:25:17 +02:00
""Z"".""ID"" AS ""BlocklistHashBlockID""
FROM
2016-03-17 21:51:42 +01:00
""BlocklistHash"" Y,
2025-05-13 11:25:17 +02:00
""Block"" Z
WHERE
2025-04-02 23:34:30 +02:00
""Y"".""Hash"" = ""Z"".""Hash"" AND ""Y"".""Hash"" = @Hash AND ""Z"".""Size"" = @Size
2016-03-17 21:51:42 +01:00
LIMIT 1
) B,
""Block"" C
2025-05-13 11:25:17 +02:00
WHERE
""A"".""BlocksetID"" = ""B"".""BlocksetID""
AND
2016-03-17 21:51:42 +01:00
""A"".""BlockID"" = ""C"".""ID""
AND
2025-03-14 12:13:17 +01:00
""A"".""Index"" >= ""B"".""BlocklistHashIndex"" * ({blocksize} / {blockhashlength})
2016-03-17 21:51:42 +01:00
AND
2025-03-14 12:13:17 +01:00
""A"".""Index"" < (""B"".""BlocklistHashIndex"" + 1) * ({blocksize} / {blockhashlength})
2025-05-13 11:25:17 +02:00
ORDER BY
2016-03-17 21:51:42 +01:00
""A"".""Index""
2025-03-14 12:13:17 +01:00
" );
2016-03-17 21:51:42 +01:00
2025-05-13 11:27:47 +02:00
using var en = blocklist . GetEnumerator ();
2016-03-17 21:51:42 +01:00
2025-05-13 11:27:47 +02:00
cmd . SetCommandAndParameters ( query );
cmd . SetParameterValue ( "@Hash" , hash );
cmd . SetParameterValue ( "@Size" , length );
await foreach ( var r in cmd . ExecuteReaderEnumerableAsync ())
{
if (! en . MoveNext ())
throw new Exception ( $"Too few entries in source blocklist with hash {hash}" );
if ( en . Current != r . ConvertValueToString ( 0 ))
throw new Exception ( $"Mismatch in blocklist with hash {hash}" );
2016-03-17 21:51:42 +01:00
}
2025-05-13 11:27:47 +02:00
if ( en . MoveNext ())
throw new Exception ( $"Too many source blocklist entries in {hash}" );
2016-03-17 21:51:42 +01:00
}
2025-03-10 14:41:12 +01:00
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < string > MissingLocalFilesets ( SqliteTransaction transaction )
2025-03-10 14:41:12 +01:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT ""Name"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" NOT IN (@States) AND ""ID"" NOT IN (SELECT ""VolumeID"" FROM ""Fileset"")" );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ());
cmd . ExpandInClauseParameter ( "@States" , [ RemoteVolumeState . Deleting . ToString (), RemoteVolumeState . Deleted . ToString ()]);
2025-04-02 23:34:30 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-04-03 15:46:20 +02:00
yield return rd . ConvertValueToString ( 0 ) ?? "" ;
2025-03-10 14:41:12 +01:00
}
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable <( long FilesetID , DateTime Timestamp , bool IsFull )> MissingRemoteFilesets ( SqliteTransaction transaction )
2025-03-10 14:41:12 +01:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT ""ID"", ""Timestamp"", ""IsFullBackup"" FROM ""Fileset"" WHERE ""VolumeID"" NOT IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" NOT IN (@States))" );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Files . ToString ());
cmd . ExpandInClauseParameter ( "@States" , [ RemoteVolumeState . Deleting . ToString (), RemoteVolumeState . Deleted . ToString ()]);
2025-04-02 23:34:30 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-04-02 23:34:30 +02:00
yield return ( rd . ConvertValueToInt64 ( 0 ), ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 )), rd . ConvertValueToInt64 ( 2 ) == BackupType . FULL_BACKUP );
2025-03-10 14:41:12 +01:00
}
2025-03-20 22:21:57 +01:00
2025-05-13 11:25:17 +02:00
public async IAsyncEnumerable < IRemoteVolume > EmptyIndexFiles ( SqliteTransaction transaction )
2025-03-20 22:21:57 +01:00
{
2025-05-13 11:25:17 +02:00
using var cmd = m_connection . CreateCommand ( @"SELECT ""Name"", ""Hash"", ""Size"" FROM ""RemoteVolume"" WHERE ""Type"" = @Type AND ""State"" IN (@States) AND ""ID"" NOT IN (SELECT ""IndexVolumeId"" FROM ""IndexBlockLink"")" );
cmd . Transaction = transaction ;
cmd . SetParameterValue ( "@Type" , RemoteVolumeType . Index . ToString ());
cmd . ExpandInClauseParameter ( "@States" , [ RemoteVolumeState . Uploaded . ToString (), RemoteVolumeState . Verified . ToString ()]);
2025-04-02 23:34:30 +02:00
2025-05-13 11:25:17 +02:00
await foreach ( var rd in cmd . ExecuteReaderEnumerableAsync ())
2025-04-03 15:46:20 +02:00
yield return new RemoteVolume ( rd . ConvertValueToString ( 0 ) ?? "" , rd . ConvertValueToString ( 1 ) ?? "" , rd . ConvertValueToInt64 ( 2 ));
2025-03-20 22:21:57 +01:00
}
2016-09-15 11:39:27 +02:00
}
2013-03-30 15:23:09 +01:00
}