2025-03-13 23:06:32 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-05-21 09:05:15 +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-13 23:06:32 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-05-21 09:05:15 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-03-13 23:06:32 +01:00
// all copies or substantial portions of the Software.
2025-05-21 09:05:15 +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-06-26 21:59:01 +02:00
2025-04-03 15:46:20 +02:00
#nullable enable
2013-06-26 21:59:01 +02:00
using System ;
2013-07-30 22:42:59 +02:00
using System.Linq ;
2025-05-21 09:05:34 +02:00
using System.Threading.Tasks ;
2013-06-26 21:59:01 +02:00
using System.Collections.Generic ;
2025-05-21 09:11:25 +02:00
using Microsoft.Data.Sqlite ;
2025-05-21 09:22:29 +02:00
using Duplicati.Library.Utility ;
2013-06-26 21:59:01 +02:00
namespace Duplicati.Library.Main.Database
{
2013-08-22 20:52:54 +02:00
internal class LocalTestDatabase : LocalDatabase
2013-06-26 21:59:01 +02:00
{
2025-05-21 09:05:34 +02:00
public static async Task < LocalTestDatabase > CreateAsync ( string path , long pagecachesize , LocalTestDatabase ? dbnew = null )
2024-11-01 14:50:26 +01:00
{
2025-05-21 09:05:34 +02:00
dbnew ??= new LocalTestDatabase ();
dbnew = ( LocalTestDatabase ) await CreateLocalDatabaseAsync ( path , "Test" , true , pagecachesize , dbnew );
dbnew . ShouldCloseConnection = true ;
return dbnew ;
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:05:34 +02:00
public static async Task < LocalTestDatabase > CreateAsync ( LocalDatabase dbparent , LocalTestDatabase ? dbnew = null )
2024-11-01 14:50:26 +01:00
{
2025-05-21 09:05:34 +02:00
dbnew ??= new LocalTestDatabase ();
return ( LocalTestDatabase ) await CreateLocalDatabaseAsync ( dbparent , dbnew );
2013-08-24 20:59:09 +02:00
}
2024-11-01 14:50:26 +01:00
2025-05-21 14:51:47 +02:00
public async Task UpdateVerificationCount ( string name )
2013-07-30 22:42:59 +02:00
{
2025-05-21 09:11:25 +02:00
using ( var cmd = m_connection . CreateCommand ( m_rtr ))
2025-05-21 14:51:47 +02:00
await cmd . SetCommandAndParameters ( @"
2025-05-21 09:40:32 +02:00
UPDATE ""RemoteVolume""
SET ""VerificationCount"" = MAX(1,
CASE
WHEN ""VerificationCount"" <= 0
THEN (
SELECT MAX(""VerificationCount"")
FROM ""RemoteVolume""
)
ELSE ""VerificationCount"" + 1
END
)
WHERE ""Name"" = @Name
" )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@Name" , name )
2025-05-21 14:51:47 +02:00
. ExecuteNonQueryAsync ();
2013-07-30 22:42:59 +02:00
}
2024-11-01 14:50:26 +01:00
2025-04-03 15:46:20 +02:00
private record RemoteVolume : IRemoteVolume
2013-06-26 21:59:01 +02:00
{
2025-04-03 15:46:20 +02:00
public long ID { get ; init ; }
public string Name { get ; init ; }
public long Size { get ; init ; }
public string Hash { get ; init ; }
public long VerificationCount { get ; init ; }
2024-11-01 14:50:26 +01:00
2025-05-21 09:11:25 +02:00
public RemoteVolume ( SqliteDataReader rd )
2013-06-26 21:59:01 +02:00
{
2025-04-03 15:46:20 +02:00
ID = rd . ConvertValueToInt64 ( 0 );
Name = rd . ConvertValueToString ( 1 ) ?? "" ;
2025-03-14 14:34:56 +01:00
Size = rd . ConvertValueToInt64 ( 2 );
2025-04-03 15:46:20 +02:00
Hash = rd . ConvertValueToString ( 3 ) ?? throw new ArgumentNullException ( "Hash cannot be null" );
2025-03-14 14:34:56 +01:00
VerificationCount = rd . ConvertValueToInt64 ( 4 );
2013-06-26 21:59:01 +02:00
}
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:22:29 +02:00
private static List < RemoteVolume > FilterByVerificationCount ( IEnumerable < RemoteVolume > volumes , long samples , long maxverification )
2013-07-30 22:42:59 +02:00
{
var rnd = new Random ();
2025-05-21 09:05:34 +02:00
// First round is the new items
2013-07-30 22:42:59 +02:00
var res = ( from n in volumes where n . VerificationCount == 0 select n ). ToList ();
while ( res . Count > samples )
res . RemoveAt ( rnd . Next ( 0 , res . Count ));
2024-11-01 14:50:26 +01:00
2013-07-30 22:42:59 +02:00
// Quick exit if we are done
if ( res . Count == samples )
return res ;
// Next is the volumes that are not
// verified as much, with preference for low verification count
var starved = ( from n in volumes where n . VerificationCount != 0 && n . VerificationCount < maxverification orderby n . VerificationCount select n );
if ( starved . Any ())
{
var max = starved . Select ( x => x . VerificationCount ). Max ();
var min = starved . Select ( x => x . VerificationCount ). Min ();
2024-11-01 14:50:26 +01:00
for ( var i = min ; i <= max ; i ++)
2013-07-30 22:42:59 +02:00
{
var p = starved . Where ( x => x . VerificationCount == i ). ToList ();
while ( res . Count < samples && p . Count > 0 )
{
var n = rnd . Next ( 0 , p . Count );
res . Add ( p [ n ]);
p . RemoveAt ( n );
}
}
2024-11-01 14:50:26 +01:00
2013-07-30 22:42:59 +02:00
// Quick exit if we are done
if ( res . Count == samples )
return res ;
}
2024-11-01 14:50:26 +01:00
2013-07-30 22:42:59 +02:00
if ( maxverification > 0 )
{
// Last is the items that are verified mostly
var remainder = ( from n in volumes where n . VerificationCount >= maxverification select n ). ToList ();
while ( res . Count < samples && remainder . Count > 0 )
{
var n = rnd . Next ( 0 , remainder . Count );
res . Add ( remainder [ n ]);
remainder . RemoveAt ( n );
}
}
2024-11-01 14:50:26 +01:00
2013-07-30 22:42:59 +02:00
return res ;
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:22:29 +02:00
public async IAsyncEnumerable < IRemoteVolume > SelectTestTargets ( long samples , Options options )
2013-06-26 21:59:01 +02:00
{
2025-05-21 09:22:29 +02:00
var tp = await GetFilelistWhereClause ( options . Time , options . Version );
2024-11-01 14:50:26 +01:00
2013-06-26 21:59:01 +02:00
samples = Math . Max ( 1 , samples );
2025-05-21 09:11:25 +02:00
using ( var cmd = m_connection . CreateCommand ( m_rtr ))
2013-06-26 21:59:01 +02:00
{
2025-05-27 17:15:02 +02:00
var files = new List < RemoteVolume >();
var max = cmd . ExecuteScalarInt64 ( @"SELECT MAX(""VerificationCount"") FROM ""RemoteVolume""" , 0 );
2025-04-03 09:03:22 +02:00
2025-05-27 17:15:02 +02:00
if ( options . FullRemoteVerification != Options . RemoteTestStrategy . IndexesOnly )
{
// Select any broken items
cmd . SetCommandAndParameters ( @"SELECT ""ID"", ""Name"", ""Size"", ""Hash"", ""VerificationCount"" FROM ""Remotevolume"" WHERE (""State"" IN (@States)) AND (""Hash"" = '' OR ""Hash"" IS NULL OR ""Size"" <= 0) AND (""ArchiveTime"" = 0)" )
. ExpandInClauseParameter ( "@States" , [ RemoteVolumeState . Verified . ToString (), RemoteVolumeState . Uploaded . ToString ()]);
2014-08-26 12:50:03 +02:00
2025-05-27 17:15:02 +02:00
using ( var rd = cmd . ExecuteReader ())
while ( rd . Read ())
yield return new RemoteVolume ( rd );
2024-11-01 14:50:26 +01:00
2025-05-27 17:15:02 +02:00
//First we select some filesets
var whereClause = string . IsNullOrEmpty ( tp . Item1 ) ? " WHERE " : ( " " + tp . Item1 + " AND " );
using ( var rd = cmd . SetCommandAndParameters ( @"SELECT ""A"".""VolumeID"", ""A"".""Name"", ""A"".""Size"", ""A"".""Hash"", ""A"".""VerificationCount"" FROM (SELECT ""ID"" AS ""VolumeID"", ""Name"", ""Size"", ""Hash"", ""VerificationCount"" FROM ""Remotevolume"" WHERE ""ArchiveTime"" = 0 AND ""State"" IN (@State1, @State2)) A, ""Fileset"" " + whereClause + @" ""A"".""VolumeID"" = ""Fileset"".""VolumeID"" ORDER BY ""Fileset"".""Timestamp"" " )
2025-05-21 09:40:32 +02:00
. SetParameterValue ( "@State1" , RemoteVolumeState . Uploaded . ToString ())
. SetParameterValue ( "@State2" , RemoteVolumeState . Verified . ToString ())
2025-05-27 17:15:02 +02:00
. SetParameterValues ( tp . Item2 )
. ExecuteReader ())
while ( rd . Read ())
files . Add ( new RemoteVolume ( rd ));
2024-11-01 14:50:26 +01:00
2025-05-27 17:15:02 +02:00
if ( files . Count == 0 )
yield break ;
2013-06-26 21:59:01 +02:00
2025-05-27 17:15:02 +02:00
if ( string . IsNullOrEmpty ( tp . Item1 ))
files = FilterByVerificationCount ( files , samples , max ). ToList ();
2024-11-01 14:50:26 +01:00
2025-05-27 17:15:02 +02:00
foreach ( var f in files )
yield return f ;
2024-11-01 14:50:26 +01:00
2025-05-27 17:15:02 +02:00
//Then we select some index files
files . Clear ();
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:40:32 +02:00
cmd . SetCommandAndParameters ( @"
SELECT
""ID"",
""Name"",
""Size"",
""Hash"",
""VerificationCount""
FROM ""Remotevolume""
WHERE
""Type"" = @Type
AND ""State"" IN (@States)
AND ""ArchiveTime"" = 0
" )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@Type" , RemoteVolumeType . Index . ToString ())
. ExpandInClauseParameter ( "@States" , [ RemoteVolumeState . Uploaded . ToString (), RemoteVolumeState . Verified . ToString ()]);
2025-05-21 09:22:29 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
2013-06-26 21:59:01 +02:00
files . Add ( new RemoteVolume ( rd ));
2024-11-01 14:50:26 +01:00
foreach ( var f in FilterByVerificationCount ( files , samples , max ))
2013-06-26 21:59:01 +02:00
yield return f ;
2014-08-26 12:50:03 +02:00
2025-05-27 17:15:02 +02:00
if ( options . FullRemoteVerification == Options . RemoteTestStrategy . ListAndIndexes || options . FullRemoteVerification == Options . RemoteTestStrategy . IndexesOnly )
2024-11-01 14:50:26 +01:00
yield break ;
2023-12-10 00:24:07 +01:00
2014-08-26 12:50:03 +02:00
//And finally some block files
2013-06-26 21:59:01 +02:00
files . Clear ();
2024-11-01 14:50:26 +01:00
2025-05-21 09:40:32 +02:00
cmd . SetCommandAndParameters ( @"
SELECT
""ID"",
""Name"",
""Size"",
""Hash"",
""VerificationCount""
FROM ""Remotevolume""
WHERE
""Type"" = @Type
AND ""State"" IN (@States)
AND ""ArchiveTime"" = 0
" )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@Type" , RemoteVolumeType . Blocks . ToString ())
. ExpandInClauseParameter ( "@States" , [ RemoteVolumeState . Uploaded . ToString (), RemoteVolumeState . Verified . ToString ()]);
2025-05-21 09:22:29 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
2013-06-26 21:59:01 +02:00
files . Add ( new RemoteVolume ( rd ));
2024-11-01 14:50:26 +01:00
foreach ( var f in FilterByVerificationCount ( files , samples , max ))
2013-06-26 21:59:01 +02:00
yield return f ;
}
}
2024-11-01 14:50:26 +01:00
2013-06-26 21:59:01 +02:00
private abstract class Basiclist : IDisposable
{
2025-05-21 09:11:25 +02:00
protected LocalDatabase m_db = null !;
2025-05-21 09:05:34 +02:00
protected string m_volumename = null !;
protected string m_tablename = null !;
2025-05-21 09:11:25 +02:00
protected SqliteCommand m_insertCommand = null !;
2024-11-01 14:50:26 +01:00
2025-05-21 09:05:34 +02:00
[Obsolete("Calling this constructor will throw an exception. Use the CreateAsync method instead.")]
2025-05-21 09:11:25 +02:00
protected Basiclist ( SqliteConnection connection , ReusableTransaction rtr , string volumename , string tablePrefix , string tableFormat , string insertCommand )
2024-11-01 14:50:26 +01:00
{
2025-05-21 09:05:34 +02:00
throw new NotSupportedException ( "Use CreateAsync method instead." );
}
protected Basiclist () { }
2025-05-21 09:11:25 +02:00
protected static async Task < Basiclist > CreateAsync ( Basiclist bl , LocalDatabase db , string volumename , string tablePrefix , string tableFormat , string insertCommand )
2025-05-21 09:05:34 +02:00
{
2025-05-21 09:11:25 +02:00
bl . m_db = db ;
2025-05-21 09:05:34 +02:00
bl . m_volumename = volumename ;
2018-10-16 20:39:06 -07:00
var tablename = tablePrefix + "-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2013-06-26 21:59:01 +02:00
2025-05-21 09:11:25 +02:00
using ( var cmd = bl . m_db . Connection . CreateCommand ( bl . m_db . Transaction ))
2013-06-26 21:59:01 +02:00
{
2025-05-21 09:40:32 +02:00
await cmd . ExecuteNonQueryAsync ( $@"
CREATE TEMPORARY TABLE ""{tablename}""
{tableFormat}
" );
2025-05-21 09:05:34 +02:00
bl . m_tablename = tablename ;
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:40:32 +02:00
bl . m_insertCommand = await bl . m_db . Connection . CreateCommandAsync ( $@"
INSERT INTO ""{bl.m_tablename}""
{insertCommand}
" );
2025-05-21 09:05:34 +02:00
return bl ;
2013-06-26 21:59:01 +02:00
}
2025-05-21 09:22:29 +02:00
public void Dispose ()
{
DisposeAsync (). Await ();
}
public virtual async Task DisposeAsync ()
2013-06-26 21:59:01 +02:00
{
if ( m_tablename != null )
2024-11-01 14:50:26 +01:00
try
{
2025-05-21 09:11:25 +02:00
using ( var cmd = m_db . Connection . CreateCommand ( m_db . Transaction . Transaction ))
2025-05-21 09:22:29 +02:00
await cmd . ExecuteNonQueryAsync ( $@"DROP TABLE IF EXISTS ""{m_tablename}""" );
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
catch { }
2025-04-03 15:46:20 +02:00
finally { m_tablename = null !; }
2024-11-01 14:50:26 +01:00
2025-05-21 09:22:29 +02:00
await m_insertCommand . DisposeAsync ();
2013-06-26 21:59:01 +02:00
}
}
2024-11-01 14:50:26 +01:00
2013-06-26 21:59:01 +02:00
public interface IFilelist : IDisposable
{
2025-05-21 09:22:29 +02:00
Task Add ( string path , long size , string hash , long metasize , string metahash , IEnumerable < string > blocklistHashes , FilelistEntryType type , DateTime time );
IAsyncEnumerable < KeyValuePair < Interface . TestEntryStatus , string >> Compare ();
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2013-06-26 21:59:01 +02:00
private class Filelist : Basiclist , IFilelist
{
2018-10-16 20:39:06 -07:00
private const string TABLE_PREFIX = "Filelist" ;
2025-05-21 09:40:32 +02:00
private const string TABLE_FORMAT = @"
(
""Path"" TEXT NOT NULL,
""Size"" INTEGER NOT NULL,
""Hash"" TEXT NULL,
""Metasize"" INTEGER NOT NULL,
""Metahash"" TEXT NOT NULL
)
" ;
private const string INSERT_COMMAND = @"
(
""Path"",
""Size"",
""Hash"",
""Metasize"",
""Metahash""
)
VALUES (
@Path,
@Size,
@Hash,
@Metasize,
@Metahash
)
" ;
2025-05-21 09:05:34 +02:00
[Obsolete("Calling this constructor will throw an exception. Use the CreateAsync method instead.")]
2025-05-21 09:11:25 +02:00
public Filelist ( SqliteConnection connection , string volumename , ReusableTransaction rtr )
2025-04-15 23:24:35 +02:00
: base ( connection , rtr , volumename , TABLE_PREFIX , TABLE_FORMAT , INSERT_COMMAND )
2024-11-01 14:50:26 +01:00
{
2025-05-21 09:05:34 +02:00
throw new NotSupportedException ( "Use CreateAsync method instead." );
}
private Filelist () { }
2025-05-21 09:11:25 +02:00
public static async Task < Filelist > CreateAsync ( LocalDatabase db , string volumename )
2025-05-21 09:05:34 +02:00
{
var bl = new Filelist ();
2025-05-21 09:11:25 +02:00
return ( Filelist ) await CreateAsync ( bl , db , volumename , TABLE_PREFIX , TABLE_FORMAT , INSERT_COMMAND );
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:22:29 +02:00
public async Task Add ( string path , long size , string hash , long metasize , string metahash , IEnumerable < string > blocklistHashes , FilelistEntryType type , DateTime time )
2013-06-26 21:59:01 +02:00
{
2025-05-21 09:22:29 +02:00
await m_insertCommand
. SetTransaction ( m_db . Transaction )
2025-04-15 23:24:35 +02:00
. SetParameterValue ( "@Path" , path )
. SetParameterValue ( "@Size" , hash == null ? - 1 : size )
. SetParameterValue ( "@Hash" , hash )
. SetParameterValue ( "@Metasize" , metasize )
. SetParameterValue ( "@Metahash" , metahash )
2025-05-21 09:22:29 +02:00
. ExecuteNonQueryAsync ();
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:22:29 +02:00
public async IAsyncEnumerable < KeyValuePair < Interface . TestEntryStatus , string >> Compare ()
2013-06-26 21:59:01 +02:00
{
var cmpName = "CmpTable-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2024-11-01 14:50:26 +01:00
2025-05-21 09:40:32 +02:00
var create = $@"
CREATE TEMPORARY TABLE ""{cmpName}"" AS
SELECT
""A"".""Path"" AS ""Path"",
CASE
WHEN ""B"".""Fullhash"" IS NULL
THEN -1
ELSE ""B"".""Length"" END AS ""Size"",
""B"".""Fullhash"" AS ""Hash"",
""C"".""Length"" AS ""Metasize"",
""C"".""Fullhash"" AS ""Metahash""
FROM (
SELECT
""File"".""Path"",
""File"".""BlocksetID"" AS ""FileBlocksetID"",
""Metadataset"".""BlocksetID"" AS ""MetadataBlocksetID""
FROM
""Remotevolume"",
""Fileset"",
""FilesetEntry"",
""File"",
""Metadataset""
WHERE
""Remotevolume"".""Name"" = @Name
AND ""Fileset"".""VolumeID"" = ""Remotevolume"".""ID""
AND ""Fileset"".""ID"" = ""FilesetEntry"".""FilesetID""
AND ""File"".""ID"" = ""FilesetEntry"".""FileID""
AND ""File"".""MetadataID"" = ""Metadataset"".""ID""
) A
LEFT OUTER JOIN ""Blockset"" B
ON ""B"".""ID"" = ""A"".""FileBlocksetID""
LEFT OUTER JOIN ""Blockset"" C
ON ""C"".""ID""=""A"".""MetadataBlocksetID""
" ;
var extra = $@"
SELECT
@TypeExtra AS ""Type"",
""{m_tablename}"".""Path"" AS ""Path""
FROM ""{m_tablename}""
WHERE ""{m_tablename}"".""Path"" NOT IN (
SELECT ""Path""
FROM ""{cmpName}""
)" ;
var missing = $@"
SELECT
@TypeMissing AS ""Type"",
""Path"" AS ""Path""
FROM ""{cmpName}""
WHERE ""Path"" NOT IN (
SELECT ""Path""
FROM ""{m_tablename}""
)
" ;
var modified = $@"
SELECT
@TypeModified AS ""Type"",
""E"".""Path"" AS ""Path""
FROM
""{m_tablename}"" E,
""{cmpName}"" D
WHERE
""D"".""Path"" = ""E"".""Path""
AND (
""D"".""Size"" != ""E"".""Size""
OR ""D"".""Hash"" != ""E"".""Hash""
OR ""D"".""Metasize"" != ""E"".""Metasize""
OR ""D"".""Metahash"" != ""E"".""Metahash""
)
" ;
2025-05-21 09:13:01 +02:00
var drop = $@"DROP TABLE IF EXISTS ""{cmpName}"" " ;
2024-11-01 14:50:26 +01:00
2025-05-21 09:11:25 +02:00
using ( var cmd = m_db . Connection . CreateCommand ( m_db . Transaction ))
2013-06-26 21:59:01 +02:00
{
try
{
2025-05-21 09:22:29 +02:00
await cmd
. SetCommandAndParameters ( create )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@Name" , m_volumename )
2025-05-21 09:22:29 +02:00
. ExecuteNonQueryAsync ();
2025-04-03 09:03:22 +02:00
2025-05-21 09:22:29 +02:00
cmd
. SetCommandAndParameters ( $"{extra} UNION {missing} UNION {modified}" )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@TypeExtra" , ( int ) Interface . TestEntryStatus . Extra )
. SetParameterValue ( "@TypeMissing" , ( int ) Interface . TestEntryStatus . Missing )
. SetParameterValue ( "@TypeModified" , ( int ) Interface . TestEntryStatus . Modified );
2025-05-21 09:22:29 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
2025-05-21 09:40:32 +02:00
yield return new KeyValuePair < Interface . TestEntryStatus , string >(
( Interface . TestEntryStatus ) rd . ConvertValueToInt64 ( 0 ),
rd . ConvertValueToString ( 1 ) ?? ""
);
2024-11-01 14:50:26 +01:00
2013-06-26 21:59:01 +02:00
}
finally
{
2025-05-21 09:22:29 +02:00
try
{
await cmd . ExecuteNonQueryAsync ( drop );
}
2024-11-01 14:50:26 +01:00
catch { }
2013-06-26 21:59:01 +02:00
}
}
}
}
2024-11-01 14:50:26 +01:00
public interface IIndexlist : IDisposable
2013-06-26 21:59:01 +02:00
{
2025-05-21 09:22:29 +02:00
Task AddBlockLink ( string filename , string hash , long length );
IAsyncEnumerable < KeyValuePair < Library . Interface . TestEntryStatus , string >> Compare ();
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2013-06-26 21:59:01 +02:00
private class Indexlist : Basiclist , IIndexlist
{
2018-10-16 20:39:06 -07:00
private const string TABLE_PREFIX = "Indexlist" ;
2025-05-21 09:40:32 +02:00
private const string TABLE_FORMAT = @"
(
""Name"" TEXT NOT NULL,
""Hash"" TEXT NOT NULL,
""Size"" INTEGER NOT NULL
)
" ;
private const string INSERT_COMMAND = @"
(
""Name"",
""Hash"",
""Size""
)
VALUES (
@Name,
@Hash,
@Size
)
" ;
2024-11-01 14:50:26 +01:00
2025-05-21 09:05:34 +02:00
[Obsolete("Calling this constructor will throw an exception. Use the CreateAsync method instead.")]
2025-05-21 09:11:25 +02:00
public Indexlist ( SqliteConnection connection , string volumename , ReusableTransaction rtr )
2025-04-15 23:24:35 +02:00
: base ( connection , rtr , volumename , TABLE_PREFIX , TABLE_FORMAT , INSERT_COMMAND )
2013-06-26 21:59:01 +02:00
{
2025-05-21 09:05:34 +02:00
throw new NotSupportedException ( "Use CreateAsync method instead." );
}
private Indexlist () { }
2025-05-21 09:11:25 +02:00
public static async Task < Indexlist > CreateAsync ( LocalDatabase db , string volumename )
2025-05-21 09:05:34 +02:00
{
var bl = new Indexlist ();
2025-05-21 09:11:25 +02:00
return ( Indexlist ) await CreateAsync ( bl , db , volumename , TABLE_PREFIX , TABLE_FORMAT , INSERT_COMMAND );
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:22:29 +02:00
public async Task AddBlockLink ( string filename , string hash , long length )
2013-06-26 21:59:01 +02:00
{
2025-05-21 09:22:29 +02:00
await m_insertCommand . SetTransaction ( m_db . Transaction )
2025-04-15 23:24:35 +02:00
. SetParameterValue ( "@Name" , filename )
. SetParameterValue ( "@Hash" , hash )
. SetParameterValue ( "@Size" , length )
2025-05-21 09:22:29 +02:00
. ExecuteNonQueryAsync ();
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:22:29 +02:00
public async IAsyncEnumerable < KeyValuePair < Duplicati . Library . Interface . TestEntryStatus , string >> Compare ()
2013-06-26 21:59:01 +02:00
{
var cmpName = "CmpTable-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2025-05-21 09:40:32 +02:00
var create = $@"
CREATE TEMPORARY TABLE ""{cmpName}"" AS
SELECT
""A"".""Name"",
""A"".""Hash"",
""A"".""Size""
FROM
""Remotevolume"" A,
""Remotevolume"" B,
""IndexBlockLink""
WHERE
""B"".""Name"" = @Name
AND ""A"".""ID"" = ""IndexBlockLink"".""BlockVolumeID""
AND ""B"".""ID"" = ""IndexBlockLink"".""IndexVolumeID""
" ;
var extra = $@"
SELECT
@TypeExtra AS ""Type"",
""{m_tablename}"".""Name"" AS ""Name""
FROM ""{m_tablename}""
WHERE ""{m_tablename}"".""Name"" NOT IN (
SELECT ""Name""
FROM ""{cmpName}""
)
" ;
var missing = $@"
SELECT
@TypeMissing AS ""Type"",
""Name"" AS ""Name""
FROM ""{cmpName}""
WHERE ""Name"" NOT IN (
SELECT ""Name""
FROM ""{m_tablename}""
)
" ;
var modified = $@"
SELECT
@TypeModified AS ""Type"",
""E"".""Name"" AS ""Name""
FROM
""{m_tablename}"" E,
""{cmpName}"" D
WHERE
""D"".""Name"" = ""E"".""Name""
AND (
""D"".""Hash"" != ""E"".""Hash""
OR ""D"".""Size"" != ""E"".""Size""
)
" ;
2025-05-21 09:13:01 +02:00
var drop = $@"DROP TABLE IF EXISTS ""{cmpName}"" " ;
2024-11-01 14:50:26 +01:00
2025-05-21 09:11:25 +02:00
using ( var cmd = m_db . Connection . CreateCommand ( m_db . Transaction ))
2013-06-26 21:59:01 +02:00
{
try
{
2025-05-21 09:22:29 +02:00
await cmd
. SetCommandAndParameters ( create )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@Name" , m_volumename )
2025-05-21 09:22:29 +02:00
. ExecuteNonQueryAsync ();
2025-04-03 09:03:22 +02:00
2025-05-21 09:22:29 +02:00
cmd
. SetCommandAndParameters ( $"{extra} UNION {missing} UNION {modified}" )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@TypeExtra" , ( int ) Interface . TestEntryStatus . Extra )
. SetParameterValue ( "@TypeMissing" , ( int ) Interface . TestEntryStatus . Missing )
. SetParameterValue ( "@TypeModified" , ( int ) Interface . TestEntryStatus . Modified );
2025-05-21 09:22:29 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
2025-04-03 15:46:20 +02:00
yield return new KeyValuePair < Interface . TestEntryStatus , string >(( Interface . TestEntryStatus ) rd . ConvertValueToInt64 ( 0 ), rd . ConvertValueToString ( 1 ) ?? "" );
2024-11-01 14:50:26 +01:00
2013-06-26 21:59:01 +02:00
}
finally
{
2025-05-21 09:22:29 +02:00
try
{
await cmd . ExecuteNonQueryAsync ( drop );
}
2024-11-01 14:50:26 +01:00
catch { }
2013-06-26 21:59:01 +02:00
}
}
}
}
2024-11-01 14:50:26 +01:00
public interface IBlocklist : IDisposable
2013-06-26 21:59:01 +02:00
{
2025-06-02 11:50:35 +02:00
Task AddBlock ( string key , long value );
IAsyncEnumerable < KeyValuePair < Library . Interface . TestEntryStatus , string >> Compare ();
2013-06-26 21:59:01 +02:00
}
2025-05-27 17:15:02 +02:00
public interface IBlocklistHashList : IDisposable
{
2025-06-02 11:50:35 +02:00
Task AddBlockHash ( string hash , long size );
IAsyncEnumerable < KeyValuePair < Interface . TestEntryStatus , string >> Compare ( int hashesPerBlock , int hashSize , int blockSize );
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
private class Blocklist : Basiclist , IBlocklist
{
2018-10-16 20:39:06 -07:00
private const string TABLE_PREFIX = "Blocklist" ;
private const string TABLE_FORMAT = @"(""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL)" ;
2025-04-03 09:03:22 +02:00
private const string INSERT_COMMAND = @"(""Hash"", ""Size"") VALUES (@Hash,@Size)" ;
2024-11-01 14:50:26 +01:00
2025-06-02 11:50:35 +02:00
[Obsolete("Calling this constructor will throw an exception. Use the CreateAsync method instead.")]
public Blocklist ( SqliteConnection connection , string volumename , ReusableTransaction rtr )
{
throw new NotSupportedException ( "Use CreateAsync method instead." );
}
private Blocklist () { }
2024-11-01 14:50:26 +01:00
2025-06-02 11:50:35 +02:00
public static async Task < Blocklist > CreateAsync ( LocalDatabase db , string volumename )
2025-05-21 09:05:34 +02:00
{
2025-06-02 11:50:35 +02:00
var bl = new Blocklist ();
return ( Blocklist ) await Basiclist . CreateAsync ( bl , db , volumename , TABLE_PREFIX , TABLE_FORMAT , INSERT_COMMAND );
}
public async Task AddBlock ( string hash , long size )
{
await m_insertCommand
. SetTransaction ( m_db . Transaction )
2025-04-15 23:24:35 +02:00
. SetParameterValue ( "@Hash" , hash )
. SetParameterValue ( "@Size" , size )
2025-06-02 11:50:35 +02:00
. ExecuteNonQueryAsync ();
2025-05-21 09:05:34 +02:00
}
2025-06-02 11:50:35 +02:00
public async IAsyncEnumerable < KeyValuePair < Interface . TestEntryStatus , string >> Compare ()
2025-05-21 09:05:34 +02:00
{
2013-06-26 21:59:01 +02:00
var cmpName = "CmpTable-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2025-06-02 11:50:35 +02:00
var curBlocks = @"
SELECT
""Block"".""Hash"" AS ""Hash"",
""Block"".""Size"" AS ""Size""
FROM
""Remotevolume"",
""Block""
WHERE
""Remotevolume"".""Name"" = @Name
AND ""Remotevolume"".""ID"" = ""Block"".""VolumeID""
" ;
var duplBlocks = @"
SELECT
""Block"".""Hash"" AS ""Hash"",
""Block"".""Size"" AS ""Size""
FROM
""DuplicateBlock"",
""Block""
WHERE
""DuplicateBlock"".""VolumeID"" = (
SELECT ""ID""
FROM ""RemoteVolume""
WHERE ""Name"" = @Name
)
AND ""Block"".""ID"" = ""DuplicateBlock"".""BlockID""
" ;
var delBlocks = @"
SELECT
""DeletedBlock"".""Hash"" AS ""Hash"",
""DeletedBlock"".""Size"" AS ""Size""
FROM
""DeletedBlock"",
""RemoteVolume""
WHERE
""RemoteVolume"".""Name"" = @Name
AND ""RemoteVolume"".""ID"" = ""DeletedBlock"".""VolumeID""
" ;
var create = $@"
CREATE TEMPORARY TABLE ""{cmpName}"" AS
SELECT DISTINCT
""Hash"" AS ""Hash"",
""Size"" AS ""Size""
FROM (
{curBlocks}
UNION {delBlocks}
UNION {duplBlocks}
)
" ;
var extra = $@"
SELECT
@TypeExtra AS ""Type"",
""{m_tablename}"".""Hash"" AS ""Hash""
FROM ""{m_tablename}""
WHERE ""{m_tablename}"".""Hash"" NOT IN (
SELECT ""Hash""
FROM ""{cmpName}""
)
" ;
var missing = $@"
SELECT
@TypeMissing AS ""Type"",
""Hash"" AS ""Hash""
FROM ""{cmpName}""
WHERE ""Hash"" NOT IN (
SELECT ""Hash""
FROM ""{m_tablename}""
)
" ;
var modified = $@"
SELECT
@TypeModified AS ""Type"",
""E"".""Hash"" AS ""Hash""
FROM
""{m_tablename}"" E,
""{cmpName}"" D
WHERE
""D"".""Hash"" = ""E"".""Hash""
AND ""D"".""Size"" != ""E"".""Size""
" ;
var drop = $@"DROP TABLE IF EXISTS ""{cmpName}"" " ;
using ( var cmd = m_db . Connection . CreateCommand ( m_db . Transaction ))
2013-06-26 21:59:01 +02:00
{
try
{
2025-06-02 11:50:35 +02:00
await cmd
. SetCommandAndParameters ( create )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@Name" , m_volumename )
2025-06-02 11:50:35 +02:00
. ExecuteNonQueryAsync ();
2025-04-03 09:03:22 +02:00
2025-06-02 11:50:35 +02:00
cmd
. SetCommandAndParameters ( $@"
{extra}
UNION {missing}
UNION {modified}
" )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@TypeExtra" , ( int ) Library . Interface . TestEntryStatus . Extra )
. SetParameterValue ( "@TypeMissing" , ( int ) Library . Interface . TestEntryStatus . Missing )
. SetParameterValue ( "@TypeModified" , ( int ) Library . Interface . TestEntryStatus . Modified );
2025-06-02 11:50:35 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
2025-04-03 15:46:20 +02:00
yield return new KeyValuePair < Duplicati . Library . Interface . TestEntryStatus , string >(( Duplicati . Library . Interface . TestEntryStatus ) rd . ConvertValueToInt64 ( 0 ), rd . ConvertValueToString ( 1 ) ?? "" );
2024-11-01 14:50:26 +01:00
2013-06-26 21:59:01 +02:00
}
finally
{
2025-06-02 11:50:35 +02:00
try
{
await cmd . ExecuteNonQueryAsync ( drop );
}
2024-11-01 14:50:26 +01:00
catch { }
2013-06-26 21:59:01 +02:00
}
}
2025-05-21 09:05:34 +02:00
}
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2025-05-27 17:15:02 +02:00
private class BlocklistHashList : Basiclist , IBlocklistHashList
{
private const string TABLE_PREFIX = "BlocklistHashList" ;
private const string TABLE_FORMAT = @"(""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL)" ;
private const string INSERT_COMMAND = @"(""Hash"", ""Size"") VALUES (@Hash,@Size)" ;
2025-06-02 11:50:35 +02:00
[Obsolete("Calling this constructor will throw an exception. Use the CreateAsync method instead.")]
public BlocklistHashList ( SqliteConnection connection , string volumename , ReusableTransaction rtr )
{
throw new NotSupportedException ( "Use CreateAsync method instead." );
}
2025-05-27 17:15:02 +02:00
2025-06-02 11:50:35 +02:00
private BlocklistHashList () { }
public static async Task < BlocklistHashList > CreateAsync ( LocalDatabase db , string volumename )
{
var bl = new BlocklistHashList ();
return ( BlocklistHashList ) await Basiclist . CreateAsync ( bl , db , volumename , TABLE_PREFIX , TABLE_FORMAT , INSERT_COMMAND );
}
public async Task AddBlockHash ( string hash , long size )
2013-06-26 21:59:01 +02:00
{
2025-06-02 11:50:35 +02:00
await m_insertCommand
. SetTransaction ( m_db . Transaction )
2025-04-15 23:24:35 +02:00
. SetParameterValue ( "@Hash" , hash )
. SetParameterValue ( "@Size" , size )
2025-06-02 11:50:35 +02:00
. ExecuteNonQueryAsync ();
2013-06-26 21:59:01 +02:00
}
2024-11-01 14:50:26 +01:00
2025-06-02 11:50:35 +02:00
public async IAsyncEnumerable < KeyValuePair < Interface . TestEntryStatus , string >> Compare ( int hashesPerBlock , int hashSize , int blockSize )
2013-06-26 21:59:01 +02:00
{
var cmpName = "CmpTable-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2025-05-21 09:40:32 +02:00
2025-06-02 11:50:35 +02:00
var create = $@"
2025-05-27 17:15:02 +02:00
CREATE TEMPORARY TABLE ""{cmpName}"" (
""Hash"" TEXT NOT NULL,
""Size"" INTEGER NOT NULL
);
2025-05-21 09:40:32 +02:00
2025-06-02 11:50:35 +02:00
INSERT INTO ""{cmpName}"" (
""Hash"",
""Size""
)
SELECT
b.""Hash"",
b.""Size""
2025-05-27 17:15:02 +02:00
FROM Block b
JOIN (
SELECT
blh.""Hash"",
CASE
WHEN blh.""Index"" = (((bs.""Length"" + {blockSize} - 1) / {blockSize} - 1) / {hashesPerBlock})
AND ((bs.""Length"" + {blockSize} - 1) / {blockSize}) % {hashesPerBlock} != 0
THEN {hashSize} * ((bs.""Length"" + {blockSize} - 1) / {blockSize} % {hashesPerBlock})
ELSE {hashSize} * {hashesPerBlock}
END AS ""Size""
FROM BlocklistHash blh
2025-06-02 11:50:35 +02:00
JOIN Blockset bs
ON bs.""ID"" = blh.""BlocksetID""
) expected
ON
b.""Hash"" = expected.""Hash""
AND b.""Size"" = expected.""Size""
2025-05-27 17:15:02 +02:00
WHERE b.""VolumeID"" IN (
SELECT ibl.""BlockVolumeID""
FROM Remotevolume idx
2025-06-02 11:50:35 +02:00
JOIN IndexBlockLink ibl
ON ibl.""IndexVolumeID"" = idx.""ID""
2025-05-27 17:15:02 +02:00
WHERE idx.""Name"" = @Name
);
2025-06-02 11:50:35 +02:00
" ;
2025-05-21 09:40:32 +02:00
2025-06-02 11:50:35 +02:00
var compare = $@"
2025-05-27 17:15:02 +02:00
WITH
Expected AS (
2025-06-02 11:50:35 +02:00
SELECT
""Hash"",
""Size""
FROM ""{cmpName}""
2025-05-27 17:15:02 +02:00
),
Actual AS (
2025-06-02 11:50:35 +02:00
SELECT
""Hash"",
""Size""
FROM ""{m_tablename}""
2025-05-27 17:15:02 +02:00
),
Extra AS (
SELECT @TypeExtra AS Type, a.""Hash""
FROM Actual a
2025-06-02 11:50:35 +02:00
LEFT JOIN Expected e
ON
a.""Hash"" = e.""Hash""
AND a.""Size"" = e.""Size""
2025-05-27 17:15:02 +02:00
WHERE e.""Hash"" IS NULL
),
Missing AS (
SELECT @TypeMissing AS Type, e.""Hash""
FROM Expected e
2025-06-02 11:50:35 +02:00
LEFT JOIN Actual a
ON
a.""Hash"" = e.""Hash""
AND a.""Size"" = e.""Size""
2025-05-27 17:15:02 +02:00
WHERE a.""Hash"" IS NULL
),
Modified AS (
SELECT @TypeModified AS Type, a.""Hash""
FROM Actual a
2025-06-02 11:50:35 +02:00
JOIN Expected e
ON a.""Hash"" = e.""Hash""
WHERE
a.""Size"" != e.""Size""
AND NOT EXISTS (
SELECT 1
FROM Extra x
WHERE x.""Hash"" = a.""Hash""
)
2025-05-27 17:15:02 +02:00
)
2025-06-02 11:50:35 +02:00
SELECT *
FROM Extra
2025-05-27 17:15:02 +02:00
UNION
2025-06-02 11:50:35 +02:00
SELECT *
FROM Missing
2025-05-27 17:15:02 +02:00
UNION
2025-06-02 11:50:35 +02:00
SELECT *
FROM Modified;
" ;
2025-05-21 09:40:32 +02:00
2025-06-02 11:50:35 +02:00
var drop = $@"DROP TABLE IF EXISTS ""{cmpName}""" ;
2024-11-01 14:50:26 +01:00
2025-06-02 11:50:35 +02:00
using ( var cmd = m_db . Connection . CreateCommand ( m_db . Transaction ))
2013-06-26 21:59:01 +02:00
{
try
{
2025-05-27 17:15:02 +02:00
// Create expected hash+size table filtered by volume
2025-06-02 11:50:35 +02:00
await cmd
. SetCommandAndParameters ( create )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@Name" , m_volumename )
2025-06-02 11:50:35 +02:00
. ExecuteNonQueryAsync ();
2025-04-03 09:03:22 +02:00
2025-05-27 17:15:02 +02:00
// Compare against actual values inserted into temp table
2025-06-02 11:50:35 +02:00
cmd
. SetCommandAndParameters ( compare )
2025-04-03 09:03:22 +02:00
. SetParameterValue ( "@TypeExtra" , ( int ) Library . Interface . TestEntryStatus . Extra )
. SetParameterValue ( "@TypeMissing" , ( int ) Library . Interface . TestEntryStatus . Missing )
. SetParameterValue ( "@TypeModified" , ( int ) Library . Interface . TestEntryStatus . Modified );
2025-05-21 09:22:29 +02:00
2025-06-02 11:50:35 +02:00
using ( var rd = await cmd . ExecuteReaderAsync ())
while ( await rd . ReadAsync ())
2025-05-27 17:15:02 +02:00
yield return new KeyValuePair < Library . Interface . TestEntryStatus , string >(
( Library . Interface . TestEntryStatus ) rd . ConvertValueToInt64 ( 0 ),
rd . ConvertValueToString ( 1 ) ?? "" );
2013-06-26 21:59:01 +02:00
}
finally
{
2025-06-02 11:50:35 +02:00
try
{
await cmd . ExecuteNonQueryAsync ( drop );
}
catch { }
2013-06-26 21:59:01 +02:00
}
}
}
}
2024-11-01 14:50:26 +01:00
2025-05-21 09:22:29 +02:00
public async Task < IFilelist > CreateFilelist ( string name )
2013-06-26 21:59:01 +02:00
{
2025-05-21 09:22:29 +02:00
return await Filelist . CreateAsync ( this , name );
2013-06-26 21:59:01 +02:00
}
2025-06-02 11:50:35 +02:00
public async Task < IIndexlist > CreateIndexlist ( string name )
2013-06-26 21:59:01 +02:00
{
2025-06-02 11:50:35 +02:00
return await Indexlist . CreateAsync ( this , name );
2013-06-26 21:59:01 +02:00
}
2025-06-02 11:50:35 +02:00
public async Task < IBlocklist > CreateBlocklist ( string name )
2013-06-26 21:59:01 +02:00
{
2025-06-02 11:50:35 +02:00
return await Blocklist . CreateAsync ( this , name );
2013-06-26 21:59:01 +02:00
}
2025-06-02 11:50:35 +02:00
public async Task < IBlocklistHashList > CreateBlocklistHashList ( string name )
2013-06-26 21:59:01 +02:00
{
2025-06-02 11:50:35 +02:00
return await BlocklistHashList . CreateAsync ( this , name );
2013-06-26 21:59:01 +02:00
}
}
}