2025-03-10 14:41:12 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// 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
using System ;
2015-02-04 21:39:37 +01:00
using System.Linq ;
2013-03-30 15:23:09 +01:00
using System.Collections.Generic ;
2018-09-09 14:42:40 +02:00
using System.Security.Cryptography ;
2021-04-04 11:17:13 -07:00
using Duplicati.Library.Utility ;
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 ));
2016-09-15 11:39:27 +02:00
public LocalRepairDatabase ( string path )
: base ( path , "Repair" , true )
{
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
}
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
public long GetFilesetIdFromRemotename ( string filelist )
{
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ())
2016-09-15 11:39:27 +02:00
{
var filesetid = cmd . ExecuteScalarInt64 ( @"SELECT ""Fileset"".""ID"" FROM ""Fileset"",""RemoteVolume"" WHERE ""Fileset"".""VolumeID"" = ""RemoteVolume"".""ID"" AND ""RemoteVolume"".""Name"" = ?" , - 1 , filelist );
2015-01-24 21:59:53 +01:00
if ( filesetid == - 1 )
2016-09-15 11:39:27 +02:00
throw new Exception ( string . Format ( "No such remote file: {0}" , filelist ));
2025-03-10 14:41:12 +01:00
2015-01-24 21:59:53 +01:00
return filesetid ;
2016-09-15 11:39:27 +02:00
}
}
public interface IBlockSource
{
string File { get ; }
long Offset { get ; }
}
2025-03-10 14:41:12 +01:00
public interface IBlockWithSources : LocalBackupDatabase . IBlock
2016-09-15 11:39:27 +02:00
{
IEnumerable < IBlockSource > Sources { get ; }
}
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
private class BlockWithSources : LocalBackupDatabase . Block , IBlockWithSources
{
private class BlockSource : IBlockSource
{
public string File { get ; private set ; }
public long Offset { get ; private set ; }
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
public BlockSource ( string file , long offset )
{
this . File = file ;
this . Offset = offset ;
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
}
}
2018-05-23 21:18:01 -07:00
private readonly System . Data . IDataReader m_rd ;
2016-09-15 11:39:27 +02:00
public bool Done { get ; private set ; }
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
public BlockWithSources ( System . Data . IDataReader rd )
2015-01-24 21:59:53 +01:00
: base ( rd . GetString ( 0 ), rd . GetInt64 ( 1 ))
2016-09-15 11:39:27 +02:00
{
m_rd = rd ;
Done = ! m_rd . Read ();
}
2025-03-10 14:41:12 +01:00
2016-09-15 11:39:27 +02:00
public IEnumerable < IBlockSource > Sources
{
get
{
if ( Done )
yield break ;
2025-03-10 14:41:12 +01:00
2015-01-24 21:59:53 +01:00
var cur = new BlockSource ( m_rd . GetString ( 2 ), m_rd . GetInt64 ( 3 ));
2016-09-15 11:39:27 +02:00
var file = cur . File ;
2025-03-10 14:41:12 +01:00
while (! Done && cur . File == file )
2016-09-15 11:39:27 +02:00
{
yield return cur ;
Done = m_rd . Read ();
if (! Done )
2015-01-24 21:59:53 +01:00
cur = new BlockSource ( m_rd . GetString ( 2 ), m_rd . GetInt64 ( 3 ));
2016-09-15 11:39:27 +02:00
}
}
}
}
2025-03-10 14:41:12 +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
2016-09-15 11:39:27 +02:00
public IEnumerable < IRemoteVolume > GetBlockVolumesFromIndexName ( string name )
{
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ())
foreach ( var rd in cmd . ExecuteReaderEnumerable ( @"SELECT ""Name"", ""Hash"", ""Size"" FROM ""RemoteVolume"" WHERE ""ID"" IN (SELECT ""BlockVolumeID"" FROM ""IndexBlockLink"" WHERE ""IndexVolumeID"" IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" = ?))" , name ))
2015-05-17 13:38:25 +02:00
yield return new RemoteVolume ( rd . GetString ( 0 ), rd . ConvertValueToString ( 1 ), rd . ConvertValueToInt64 ( 2 ));
2016-09-15 11:39:27 +02:00
}
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
public interface IMissingBlockList : IDisposable
{
bool SetBlockRestored ( string hash , long size );
IEnumerable < IBlockWithSources > GetSourceFilesWithBlocks ( long blocksize );
IEnumerable < KeyValuePair < string , long >> GetMissingBlocks ();
IEnumerable < IRemoteVolume > GetFilesetsUsingMissingBlocks ();
IEnumerable < IRemoteVolume > GetMissingBlockSources ();
}
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
private class MissingBlockList : IMissingBlockList
{
2018-05-23 21:18:01 -07:00
private readonly System . Data . IDbConnection m_connection ;
private readonly TemporaryTransactionWrapper m_transaction ;
2013-06-24 20:49:32 +02:00
private System . Data . IDbCommand m_insertCommand ;
private string m_tablename ;
2018-05-23 21:18:01 -07:00
private readonly string m_volumename ;
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
public MissingBlockList ( string volumename , System . Data . IDbConnection connection , System . Data . IDbTransaction transaction )
{
m_connection = connection ;
m_transaction = new TemporaryTransactionWrapper ( m_connection , transaction );
m_volumename = volumename ;
var tablename = "MissingBlocks-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ())
2013-06-24 20:49:32 +02:00
{
cmd . Transaction = m_transaction . Parent ;
cmd . ExecuteNonQuery ( string . Format ( @"CREATE TEMPORARY TABLE ""{0}"" (""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL, ""Restored"" INTEGER NOT NULL) " , tablename ));
m_tablename = tablename ;
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
var blockCount = cmd . ExecuteNonQuery ( string . Format ( @"INSERT INTO ""{0}"" (""Hash"", ""Size"", ""Restored"") SELECT DISTINCT ""Block"".""Hash"", ""Block"".""Size"", 0 AS ""Restored"" FROM ""Block"",""Remotevolume"" WHERE ""Block"".""VolumeID"" = ""Remotevolume"".""ID"" AND ""Remotevolume"".""Name"" = ? " , m_tablename ), volumename );
if ( blockCount == 0 )
throw new Exception ( string . Format ( "Unexpected empty block volume: {0}" , volumename ));
2014-11-23 22:07:55 +01:00
cmd . ExecuteNonQuery ( string . Format ( @"CREATE UNIQUE INDEX ""{0}-Ix"" ON ""{0}"" (""Hash"", ""Size"", ""Restored"")" , tablename ));
2013-06-24 20:49:32 +02:00
}
2014-11-23 22:07:55 +01:00
2013-06-24 20:49:32 +02:00
m_insertCommand = m_connection . CreateCommand ();
m_insertCommand . Transaction = m_transaction . Parent ;
2014-11-23 22:07:55 +01:00
m_insertCommand . CommandText = string . Format ( @"UPDATE ""{0}"" SET ""Restored"" = ? WHERE ""Hash"" = ? AND ""Size"" = ? AND ""Restored"" = ? " , tablename );
m_insertCommand . AddParameters ( 4 );
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 bool SetBlockRestored ( string hash , long size )
{
2014-11-23 22:07:55 +01:00
m_insertCommand . SetParameterValue ( 0 , 1 );
m_insertCommand . SetParameterValue ( 1 , hash );
m_insertCommand . SetParameterValue ( 2 , size );
m_insertCommand . SetParameterValue ( 3 , 0 );
2013-06-24 20:49:32 +02:00
return m_insertCommand . ExecuteNonQuery () == 1 ;
}
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
public IEnumerable < IBlockWithSources > GetSourceFilesWithBlocks ( long blocksize )
{
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ( m_transaction . Parent ))
using ( var rd = cmd . ExecuteReader ( string . Format ( @"SELECT DISTINCT ""{0}"".""Hash"", ""{0}"".""Size"", ""File"".""Path"", ""BlocksetEntry"".""Index"" * {1} FROM ""{0}"", ""Block"", ""BlocksetEntry"", ""File"" WHERE ""File"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID"" AND ""Block"".""ID"" = ""BlocksetEntry"".""BlockID"" AND ""{0}"".""Hash"" = ""Block"".""Hash"" AND ""{0}"".""Size"" = ""Block"".""Size"" AND ""{0}"".""Restored"" = ? " , m_tablename , blocksize ), 0 ))
if ( rd . Read ())
{
var bs = new BlockWithSources ( rd );
while (! bs . Done )
yield return bs ;
}
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 IEnumerable < KeyValuePair < string , long >> GetMissingBlocks ()
{
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ( m_transaction . Parent ))
foreach ( var rd in cmd . ExecuteReaderEnumerable ( string . Format ( @"SELECT ""{0}"".""Hash"", ""{0}"".""Size"" FROM ""{0}"" WHERE ""{0}"".""Restored"" = ? " , m_tablename ), 0 ))
2015-05-17 13:38:25 +02:00
yield return new KeyValuePair < string , long >( rd . ConvertValueToString ( 0 ), rd . ConvertValueToInt64 ( 1 ));
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 IEnumerable < IRemoteVolume > GetFilesetsUsingMissingBlocks ()
{
2018-06-14 10:12:24 +02:00
var blocks = @"SELECT DISTINCT ""FileLookup"".""ID"" AS ID FROM ""{0}"", ""Block"", ""Blockset"", ""BlocksetEntry"", ""FileLookup"" WHERE ""Block"".""Hash"" = ""{0}"".""Hash"" AND ""Block"".""Size"" = ""{0}"".""Size"" AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""BlocksetEntry"".""BlocksetID"" = ""Blockset"".""ID"" AND ""FileLookup"".""BlocksetID"" = ""Blockset"".""ID"" " ;
var blocklists = @"SELECT DISTINCT ""FileLookup"".""ID"" AS ID FROM ""{0}"", ""Block"", ""Blockset"", ""BlocklistHash"", ""FileLookup"" WHERE ""Block"".""Hash"" = ""{0}"".""Hash"" AND ""Block"".""Size"" = ""{0}"".""Size"" AND ""BlocklistHash"".""Hash"" = ""Block"".""Hash"" AND ""BlocklistHash"".""BlocksetID"" = ""Blockset"".""ID"" AND ""FileLookup"".""BlocksetID"" = ""Blockset"".""ID"" " ;
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
var cmdtxt = @"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"" = ? AND ""FilesetEntry"".""FileID"" IN (SELECT DISTINCT ""ID"" FROM ( " + blocks + " UNION " + blocklists + " ))" ;
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ( m_transaction . Parent ))
foreach ( var rd in cmd . ExecuteReaderEnumerable ( string . Format ( cmdtxt , m_tablename ), RemoteVolumeType . Files . ToString ()))
2015-05-17 13:38:25 +02:00
yield return new RemoteVolume ( rd . GetString ( 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 IEnumerable < IRemoteVolume > GetMissingBlockSources ()
{
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ( m_transaction . Parent ))
foreach ( var rd in cmd . ExecuteReaderEnumerable ( string . Format ( @"SELECT DISTINCT ""RemoteVolume"".""Name"", ""RemoteVolume"".""Hash"", ""RemoteVolume"".""Size"" FROM ""RemoteVolume"", ""Block"", ""{0}"" WHERE ""Block"".""Hash"" = ""{0}"".""Hash"" AND ""Block"".""Size"" = ""{0}"".""Size"" AND ""Block"".""VolumeID"" = ""RemoteVolume"".""ID"" AND ""Remotevolume"".""Name"" != ? " , m_tablename ), m_volumename ))
2015-05-17 13:38:25 +02:00
yield return new RemoteVolume ( rd . GetString ( 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 ()
{
if ( m_tablename != null )
{
try
{
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ( m_transaction . Parent ))
2013-08-23 23:36:25 +02:00
cmd . ExecuteNonQuery ( string . Format ( @"DROP TABLE IF EXISTS ""{0}"" " , m_transaction ));
2013-06-24 20:49:32 +02:00
}
catch { }
finally { m_tablename = null ; }
}
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
if ( m_insertCommand != null )
try { m_insertCommand . Dispose (); }
2025-03-10 14:41:12 +01:00
catch { }
2013-06-24 20:49:32 +02:00
finally { m_insertCommand = null ; }
}
}
2025-03-10 14:41:12 +01:00
2013-06-24 20:49:32 +02:00
public IMissingBlockList CreateBlockList ( string volumename , System . Data . IDbTransaction transaction = null )
{
return new MissingBlockList ( volumename , m_connection , transaction );
}
2013-07-24 21:25:41 +02:00
2014-10-30 15:44:32 +01:00
public void FixDuplicateMetahash ()
{
2025-03-10 14:41:12 +01:00
using ( var tr = m_connection . BeginTransaction ())
using ( var cmd = m_connection . CreateCommand ( tr ))
2014-10-30 15:44:32 +01:00
{
cmd . Transaction = tr ;
2025-03-10 14:41:12 +01:00
var sql_count =
2014-10-30 15:44:32 +01:00
@"SELECT COUNT(*) FROM (" +
2014-11-23 21:54:38 +01:00
@" SELECT DISTINCT c1 FROM (" +
@"SELECT COUNT(*) AS ""C1"" FROM (SELECT DISTINCT ""BlocksetID"" FROM ""Metadataset"") UNION SELECT COUNT(*) AS ""C1"" FROM ""Metadataset"" " +
@")" +
2014-10-30 15:44:32 +01:00
@")" ;
2015-01-24 21:59:53 +01:00
var x = cmd . ExecuteScalarInt64 ( sql_count , 0 );
if ( x > 1 )
2014-10-30 15:44:32 +01:00
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateMetadataHashes" , "Found duplicate metadatahashes, repairing" );
2014-10-30 15:44:32 +01:00
2014-11-23 21:54:38 +01:00
var tablename = "TmpFile-" + Guid . NewGuid (). ToString ( "N" );
2015-07-12 21:36:48 +02:00
cmd . ExecuteNonQuery ( string . Format ( @"CREATE TEMPORARY TABLE ""{0}"" AS SELECT * FROM ""File""" , tablename ));
2014-11-23 21:54:38 +01: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-03-10 14:41:12 +01:00
using ( var c2 = m_connection . CreateCommand ( tr ))
2014-10-30 15:44:32 +01:00
{
2014-11-23 21:54:38 +01:00
c2 . CommandText = string . Format ( @"UPDATE ""{0}"" SET ""MetadataID"" = ? WHERE ""MetadataID"" IN (SELECT ""ID"" FROM ""Metadataset"" WHERE ""BlocksetID"" = ?)" , tablename );
2014-10-30 15:44:32 +01:00
c2 . CommandText += @"; DELETE FROM ""Metadataset"" WHERE ""BlocksetID"" = ? AND ""ID"" != ?" ;
2025-03-10 14:41:12 +01:00
using ( var rd = cmd . ExecuteReader ( sql ))
2014-10-30 15:44:32 +01:00
while ( rd . Read ())
c2 . ExecuteNonQuery ( null , rd . GetValue ( 0 ), rd . GetValue ( 1 ), rd . GetValue ( 1 ), rd . GetValue ( 0 ));
}
2014-11-23 21:54:38 +01:00
sql = string . Format ( @"SELECT ""ID"", ""Path"", ""BlocksetID"", ""MetadataID"", ""Entries"" FROM (
SELECT MIN(""ID"") AS ""ID"", ""Path"", ""BlocksetID"", ""MetadataID"", COUNT(*) as ""Entries"" FROM ""{0}"" GROUP BY ""Path"", ""BlocksetID"", ""MetadataID"")
WHERE ""Entries"" > 1 ORDER BY ""ID""" , tablename );
2025-03-10 14:41:12 +01:00
using ( var c2 = m_connection . CreateCommand ())
2014-11-23 21:54:38 +01:00
{
c2 . Transaction = tr ;
2015-07-12 21:36:48 +02:00
c2 . CommandText = string . Format ( @"UPDATE ""FilesetEntry"" SET ""FileID"" = ? WHERE ""FileID"" IN (SELECT ""ID"" FROM ""{0}"" WHERE ""Path"" = ? AND ""BlocksetID"" = ? AND ""MetadataID"" = ?)" , tablename );
2014-11-23 21:54:38 +01:00
c2 . CommandText += string . Format ( @"; DELETE FROM ""{0}"" WHERE ""Path"" = ? AND ""BlocksetID"" = ? AND ""MetadataID"" = ? AND ""ID"" != ?" , tablename );
2025-03-10 14:41:12 +01:00
foreach ( var rd in cmd . ExecuteReaderEnumerable ( sql ))
2015-02-04 21:39:37 +01:00
c2 . ExecuteNonQuery ( null , rd . GetValue ( 0 ), rd . GetValue ( 1 ), rd . GetValue ( 2 ), rd . GetValue ( 3 ), rd . GetValue ( 1 ), rd . GetValue ( 2 ), rd . GetValue ( 3 ), rd . GetValue ( 0 ));
2014-11-23 21:54:38 +01:00
}
2018-06-14 10:12:24 +02:00
cmd . ExecuteNonQuery ( string . Format ( @"DELETE FROM ""FileLookup"" WHERE ""ID"" NOT IN (SELECT ""ID"" FROM ""{0}"") " , tablename ));
2014-11-23 21:54:38 +01:00
cmd . ExecuteNonQuery ( string . Format ( @"CREATE INDEX ""{0}-Ix"" ON ""{0}"" (""ID"", ""MetadataID"")" , tablename ));
2018-06-14 10:12:24 +02:00
cmd . ExecuteNonQuery ( string . Format ( @"UPDATE ""FileLookup"" SET ""MetadataID"" = (SELECT ""MetadataID"" FROM ""{0}"" A WHERE ""A"".""ID"" = ""FileLookup"".""ID"") " , tablename ));
2014-11-23 21:54:38 +01:00
cmd . ExecuteNonQuery ( string . Format ( @"DROP TABLE ""{0}"" " , tablename ));
cmd . CommandText = sql_count ;
2015-01-24 21:59:53 +01:00
x = cmd . ExecuteScalarInt64 ( 0 );
if ( x > 1 )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( "Repair failed, there are still duplicate metadatahashes!" , "DuplicateHashesRepairFailed" );
2014-10-30 15:44:32 +01:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateMetadataHashesFixed" , "Duplicate metadatahashes repaired succesfully" );
2014-10-30 15:44:32 +01:00
tr . Commit ();
}
}
}
public void FixDuplicateFileentries ()
{
2025-03-10 14:41:12 +01:00
using ( var tr = m_connection . BeginTransaction ())
using ( var cmd = m_connection . CreateCommand ( tr ))
2014-10-30 15:44:32 +01:00
{
2018-06-14 10:12:24 +02:00
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" ;
2014-10-30 15:44:32 +01:00
2015-01-24 21:59:53 +01:00
var x = cmd . ExecuteScalarInt64 ( sql_count , 0 );
if ( x > 0 )
2014-10-30 15:44:32 +01:00
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateFileEntries" , "Found duplicate file entries, repairing" );
2014-10-30 15:44:32 +01:00
2018-06-14 10:12:24 +02:00
var sql = @"SELECT ""ID"", ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"", ""Entries"" FROM (
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-03-10 14:41:12 +01:00
using ( var c2 = m_connection . CreateCommand ( tr ))
2014-10-30 15:44:32 +01:00
{
2018-06-14 10:12:24 +02:00
c2 . CommandText = @"UPDATE ""FilesetEntry"" SET ""FileID"" = ? WHERE ""FileID"" IN (SELECT ""ID"" FROM ""FileLookup"" WHERE ""PrefixID"" = ? AND ""Path"" = ? AND ""BlocksetID"" = ? AND ""MetadataID"" = ?)" ;
c2 . CommandText += @"; DELETE FROM ""FileLookup"" WHERE ""PrefixID"" = ? AND ""Path"" = ? AND ""BlocksetID"" = ? AND ""MetadataID"" = ? AND ""ID"" != ?" ;
2025-03-10 14:41:12 +01:00
foreach ( var rd in cmd . ExecuteReaderEnumerable ( sql ))
2018-06-14 10:12:24 +02:00
c2 . ExecuteNonQuery ( null , rd . GetValue ( 0 ), rd . GetValue ( 1 ), rd . GetValue ( 2 ), rd . GetValue ( 3 ), rd . GetValue ( 4 ), rd . GetValue ( 1 ), rd . GetValue ( 2 ), rd . GetValue ( 3 ), rd . GetValue ( 4 ), rd . GetValue ( 0 ));
2014-10-30 15:44:32 +01:00
}
2014-11-23 21:54:38 +01:00
cmd . CommandText = sql_count ;
2015-01-24 21:59:53 +01:00
x = cmd . ExecuteScalarInt64 ( 0 );
if ( x > 1 )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( "Repair failed, there are still duplicate file entries!" , "DuplicateFilesRepairFailed" );
2014-10-30 15:44:32 +01:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateFileEntriesFixed" , "Duplicate file entries repaired succesfully" );
2014-10-30 15:44:32 +01:00
tr . Commit ();
}
}
}
2015-01-12 23:07:57 +01:00
2015-02-03 23:49:46 +01:00
public void FixMissingBlocklistHashes ( string blockhashalgorithm , long blocksize )
{
var blocklistbuffer = new byte [ blocksize ];
2025-03-10 14:41:12 +01:00
using ( var tr = m_connection . BeginTransaction ())
using ( var cmd = m_connection . CreateCommand ( tr ))
using ( var blockhasher = HashFactory . CreateHasher ( blockhashalgorithm ))
2015-02-03 23:49:46 +01:00
{
2021-04-04 11:17:13 -07:00
var hashsize = blockhasher . HashSize / 8 ;
var sql = string . Format ( @"SELECT * FROM (SELECT ""N"".""BlocksetID"", ((""N"".""BlockCount"" + {0} - 1) / {0}) 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""" , blocksize / hashsize );
var countsql = @"SELECT COUNT(*) FROM (" + sql + @")" ;
2015-02-03 23:49:46 +01:00
var itemswithnoblocklisthash = cmd . ExecuteScalarInt64 ( countsql , 0 );
if ( itemswithnoblocklisthash != 0 )
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "MissingBlocklistHashes" , "Found {0} missing blocklisthash entries, repairing" , itemswithnoblocklisthash );
2025-03-10 14:41:12 +01:00
using ( var c2 = m_connection . CreateCommand ( tr ))
using ( var c3 = m_connection . CreateCommand ( tr ))
using ( var c4 = m_connection . CreateCommand ( tr ))
using ( var c5 = m_connection . CreateCommand ( tr ))
using ( var c6 = m_connection . CreateCommand ( tr ))
2015-02-03 23:49:46 +01:00
{
2015-02-04 21:52:49 +01:00
c3 . CommandText = @"INSERT INTO ""BlocklistHash"" (""BlocksetID"", ""Index"", ""Hash"") VALUES (?, ?, ?) " ;
2015-02-03 23:49:46 +01:00
c4 . CommandText = @"SELECT COUNT(*) FROM ""Block"" WHERE ""Hash"" = ? AND ""Size"" = ?" ;
2015-02-04 21:52:49 +01:00
c5 . CommandText = @"SELECT ""ID"" FROM ""DeletedBlock"" WHERE ""Hash"" = ? AND ""Size"" = ? AND ""VolumeID"" IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = ? AND (""State"" = ? OR ""State"" = ?))" ;
c6 . CommandText = @"INSERT INTO ""Block"" (""Hash"", ""Size"", ""VolumeID"") SELECT ""Hash"", ""Size"", ""VolumeID"" FROM ""DeletedBlock"" WHERE ""ID"" = ? LIMIT 1; DELETE FROM ""DeletedBlock"" WHERE ""ID"" = ?;" ;
2015-02-03 23:49:46 +01:00
2025-03-10 14:41:12 +01:00
foreach ( var e in cmd . ExecuteReaderEnumerable ( sql ))
2015-02-03 23:49:46 +01:00
{
var blocksetid = e . ConvertValueToInt64 ( 0 );
var ix = 0L ;
2020-12-26 21:32:28 -08:00
int blocklistoffset = 0 ;
2015-02-03 23:49:46 +01:00
c2 . ExecuteNonQuery ( @"DELETE FROM ""BlocklistHash"" WHERE ""BlocksetID"" = ?" , blocksetid );
2025-03-10 14:41:12 +01:00
foreach ( var h in c2 . ExecuteReaderEnumerable ( @"SELECT ""A"".""Hash"" FROM ""Block"" ""A"", ""BlocksetEntry"" ""B"" WHERE ""A"".""ID"" = ""B"".""BlockID"" AND ""B"".""BlocksetID"" = ? ORDER BY ""B"".""Index""" , blocksetid ))
2015-02-03 23:49:46 +01:00
{
var tmp = Convert . FromBase64String ( h . GetString ( 0 ));
if ( blocklistbuffer . Length - blocklistoffset < tmp . Length )
{
var blkey = Convert . ToBase64String ( blockhasher . ComputeHash ( blocklistbuffer , 0 , blocklistoffset ));
// Ensure that the block exists in "blocks"
if ( c4 . ExecuteScalarInt64 ( null , - 1 , blkey , blocklistoffset ) != 1 )
2015-02-04 21:52:49 +01:00
{
var c = c5 . ExecuteScalarInt64 ( null , - 1 , blkey , blocklistoffset , RemoteVolumeType . Blocks . ToString (), RemoteVolumeState . Uploaded . ToString (), RemoteVolumeState . Verified . ToString ());
if ( c <= 0 )
throw new Exception ( string . Format ( "Missing block for blocklisthash: {0}" , blkey ));
else
{
var rc = c6 . ExecuteNonQuery ( null , c , c );
if ( rc != 2 )
throw new Exception ( string . Format ( "Unexpected update count: {0}" , rc ));
}
}
2015-02-03 23:49:46 +01:00
// Add to table
c3 . ExecuteNonQuery ( null , blocksetid , ix , blkey );
ix ++;
blocklistoffset = 0 ;
}
Array . Copy ( tmp , 0 , blocklistbuffer , blocklistoffset , tmp . Length );
blocklistoffset += tmp . Length ;
}
if ( blocklistoffset != 0 )
{
var blkeyfinal = Convert . ToBase64String ( blockhasher . ComputeHash ( blocklistbuffer , 0 , blocklistoffset ));
// Ensure that the block exists in "blocks"
if ( c4 . ExecuteScalarInt64 ( null , - 1 , blkeyfinal , blocklistoffset ) != 1 )
2015-02-04 21:52:49 +01:00
{
var c = c5 . ExecuteScalarInt64 ( null , - 1 , blkeyfinal , blocklistoffset , RemoteVolumeType . Blocks . ToString (), RemoteVolumeState . Uploaded . ToString (), RemoteVolumeState . Verified . ToString ());
if ( c == 0 )
throw new Exception ( string . Format ( "Missing block for blocklisthash: {0}" , blkeyfinal ));
else
{
var rc = c6 . ExecuteNonQuery ( null , c , c );
if ( rc != 2 )
throw new Exception ( string . Format ( "Unexpected update count: {0}" , rc ));
}
}
2015-02-03 23:49:46 +01:00
// Add to table
c3 . ExecuteNonQuery ( null , blocksetid , ix , blkeyfinal );
}
}
}
itemswithnoblocklisthash = cmd . ExecuteScalarInt64 ( countsql , 0 );
if ( itemswithnoblocklisthash != 0 )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( string . Format ( "Failed to repair, after repair {0} blocklisthashes were missing" , itemswithnoblocklisthash ), "MissingBlocklistHashesRepairFailed" );
2015-02-03 23:49:46 +01:00
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "MissingBlocklisthashesRepaired" , "Missing blocklisthashes repaired succesfully" );
2015-02-03 23:49:46 +01:00
tr . Commit ();
}
}
}
2015-11-28 21:53:46 +01:00
public void FixDuplicateBlocklistHashes ( long blocksize , long hashsize )
2015-01-12 23:07:57 +01:00
{
2025-03-10 14:41:12 +01:00
using ( var tr = m_connection . BeginTransaction ())
using ( var cmd = m_connection . CreateCommand ( tr ))
2015-01-12 23:07:57 +01:00
{
var dup_sql = @"SELECT * FROM (SELECT ""BlocksetID"", ""Index"", COUNT(*) AS ""EC"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"", ""Index"") WHERE ""EC"" > 1" ;
var sql_count = @"SELECT COUNT(*) FROM (" + dup_sql + ")" ;
2015-01-24 21:59:53 +01:00
var x = cmd . ExecuteScalarInt64 ( sql_count , 0 );
if ( x > 0 )
2015-01-12 23:07:57 +01:00
{
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateBlocklistHashes" , "Found duplicate blocklisthash entries, repairing" );
2015-01-12 23:07:57 +01:00
2015-01-24 21:59:53 +01:00
var unique_count = cmd . ExecuteScalarInt64 ( @"SELECT COUNT(*) FROM (SELECT DISTINCT ""BlocksetID"", ""Index"" FROM ""BlocklistHash"")" , 0 );
2015-01-12 23:07:57 +01:00
2025-03-10 14:41:12 +01:00
using ( var c2 = m_connection . CreateCommand ( tr ))
2015-01-12 23:07:57 +01:00
{
2015-02-04 21:52:49 +01:00
c2 . CommandText = @"DELETE FROM ""BlocklistHash"" WHERE rowid IN (SELECT rowid FROM ""BlocklistHash"" WHERE ""BlocksetID"" = ? AND ""Index"" = ? LIMIT ?)" ;
2025-03-10 14:41:12 +01:00
foreach ( var rd in cmd . ExecuteReaderEnumerable ( dup_sql ))
2015-02-04 21:52:49 +01:00
{
var expected = rd . GetInt32 ( 2 ) - 1 ;
var actual = c2 . ExecuteNonQuery ( null , rd . GetValue ( 0 ), rd . GetValue ( 1 ), expected );
if ( actual != expected )
throw new Exception ( string . Format ( "Unexpected number of results after fix, got: {0}, expected: {1}" , actual , expected ));
}
2015-01-12 23:07:57 +01:00
}
cmd . CommandText = sql_count ;
2015-01-24 21:59:53 +01:00
x = cmd . ExecuteScalarInt64 ();
if ( x > 1 )
2015-01-12 23:07:57 +01:00
throw new Exception ( "Repair failed, there are still duplicate file entries!" );
2015-01-24 21:59:53 +01:00
var real_count = cmd . ExecuteScalarInt64 ( @"SELECT Count(*) FROM ""BlocklistHash""" , 0 );
2015-01-12 23:07:57 +01:00
2015-01-24 21:59:53 +01:00
if ( real_count != unique_count )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( string . Format ( "Failed to repair, result should have been {0} blocklist hashes, but result was {1} blocklist hashes" , unique_count , real_count ), "DuplicateBlocklistHashesRepairFailed" );
2015-01-12 23:07:57 +01:00
2015-11-28 21:53:46 +01:00
try
{
2016-10-18 13:27:36 +02:00
VerifyConsistency ( blocksize , hashsize , true , tr );
2015-11-28 21:53:46 +01:00
}
2025-03-10 14:41:12 +01:00
catch ( Exception ex )
2015-11-28 21:53:46 +01:00
{
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( "Repaired blocklisthashes, but the database was broken afterwards, rolled back changes" , "DuplicateBlocklistHashesRepairFailed" , ex );
2015-11-28 21:53:46 +01:00
}
2018-03-12 14:07:11 +01:00
Logging . Log . WriteInformationMessage ( LOGTAG , "DuplicateBlocklistHashesRepaired" , "Duplicate blocklisthashes repaired succesfully" );
2015-01-12 23:07:57 +01:00
tr . Commit ();
}
}
}
2016-03-17 21:51:42 +01:00
public void CheckAllBlocksAreInVolume ( string filename , IEnumerable < KeyValuePair < string , long >> blocks )
{
2025-03-10 14:41:12 +01:00
using ( var tr = m_connection . BeginTransaction ())
using ( var cmd = m_connection . CreateCommand ( tr ))
2016-03-17 21:51:42 +01:00
{
var tablename = "ProbeBlocks-" + Library . Utility . Utility . ByteArrayAsHexString ( Guid . NewGuid (). ToByteArray ());
cmd . ExecuteNonQuery ( string . Format ( @"CREATE TEMPORARY TABLE ""{0}"" (""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL)" , tablename ));
cmd . CommandText = string . Format ( @"INSERT INTO ""{0}"" (""Hash"", ""Size"") VALUES (?, ?)" , tablename );
cmd . AddParameters ( 2 );
2025-03-10 14:41:12 +01:00
foreach ( var kp in blocks )
2016-03-17 21:51:42 +01:00
{
cmd . SetParameterValue ( 0 , kp . Key );
cmd . SetParameterValue ( 1 , kp . Value );
cmd . ExecuteNonQuery ();
}
var id = cmd . ExecuteScalarInt64 ( @"SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Name"" = ?" , - 1 , filename );
var aliens = cmd . ExecuteScalarInt64 ( string . Format ( @"SELECT COUNT(*) FROM (SELECT ""A"".""VolumeID"" FROM ""{0}"" B LEFT OUTER JOIN ""Block"" A ON ""A"".""Hash"" = ""B"".""Hash"" AND ""A"".""Size"" = ""B"".""Size"") WHERE ""VolumeID"" != ? " , tablename ), 0 , id );
cmd . ExecuteNonQuery ( string . Format ( @"DROP TABLE IF EXISTS ""{0}"" " , tablename ));
if ( aliens != 0 )
throw new Exception ( string . Format ( "Not all blocks were found in {0}" , filename ));
}
}
public void CheckBlocklistCorrect ( string hash , long length , IEnumerable < string > blocklist , long blocksize , long blockhashlength )
{
2025-03-10 14:41:12 +01:00
using ( var cmd = m_connection . CreateCommand ())
2016-03-17 21:51:42 +01:00
{
var query = string . Format ( @"
SELECT
""C"".""Hash"",
""C"".""Size""
FROM
""BlocksetEntry"" A,
(
SELECT
""Y"".""BlocksetID"",
""Y"".""Hash"" AS ""BlocklistHash"",
""Y"".""Index"" AS ""BlocklistHashIndex"",
""Z"".""Size"" AS ""BlocklistSize"",
""Z"".""ID"" AS ""BlocklistHashBlockID""
FROM
""BlocklistHash"" Y,
""Block"" Z
WHERE
""Y"".""Hash"" = ""Z"".""Hash"" AND ""Y"".""Hash"" = ? AND ""Z"".""Size"" = ?
LIMIT 1
) B,
""Block"" C
WHERE
""A"".""BlocksetID"" = ""B"".""BlocksetID""
AND
""A"".""BlockID"" = ""C"".""ID""
AND
""A"".""Index"" >= ""B"".""BlocklistHashIndex"" * ({0} / {1})
AND
""A"".""Index"" < (""B"".""BlocklistHashIndex"" + 1) * ({0} / {1})
ORDER BY
""A"".""Index""
"
2025-03-10 14:41:12 +01:00
, blocksize , blockhashlength );
2016-03-17 21:51:42 +01:00
2017-10-07 09:49:46 -07:00
using ( var en = blocklist . GetEnumerator ())
2016-03-17 21:51:42 +01:00
{
2017-10-07 09:49:46 -07:00
foreach ( var r in cmd . ExecuteReaderEnumerable ( query , hash , length ))
{
if (! en . MoveNext ())
throw new Exception ( string . Format ( "Too few entries in source blocklist with hash {0}" , hash ));
if ( en . Current != r . GetString ( 0 ))
throw new Exception ( string . Format ( "Mismatch in blocklist with hash {0}" , hash ));
}
2016-03-17 21:51:42 +01:00
2017-10-07 09:49:46 -07:00
if ( en . MoveNext ())
throw new Exception ( string . Format ( "Too many source blocklist entries in {0}" , hash ));
}
2016-03-17 21:51:42 +01:00
}
}
2025-03-10 14:41:12 +01:00
public IEnumerable < string > MissingLocalFilesets ()
{
using ( var cmd = m_connection . CreateCommand ())
foreach ( var rd in cmd . ExecuteReaderEnumerable ( @"SELECT ""Name"" FROM ""RemoteVolume"" WHERE ""Type"" = ? AND ""State"" NOT IN (?, ?) AND ""ID"" NOT IN (SELECT ""VolumeID"" FROM ""Fileset"")" , RemoteVolumeType . Files . ToString (), RemoteVolumeState . Deleting . ToString (), RemoteVolumeState . Deleted . ToString ()))
yield return rd . ConvertValueToString ( 0 );
}
public IEnumerable <( long FilesetID , DateTime Timestamp , bool IsFull )> MissingRemoteFilesets ()
{
using ( var cmd = m_connection . CreateCommand ())
foreach ( var rd in cmd . ExecuteReaderEnumerable ( @"SELECT ""ID"", ""Timestamp"", ""IsFullBackup"" FROM ""Fileset"" WHERE ""VolumeID"" NOT IN (SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = ? AND ""State"" NOT IN (?, ?))" , RemoteVolumeType . Files . ToString (), RemoteVolumeState . Deleting . ToString (), RemoteVolumeState . Deleted . ToString ()))
yield return ( rd . ConvertValueToInt64 ( 0 ), ParseFromEpochSeconds ( rd . ConvertValueToInt64 ( 1 )), rd . ConvertValueToInt64 ( 2 ) == BackupType . FULL_BACKUP );
}
2016-09-15 11:39:27 +02:00
}
2013-03-30 15:23:09 +01:00
}